Skip to content
This repository was archived by the owner on Jan 21, 2026. It is now read-only.

Commit f7ae770

Browse files
authored
feat: add getProjectId and getCurrentRootSpan (#782)
1 parent 6314199 commit f7ae770

8 files changed

Lines changed: 95 additions & 45 deletions

File tree

src/cls.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ import {CLS, Func} from './cls/base';
2424
import {NullCLS} from './cls/null';
2525
import {SingularCLS} from './cls/singular';
2626
import {SpanType} from './constants';
27-
import {Span, SpanOptions} from './plugin-types';
27+
import {RootSpan} from './plugin-types';
28+
import {UNCORRELATED_ROOT_SPAN, UNTRACED_ROOT_SPAN} from './span-data';
2829
import {Trace, TraceSpan} from './trace';
2930
import {Singleton} from './util';
3031

@@ -33,7 +34,6 @@ const asyncHooksAvailable = semver.satisfies(process.version, '>=8');
3334
export interface RealRootContext {
3435
readonly span: TraceSpan;
3536
readonly trace: Trace;
36-
createChildSpan(options: SpanOptions): Span;
3737
readonly type: SpanType.ROOT;
3838
}
3939

@@ -51,7 +51,7 @@ export interface PhantomRootContext {
5151
* When we store an actual root span, the only information we need is its
5252
* current trace/span fields.
5353
*/
54-
export type RootContext = RealRootContext|PhantomRootContext;
54+
export type RootContext = RootSpan&(RealRootContext|PhantomRootContext);
5555

5656
/**
5757
* An enumeration of the possible mechanisms for supporting context propagation
@@ -101,8 +101,8 @@ export class TraceCLS implements CLS<RootContext> {
101101
private CLSClass: CLSConstructor;
102102
private enabled = false;
103103

104-
static UNCORRELATED: RootContext = {type: SpanType.UNCORRELATED};
105-
static UNTRACED: RootContext = {type: SpanType.UNTRACED};
104+
static UNCORRELATED: RootContext = UNCORRELATED_ROOT_SPAN;
105+
static UNTRACED: RootContext = UNTRACED_ROOT_SPAN;
106106

107107
/**
108108
* Stack traces are captured when a root span is started. Because the stack

src/plugin-types.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,16 @@ export interface TraceAgent {
125125
*/
126126
runInRootSpan<T>(options: RootSpanOptions, fn: (span: RootSpan) => T): T;
127127

128+
/**
129+
* Gets the active root span for the current context. This method is
130+
* guaranteed to return an object with the surface of a RootSpan object, but
131+
* it may not represent a real root span if we are not in one. Use isRealSpan
132+
* or check the `type` field to determine whether this is a real or phantom
133+
* span.
134+
* @returns An object that represents either a real or phantom root span.
135+
*/
136+
getCurrentRootSpan(): RootSpan;
137+
128138
/**
129139
* Returns a unique identifier for the currently active context. This can be
130140
* used to uniquely identify the current root span. If there is no current,
@@ -135,6 +145,12 @@ export interface TraceAgent {
135145
*/
136146
getCurrentContextId(): string|null;
137147

148+
/**
149+
* Returns the projectId that was either configured or auto-discovered by the
150+
* TraceWriter.
151+
*/
152+
getProjectId(): Promise<string>;
153+
138154
/**
139155
* Returns the projectId that was either configured or auto-discovered by the
140156
* TraceWriter. Note that the auto-discovery is done asynchronously, so this

src/trace-api.ts

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,14 @@
1616

1717
import {Logger} from '@google-cloud/common';
1818
import * as is from 'is';
19-
import * as semver from 'semver';
2019
import * as uuid from 'uuid';
2120

2221
import {cls, RootContext} from './cls';
2322
import {Constants, SpanType} from './constants';
2423
import {Func, RootSpan, RootSpanOptions, Span, SpanOptions, TraceAgent as TraceAgentInterface} from './plugin-types';
25-
import {ChildSpanData, RootSpanData, UNCORRELATED_CHILD_SPAN, UNCORRELATED_ROOT_SPAN, UNTRACED_CHILD_SPAN, UNTRACED_ROOT_SPAN} from './span-data';
26-
import {SpanKind, Trace} from './trace';
24+
import {RootSpanData, UNCORRELATED_CHILD_SPAN, UNCORRELATED_ROOT_SPAN, UNTRACED_CHILD_SPAN, UNTRACED_ROOT_SPAN} from './span-data';
2725
import {TraceLabels} from './trace-labels';
26+
import {traceWriter} from './trace-writer';
2827
import * as TracingPolicy from './tracing-policy';
2928
import * as util from './util';
3029

@@ -35,7 +34,6 @@ import * as util from './util';
3534
export interface TraceAgentConfig extends TracingPolicy.TracePolicyConfig {
3635
enhancedDatabaseReporting: boolean;
3736
ignoreContextHeader: boolean;
38-
projectId?: string;
3937
}
4038

4139
interface IncomingTraceContext {
@@ -173,21 +171,33 @@ export class TraceAgent implements TraceAgentInterface {
173171
}, rootContext);
174172
}
175173

176-
getCurrentContextId(): string|null {
174+
getCurrentRootSpan(): RootSpan {
177175
if (!this.isActive()) {
178-
return null;
176+
return UNTRACED_ROOT_SPAN;
179177
}
178+
return cls.get().getContext();
179+
}
180180

181-
const rootSpan = cls.get().getContext();
182-
if (rootSpan.type === SpanType.ROOT) {
183-
return rootSpan.trace.traceId;
181+
getCurrentContextId(): string|null {
182+
// In v3, this will be deprecated for getCurrentRootSpan.
183+
const traceContext = this.getCurrentRootSpan().getTraceContext();
184+
const parsedTraceContext = util.parseContextFromHeader(traceContext);
185+
return parsedTraceContext ? parsedTraceContext.traceId : null;
186+
}
187+
188+
getProjectId(): Promise<string> {
189+
if (traceWriter.exists() && traceWriter.get().isActive) {
190+
return traceWriter.get().getProjectId();
191+
} else {
192+
return Promise.reject(
193+
new Error('The Project ID could not be retrieved.'));
184194
}
185-
return null;
186195
}
187196

188197
getWriterProjectId(): string|null {
189-
if (this.config) {
190-
return this.config.projectId || null;
198+
// In v3, this will be deprecated for getProjectId.
199+
if (traceWriter.exists() && traceWriter.get().isActive) {
200+
return traceWriter.get().projectId;
191201
} else {
192202
return null;
193203
}

src/trace-writer.ts

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import {AxiosError} from 'axios';
1919
import * as gcpMetadata from 'gcp-metadata';
2020
import {OutgoingHttpHeaders} from 'http';
2121
import * as os from 'os';
22-
import * as util from 'util';
2322

2423
import {Constants} from './constants';
2524
import {SpanKind, Trace} from './trace';
@@ -28,6 +27,9 @@ import {Singleton} from './util';
2827

2928
const pjson = require('../../package.json');
3029

30+
// TODO(kjin): This value should be exported from @g-c/c.
31+
const NO_PROJECT_ID_TOKEN = '{{projectId}}';
32+
3133
const onUncaughtExceptionValues = ['ignore', 'flush', 'flushAndExit'];
3234

3335
const headers: OutgoingHttpHeaders = {};
@@ -52,9 +54,6 @@ export interface LabelObject { [key: string]: string; }
5254
* A class representing a service that publishes traces in the background.
5355
*/
5456
export class TraceWriter extends common.Service {
55-
// TODO(kjin): Make public members private (they're public for testing)
56-
private logger: common.Logger;
57-
private config: TraceWriterConfig;
5857
/** Stringified traces to be published */
5958
buffer: string[];
6059
/** Default labels to be attached to written spans */
@@ -71,7 +70,9 @@ export class TraceWriter extends common.Service {
7170
* @param logger The Trace Agent's logger object.
7271
* @constructor
7372
*/
74-
constructor(config: TraceWriterConfig, logger: common.Logger) {
73+
constructor(
74+
private readonly config: TraceWriterConfig,
75+
private readonly logger: common.Logger) {
7576
super(
7677
{
7778
packageJson: pjson,
@@ -82,9 +83,6 @@ export class TraceWriter extends common.Service {
8283
config);
8384

8485
this.logger = logger;
85-
// Clone the config object
86-
this.config = {...config};
87-
this.config.serviceContext = {...this.config.serviceContext};
8886
this.buffer = [];
8987
this.defaultLabels = {};
9088

@@ -214,13 +212,13 @@ export class TraceWriter extends common.Service {
214212
}
215213

216214
getProjectId() {
217-
if (this.config.projectId) {
218-
return Promise.resolve(this.config.projectId);
215+
// super.getProjectId writes to projectId, but doesn't check it first
216+
// before going through the flow of obtaining it. So we add that logic
217+
// first.
218+
if (this.projectId !== NO_PROJECT_ID_TOKEN) {
219+
return Promise.resolve(this.projectId);
219220
}
220-
return super.getProjectId().then((projectId) => {
221-
this.config.projectId = projectId;
222-
return projectId;
223-
});
221+
return super.getProjectId();
224222
}
225223

226224
/**
@@ -269,8 +267,8 @@ export class TraceWriter extends common.Service {
269267
// Any test that doesn't mock the Trace Writer will assume that traces get
270268
// buffered synchronously. We need to refactor those tests to remove that
271269
// assumption before we can make this fix.
272-
if (this.config.projectId) {
273-
afterProjectId(this.config.projectId);
270+
if (this.projectId !== NO_PROJECT_ID_TOKEN) {
271+
afterProjectId(this.projectId);
274272
} else {
275273
this.getProjectId().then(afterProjectId, (err: Error) => {
276274
// Because failing to get a project ID means that the trace agent will
@@ -329,7 +327,7 @@ export class TraceWriter extends common.Service {
329327
*/
330328
publish(json: string) {
331329
const uri = `https://cloudtrace.googleapis.com/v1/projects/${
332-
this.config.projectId}/traces`;
330+
this.projectId}/traces`;
333331
const options = {method: 'PATCH', uri, body: json, headers};
334332
this.logger.info('TraceWriter#publish: Publishing to ' + uri);
335333
this.request(options, (err, body?, response?) => {

test/test-index.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,12 @@ import { SpanType } from '../src/constants';
2222
import { FORCE_NEW } from '../src/util';
2323

2424
var assert = require('assert');
25-
var nock = require('nock');
26-
var nocks = require('./nocks'/*.js*/);
2725
var trace = require('../..');
2826

2927
var disabledAgent: TraceAgent = trace.get();
3028

3129
describe('index.js', function() {
32-
it('should get a disabled agent with `Trace.get`', function() {
30+
it('should get a disabled agent with `Trace.get`', async function() {
3331
assert.ok(!disabledAgent.isActive()); // ensure it's disabled first
3432
let ranInRootSpan = false;
3533
disabledAgent.runInRootSpan({ name: '' }, (span) => {
@@ -40,6 +38,10 @@ describe('index.js', function() {
4038
assert.strictEqual(disabledAgent.enhancedDatabaseReportingEnabled(), false);
4139
assert.strictEqual(disabledAgent.getCurrentContextId(), null);
4240
assert.strictEqual(disabledAgent.getWriterProjectId(), null);
41+
assert.strictEqual(disabledAgent.getCurrentRootSpan().type, SpanType.UNTRACED);
42+
// getting project ID should reject.
43+
await disabledAgent.getProjectId().then(
44+
() => Promise.reject(new Error()), () => Promise.resolve());
4345
assert.strictEqual(disabledAgent.createChildSpan({ name: '' }).type, SpanType.UNTRACED);
4446
assert.strictEqual(disabledAgent.getResponseTraceContext('', false), '');
4547
const fn = () => {};

test/test-trace-api.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,10 @@ describe('Trace Interface', () => {
5151
testTraceModule.setCLSForTest(TraceCLS);
5252
cls.create({mechanism: TraceCLSMechanism.ASYNC_LISTENER}, logger).enable();
5353
traceWriter
54-
.create(Object.assign({[FORCE_NEW]: true}, defaultConfig), logger)
54+
.create(
55+
Object.assign(
56+
{[FORCE_NEW]: true, projectId: 'project-1'}, defaultConfig),
57+
logger)
5558
.initialize(done);
5659
});
5760

@@ -141,6 +144,17 @@ describe('Trace Interface', () => {
141144
assert.strictEqual(testTraceModule.getTraces().length, 1);
142145
});
143146

147+
it('should return a root span when getCurrentRootSpan is called', () => {
148+
const traceAPI = createTraceAgent();
149+
// When a root span isn't running, return UNCORRELATED.
150+
assert.strictEqual(
151+
traceAPI.getCurrentRootSpan().type, SpanType.UNCORRELATED);
152+
traceAPI.runInRootSpan({name: 'root'}, (rootSpan) => {
153+
assert.strictEqual(traceAPI.getCurrentRootSpan(), rootSpan);
154+
rootSpan.endSpan();
155+
});
156+
});
157+
144158
it('should return null context id when one does not exist', () => {
145159
const traceAPI = createTraceAgent();
146160
assert.strictEqual(traceAPI.getCurrentContextId(), null);
@@ -156,9 +170,14 @@ describe('Trace Interface', () => {
156170
});
157171
});
158172

159-
it('should return get the project ID if set in config', () => {
160-
const config = {projectId: 'project-1'};
161-
const traceApi = createTraceAgent(null /* policy */, config);
173+
it('should return the project ID from the Trace Writer (promise api)',
174+
async () => {
175+
const traceApi = createTraceAgent();
176+
assert.equal(await traceApi.getProjectId(), 'project-1');
177+
});
178+
179+
it('should return get the project ID from the Trace Writer', () => {
180+
const traceApi = createTraceAgent();
162181
assert.equal(traceApi.getWriterProjectId(), 'project-1');
163182
});
164183

test/test-trace-uncaught-exception.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ describe('Trace Writer', () => {
7474
(done) => {
7575
const restoreOriginalUncaughtExceptionListeners =
7676
removeAllUncaughtExceptionListeners();
77-
const traceApi = trace.start({onUncaughtException: 'flush'});
77+
trace.start({onUncaughtException: 'flush', projectId: '0'});
7878
setImmediate(() => {
7979
setImmediate(() => {
8080
removeAllUncaughtExceptionListeners();

test/test-trace-writer.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,12 @@ describe('Trace Writer', () => {
9191
nock.disableNetConnect();
9292
oauth2Scope = oauth2().persist();
9393
shimmer.wrap(
94-
Service.prototype, 'getProjectId', () => () => getProjectIdOverride());
94+
Service.prototype, 'getProjectId', () => function(this: Service) {
95+
return getProjectIdOverride().then(projectId => {
96+
this.projectId = projectId;
97+
return projectId;
98+
});
99+
});
95100
});
96101

97102
after(() => {
@@ -117,7 +122,7 @@ describe('Trace Writer', () => {
117122
getProjectIdOverride = () => Promise.resolve('my-project');
118123
writer.initialize(err => {
119124
assert.ifError(err);
120-
assert.strictEqual(writer.getConfig().projectId, 'my-project');
125+
assert.strictEqual(writer.projectId, 'my-project');
121126
writer.stop();
122127
done();
123128
});
@@ -129,7 +134,7 @@ describe('Trace Writer', () => {
129134
getProjectIdOverride = () => Promise.resolve('my-different-project');
130135
writer.initialize(err => {
131136
assert.ifError(err);
132-
assert.strictEqual(writer.getConfig().projectId, 'my-project');
137+
assert.strictEqual(writer.projectId, 'my-project');
133138
writer.stop();
134139
done();
135140
});

0 commit comments

Comments
 (0)