Skip to content

Commit 1775ce6

Browse files
authored
refactor: Add settings attributes to trace spans. (#2181)
1 parent 7fe408e commit 1775ce6

File tree

5 files changed

+176
-14
lines changed

5 files changed

+176
-14
lines changed

dev/src/index.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ export const DEFAULT_MAX_TRANSACTION_ATTEMPTS = 5;
179179
/*!
180180
* The default number of idle GRPC channel to keep.
181181
*/
182-
const DEFAULT_MAX_IDLE_CHANNELS = 1;
182+
export const DEFAULT_MAX_IDLE_CHANNELS = 1;
183183

184184
/*!
185185
* The maximum number of concurrent requests supported by a single GRPC channel,
@@ -589,7 +589,6 @@ export class Firestore implements firestore.Firestore {
589589
}
590590

591591
this.validateAndApplySettings({...settings, ...libraryHeader});
592-
593592
this._traceUtil = this.newTraceUtilInstance(this._settings);
594593

595594
const retryConfig = serviceConfig.retry_params.default;
@@ -696,6 +695,7 @@ export class Firestore implements firestore.Firestore {
696695

697696
const mergedSettings = {...this._settings, ...settings};
698697
this.validateAndApplySettings(mergedSettings);
698+
this._traceUtil = this.newTraceUtilInstance(this._settings);
699699
this._settingsFrozen = true;
700700
}
701701

@@ -789,7 +789,6 @@ export class Firestore implements firestore.Firestore {
789789
return temp;
790790
};
791791
this._serializer = new Serializer(this);
792-
this._traceUtil = this.newTraceUtilInstance(this._settings);
793792
}
794793

795794
private newTraceUtilInstance(settings: firestore.Settings): TraceUtil {
@@ -809,10 +808,6 @@ export class Firestore implements firestore.Firestore {
809808
}
810809

811810
if (createEnabledInstance) {
812-
// Re-use the existing EnabledTraceUtil if one has been created.
813-
if (this._traceUtil && this._traceUtil instanceof EnabledTraceUtil) {
814-
return this._traceUtil;
815-
}
816811
return new EnabledTraceUtil(settings);
817812
} else {
818813
return new DisabledTraceUtil();
@@ -1550,6 +1545,10 @@ export class Firestore implements firestore.Firestore {
15501545
'Detected project ID: %s',
15511546
this._projectId
15521547
);
1548+
1549+
// If the project ID was undefined when the TraceUtil was set up, we
1550+
// need to record it.
1551+
this._traceUtil.recordProjectId(this.projectId);
15531552
} catch (err) {
15541553
logger(
15551554
'Firestore.initializeIfNeeded',

dev/src/telemetry/disabled-trace-util.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,7 @@ export class DisabledTraceUtil implements TraceUtil {
3636
currentSpan(): Span {
3737
return new Span();
3838
}
39+
40+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
41+
recordProjectId(projectId: string): void {}
3942
}

dev/src/telemetry/enabled-trace-util.ts

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,17 @@ import {
2525
} from '@opentelemetry/api';
2626

2727
import {Span} from './span';
28-
import {Attributes, TraceUtil} from './trace-util';
28+
import {ATTRIBUTE_SETTINGS_PREFIX, Attributes, TraceUtil} from './trace-util';
29+
30+
import {interfaces} from '../v1/firestore_client_config.json';
31+
import {FirestoreClient} from '../v1';
32+
import {DEFAULT_DATABASE_ID} from '../path';
33+
import {DEFAULT_MAX_IDLE_CHANNELS} from '../index';
34+
const serviceConfig = interfaces['google.firestore.v1.Firestore'];
2935

3036
export class EnabledTraceUtil implements TraceUtil {
3137
private tracer: Tracer;
38+
private settingsAttributes: Attributes;
3239

3340
constructor(settings: Settings) {
3441
let tracerProvider = settings.openTelemetryOptions?.tracerProvider;
@@ -42,6 +49,85 @@ export class EnabledTraceUtil implements TraceUtil {
4249
const libVersion = require('../../../package.json').version;
4350
const libName = require('../../../package.json').name;
4451
this.tracer = tracerProvider.getTracer(libName, libVersion);
52+
53+
this.settingsAttributes = {};
54+
this.settingsAttributes['otel.scope.name'] = libName;
55+
this.settingsAttributes['otel.scope.version'] = libVersion;
56+
57+
if (settings.projectId) {
58+
this.settingsAttributes[`${ATTRIBUTE_SETTINGS_PREFIX}.project_id`] =
59+
settings.projectId;
60+
}
61+
62+
this.settingsAttributes[`${ATTRIBUTE_SETTINGS_PREFIX}.database_id`] =
63+
settings.databaseId || DEFAULT_DATABASE_ID;
64+
65+
const host =
66+
settings.servicePath ?? settings.host ?? 'firestore.googleapis.com';
67+
const port = settings.port ?? FirestoreClient.port;
68+
this.settingsAttributes[`${ATTRIBUTE_SETTINGS_PREFIX}.host`] =
69+
`${host}:${port}`;
70+
71+
if (settings.preferRest !== undefined) {
72+
this.settingsAttributes[`${ATTRIBUTE_SETTINGS_PREFIX}.prefer_REST`] =
73+
settings.preferRest;
74+
}
75+
76+
this.settingsAttributes[`${ATTRIBUTE_SETTINGS_PREFIX}.max_idle_channels`] =
77+
settings.maxIdleChannels ?? DEFAULT_MAX_IDLE_CHANNELS;
78+
79+
const defaultRetrySettings = serviceConfig.retry_params.default;
80+
const customRetrySettings =
81+
settings.clientConfig?.interfaces?.['google.firestore.v1.Firestore']?.[
82+
'retry_params'
83+
]?.['default'];
84+
this.settingsAttributes[
85+
`${ATTRIBUTE_SETTINGS_PREFIX}.initial_retry_delay`
86+
] = this.millisToSecondString(
87+
customRetrySettings?.initial_retry_delay_millis ??
88+
defaultRetrySettings.initial_retry_delay_millis
89+
);
90+
this.settingsAttributes[
91+
`${ATTRIBUTE_SETTINGS_PREFIX}.initial_rpc_timeout`
92+
] = this.millisToSecondString(
93+
customRetrySettings?.initial_rpc_timeout_millis ??
94+
defaultRetrySettings.initial_rpc_timeout_millis
95+
);
96+
this.settingsAttributes[`${ATTRIBUTE_SETTINGS_PREFIX}.total_timeout`] =
97+
this.millisToSecondString(
98+
customRetrySettings?.total_timeout_millis ??
99+
defaultRetrySettings.total_timeout_millis
100+
);
101+
this.settingsAttributes[`${ATTRIBUTE_SETTINGS_PREFIX}.max_retry_delay`] =
102+
this.millisToSecondString(
103+
customRetrySettings?.max_retry_delay_millis ??
104+
defaultRetrySettings.max_retry_delay_millis
105+
);
106+
this.settingsAttributes[`${ATTRIBUTE_SETTINGS_PREFIX}.max_rpc_timeout`] =
107+
this.millisToSecondString(
108+
customRetrySettings?.max_rpc_timeout_millis ??
109+
defaultRetrySettings.max_rpc_timeout_millis
110+
);
111+
this.settingsAttributes[
112+
`${ATTRIBUTE_SETTINGS_PREFIX}.retry_delay_multiplier`
113+
] =
114+
customRetrySettings?.retry_delay_multiplier.toString() ??
115+
defaultRetrySettings.retry_delay_multiplier.toString();
116+
this.settingsAttributes[
117+
`${ATTRIBUTE_SETTINGS_PREFIX}.rpc_timeout_multiplier`
118+
] =
119+
customRetrySettings?.rpc_timeout_multiplier.toString() ??
120+
defaultRetrySettings.rpc_timeout_multiplier.toString();
121+
}
122+
123+
recordProjectId(projectId: string): void {
124+
this.settingsAttributes[`${ATTRIBUTE_SETTINGS_PREFIX}.project_id`] =
125+
projectId;
126+
this.currentSpan().setAttributes(this.settingsAttributes);
127+
}
128+
129+
private millisToSecondString(millis: number): string {
130+
return `${millis / 1000}s`;
45131
}
46132

47133
private endSpan(otelSpan: OpenTelemetrySpan, error: Error): void {
@@ -64,6 +150,8 @@ export class EnabledTraceUtil implements TraceUtil {
64150
attributes: attributes,
65151
},
66152
(otelSpan: OpenTelemetrySpan) => {
153+
this.addCommonAttributes(otelSpan);
154+
67155
// Note that if `fn` returns a `Promise`, we want the otelSpan to end
68156
// after the `Promise` has resolved, NOT after the `fn` has returned.
69157
// Therefore, we should not use a `finally` clause to end the otelSpan.
@@ -94,10 +182,16 @@ export class EnabledTraceUtil implements TraceUtil {
94182
}
95183

96184
startSpan(name: string): Span {
97-
return new Span(this.tracer.startSpan(name, undefined, context.active()));
185+
const otelSpan = this.tracer.startSpan(name, undefined, context.active());
186+
this.addCommonAttributes(otelSpan);
187+
return new Span(otelSpan);
98188
}
99189

100190
currentSpan(): Span {
101191
return new Span(trace.getActiveSpan());
102192
}
193+
194+
addCommonAttributes(otelSpan: OpenTelemetrySpan): void {
195+
otelSpan.setAttributes(this.settingsAttributes);
196+
}
103197
}

dev/src/telemetry/trace-util.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ export const SPAN_NAME_TRANSACTION_COMMIT = 'Transaction.Commit';
5757
export const SPAN_NAME_BATCH_COMMIT = 'Batch.Commit';
5858
export const SPAN_NAME_PARTITION_QUERY = 'PartitionQuery';
5959
export const SPAN_NAME_BULK_WRITER_COMMIT = 'BulkWriter.Commit';
60+
export const ATTRIBUTE_SERVICE_PREFIX = 'gcp.firestore';
61+
export const ATTRIBUTE_SETTINGS_PREFIX = `${ATTRIBUTE_SERVICE_PREFIX}.settings`;
6062
export const ATTRIBUTE_KEY_DOC_COUNT = 'doc_count';
6163
export const ATTRIBUTE_KEY_IS_TRANSACTIONAL = 'transactional';
6264
export const ATTRIBUTE_KEY_NUM_RESPONSES = 'response_count';
@@ -74,4 +76,6 @@ export interface TraceUtil {
7476
startSpan(name: string): Span;
7577

7678
currentSpan(): Span;
79+
80+
recordProjectId(projectId: string): void;
7781
}

dev/system-test/tracing.ts

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import {setLogFunction, Firestore} from '../src';
4444
import {verifyInstance} from '../test/util/helpers';
4545
import {
4646
ATTRIBUTE_KEY_DOC_COUNT,
47+
ATTRIBUTE_SETTINGS_PREFIX,
4748
SPAN_NAME_AGGREGATION_QUERY_GET,
4849
SPAN_NAME_BATCH_COMMIT,
4950
SPAN_NAME_BATCH_GET_DOCUMENTS,
@@ -66,7 +67,6 @@ import {
6667
SPAN_NAME_TRANSACTION_RUN,
6768
} from '../src/telemetry/trace-util';
6869
import {AsyncLocalStorageContextManager} from '@opentelemetry/context-async-hooks';
69-
import {deepStrictEqual} from 'assert';
7070
import {cloudtrace_v1, auth as gAuth} from '@googleapis/cloudtrace';
7171
import Schema$Trace = cloudtrace_v1.Schema$Trace;
7272
import Schema$TraceSpan = cloudtrace_v1.Schema$TraceSpan;
@@ -306,6 +306,43 @@ describe('Tracing Tests', () => {
306306
firestore = new Firestore(settings);
307307
}
308308

309+
function getSettingsAttributes(): Attributes {
310+
const settingsAttributes: Attributes = {};
311+
settingsAttributes['otel.scope.name'] = require('../../package.json').name;
312+
settingsAttributes['otel.scope.version'] =
313+
require('../../package.json').version;
314+
settingsAttributes[`${ATTRIBUTE_SETTINGS_PREFIX}.database_id`] =
315+
firestore.databaseId;
316+
settingsAttributes[`${ATTRIBUTE_SETTINGS_PREFIX}.host`] =
317+
'firestore.googleapis.com:443';
318+
settingsAttributes[`${ATTRIBUTE_SETTINGS_PREFIX}.prefer_REST`] =
319+
testConfig.preferRest;
320+
settingsAttributes[`${ATTRIBUTE_SETTINGS_PREFIX}.max_idle_channels`] = 1;
321+
settingsAttributes[`${ATTRIBUTE_SETTINGS_PREFIX}.initial_retry_delay`] =
322+
'0.1s';
323+
settingsAttributes[`${ATTRIBUTE_SETTINGS_PREFIX}.initial_rpc_timeout`] =
324+
'60s';
325+
settingsAttributes[`${ATTRIBUTE_SETTINGS_PREFIX}.total_timeout`] = '600s';
326+
settingsAttributes[`${ATTRIBUTE_SETTINGS_PREFIX}.max_retry_delay`] = '60s';
327+
settingsAttributes[`${ATTRIBUTE_SETTINGS_PREFIX}.max_rpc_timeout`] = '60s';
328+
settingsAttributes[`${ATTRIBUTE_SETTINGS_PREFIX}.retry_delay_multiplier`] =
329+
'1.3';
330+
settingsAttributes[`${ATTRIBUTE_SETTINGS_PREFIX}.rpc_timeout_multiplier`] =
331+
'1';
332+
333+
// Project ID is not set on the Firestore object until _after_ the first
334+
// operation is done. Therefore, the spans that are created _before_ the
335+
// first operation do not contain a project ID.
336+
try {
337+
const projectId = firestore.projectId;
338+
settingsAttributes[`${ATTRIBUTE_SETTINGS_PREFIX}.project_id`] = projectId;
339+
} catch (e) {
340+
// Project ID has not been set yet.
341+
}
342+
343+
return settingsAttributes;
344+
}
345+
309346
// Take a function and runs it inside a new root span. This makes it possible to
310347
// encapsulate all the SDK-generated spans inside a test root span. It also makes
311348
// it easy to query a trace storage backend for a known trace ID and span Id.
@@ -588,7 +625,26 @@ describe('Tracing Tests', () => {
588625
parentSpan.traceId,
589626
`'${childSpan.name}' and '${parentSpan.name}' spans do not belong to the same trace`
590627
);
591-
// TODO(tracing): expect that each span has the needed attributes.
628+
629+
// The Cloud Trace API does not return span attributes and events.
630+
if (!testConfig.e2e) {
631+
const settingsAttributes = getSettingsAttributes();
632+
for (const attributesKey in settingsAttributes) {
633+
if (
634+
attributesKey.endsWith('.project_id') &&
635+
i + 1 !== matchingSpanHierarchy.length
636+
) {
637+
// Project ID is not set on the Firestore object until _after_ the first
638+
// operation is done. Therefore, the spans that are created _before_ the
639+
// first operation do not contain a project ID. So, we'll just compare
640+
// this attribute on the leaf spans.
641+
} else {
642+
expect(childSpan.attributes[attributesKey]).to.be.equal(
643+
settingsAttributes[attributesKey]
644+
);
645+
}
646+
}
647+
}
592648
}
593649
}
594650

@@ -597,7 +653,7 @@ describe('Tracing Tests', () => {
597653
spanName: string,
598654
attributes: Attributes
599655
): void {
600-
// TODO(tracing): The current Cloud Trace API does not return span attributes and events.
656+
// The Cloud Trace API does not return span attributes and events.
601657
if (testConfig.e2e) {
602658
return;
603659
}
@@ -606,8 +662,14 @@ describe('Tracing Tests', () => {
606662
const span = getSpanByName(spanName);
607663
expect(span).to.not.be.null;
608664

609-
// Assert that the attributes are the same.
610-
deepStrictEqual(span!.attributes, attributes);
665+
// Assert that the expected attributes are present in the span attributes.
666+
// Note that the span attributes may be a superset of the attributes passed
667+
// to this function.
668+
for (const attributesKey in attributes) {
669+
expect(span!.attributes[attributesKey]).to.be.equal(
670+
attributes[attributesKey]
671+
);
672+
}
611673
}
612674

613675
describe(IN_MEMORY_TEST_SUITE_TITLE, () => {

0 commit comments

Comments
 (0)