Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Enable tracing via OpenTelemetry. #2218

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions dev/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -792,8 +792,7 @@ export class Firestore implements firestore.Firestore {
}

private newTraceUtilInstance(settings: firestore.Settings): TraceUtil {
// Take the tracing option from the settings.
let createEnabledInstance = settings.openTelemetryOptions?.enableTracing;
let createEnabledInstance = true;

// The environment variable can override options to enable/disable telemetry collection.
if ('FIRESTORE_ENABLE_TRACING' in process.env) {
Expand Down
7 changes: 6 additions & 1 deletion dev/src/reference/query-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ export class QueryUtil<
const tag = requestTag();
const startTime = Date.now();
const isExplain = explainOptions !== undefined;
const methodName = 'runQuery';

let numDocumentsReceived = 0;
let lastReceivedDocument: QueryDocumentSnapshot<
Expand Down Expand Up @@ -245,6 +246,11 @@ export class QueryUtil<

if (proto.done) {
logger('QueryUtil._stream', tag, 'Trigger Logical Termination.');
this._firestore._traceUtil
.currentSpan()
.addEvent(
`Firestore.${methodName}: Received RunQueryResponse.Done.`
);
backendStream.unpipe(stream);
backendStream.resume();
backendStream.end();
Expand All @@ -265,7 +271,6 @@ export class QueryUtil<
let streamActive: Deferred<boolean>;
do {
streamActive = new Deferred<boolean>();
const methodName = 'runQuery';

this._firestore._traceUtil
.currentSpan()
Expand Down
4 changes: 4 additions & 0 deletions dev/src/telemetry/disabled-trace-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
import {Attributes, TraceUtil} from './trace-util';
import {Span} from './span';

/**
* @private
* @internal
*/
export class DisabledTraceUtil implements TraceUtil {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
startSpan(name: string): Span {
Expand Down
27 changes: 23 additions & 4 deletions dev/src/telemetry/enabled-trace-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
trace,
Tracer,
Span as OpenTelemetrySpan,
TracerProvider,
} from '@opentelemetry/api';

import {Span} from './span';
Expand All @@ -33,22 +34,40 @@ import {DEFAULT_DATABASE_ID} from '../path';
import {DEFAULT_MAX_IDLE_CHANNELS} from '../index';
const serviceConfig = interfaces['google.firestore.v1.Firestore'];

/**
* @private
* @internal
*/
export class EnabledTraceUtil implements TraceUtil {
private tracer: Tracer;
private settingsAttributes: Attributes;

// Visible for testing
tracerProvider: TracerProvider;

constructor(settings: Settings) {
let tracerProvider = settings.openTelemetryOptions?.tracerProvider;
let provider: TracerProvider | undefined =
settings.openTelemetryOptions?.tracerProvider;

// If a TracerProvider has not been given to us, we try to use the global one.
if (!tracerProvider) {
if (!provider) {
const {trace} = require('@opentelemetry/api');
tracerProvider = trace.getTracerProvider();
provider = trace.getTracerProvider();
}

// At this point provider is guaranteed to be defined because
// `trace.getTracerProvider()` does not return null or undefined.
this.tracerProvider = provider!;

const libVersion = require('../../../package.json').version;
const libName = require('../../../package.json').name;
this.tracer = tracerProvider.getTracer(libName, libVersion);
try {
this.tracer = this.tracerProvider.getTracer(libName, libVersion);
} catch (e) {
throw new Error(
"The object provided for 'tracerProvider' does not conform to the TracerProvider interface."
);
}

this.settingsAttributes = {};
this.settingsAttributes['otel.scope.name'] = libName;
Expand Down
4 changes: 4 additions & 0 deletions dev/src/telemetry/span.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
import {Span as OpenTelemetrySpan} from '@opentelemetry/api';
import {Attributes} from './trace-util';

/**
* @private
* @internal
*/
export class Span {
constructor(private span?: OpenTelemetrySpan) {}

Expand Down
13 changes: 13 additions & 0 deletions dev/src/telemetry/trace-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,18 @@

import {Span} from './span';

/**
* @private
* @internal
*/
export interface Attributes {
[attributeKey: string]: AttributeValue | undefined;
}

/**
* @private
* @internal
*/
export declare type AttributeValue =
| string
| number
Expand Down Expand Up @@ -67,6 +76,10 @@ export const ATTRIBUTE_KEY_TRANSACTION_TYPE = 'transaction_type';
export const ATTRIBUTE_KEY_ATTEMPTS_ALLOWED = 'attempts_allowed';
export const ATTRIBUTE_KEY_ATTEMPTS_REMAINING = 'attempts_remaining';

/**
* @private
* @internal
*/
export interface TraceUtil {
startActiveSpan<F extends (span: Span) => unknown>(
name: string,
Expand Down
58 changes: 48 additions & 10 deletions dev/system-test/tracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import {
Context as OpenTelemetryContext,
} from '@opentelemetry/api';
import {TraceExporter} from '@google-cloud/opentelemetry-cloud-trace-exporter';
import {Settings} from '@google-cloud/firestore';
import {FirestoreOpenTelemetryOptions, Settings} from '@google-cloud/firestore';
import {
AlwaysOnSampler,
BatchSpanProcessor,
Expand Down Expand Up @@ -96,13 +96,6 @@ setLogFunction((msg: string) => {
console.log(`LOG: ${msg}`);
});

// TODO(tracing): This should be moved to firestore.d.ts when we want to
// release the feature.
export interface FirestoreOpenTelemetryOptions {
enableTracing?: boolean;
tracerProvider?: any;
}

interface TestConfig {
// In-Memory tests check trace correctness by inspecting traces in memory by
// utilizing InMemorySpanExporter. These tests have `e2e` set to `false`.
Expand Down Expand Up @@ -192,7 +185,6 @@ describe('Tracing Tests', () => {
tracerProvider: TracerProvider
): FirestoreOpenTelemetryOptions {
const options: FirestoreOpenTelemetryOptions = {
enableTracing: true,
tracerProvider: undefined,
};

Expand Down Expand Up @@ -660,7 +652,7 @@ describe('Tracing Tests', () => {

// Expect that the span exists first.
const span = getSpanByName(spanName);
expect(span).to.not.be.null;
expect(span, `Could not find the span named ${spanName}`).to.not.be.null;

// Assert that the expected attributes are present in the span attributes.
// Note that the span attributes may be a superset of the attributes passed
Expand All @@ -672,6 +664,29 @@ describe('Tracing Tests', () => {
}
}

// Ensures that the given span exists and has the given attributes.
function expectSpanHasEvents(spanName: string, eventNames: string[]): void {
// The Cloud Trace API does not return span attributes and events.
if (testConfig.e2e) {
return;
}

// Expect that the span exists first.
const span = getSpanByName(spanName);
expect(span, `Could not find the span named ${spanName}`).to.not.be.null;

// Assert that the expected attributes are present in the span attributes.
// Note that the span attributes may be a superset of the attributes passed
// to this function.
if (span?.events) {
const numEvents = eventNames.length;
expect(numEvents).to.equal(span.events.length);
for (let i = 0; i < numEvents; ++i) {
expect(span.events[i].name).to.equal(eventNames[i]);
}
}
}

describe(IN_MEMORY_TEST_SUITE_TITLE, () => {
describe(NON_GLOBAL_OTEL_TEST_SUITE_TITLE, () => {
describe(GRPC_TEST_SUITE_TITLE, () => {
Expand Down Expand Up @@ -753,6 +768,11 @@ describe('Tracing Tests', () => {
SPAN_NAME_DOC_REF_GET,
SPAN_NAME_BATCH_GET_DOCUMENTS
);
expectSpanHasEvents(SPAN_NAME_BATCH_GET_DOCUMENTS, [
'Firestore.batchGetDocuments: Start',
'Firestore.batchGetDocuments: First response received',
'Firestore.batchGetDocuments: Completed',
]);
});

it('document reference create()', async () => {
Expand Down Expand Up @@ -820,6 +840,11 @@ describe('Tracing Tests', () => {
);
await waitForCompletedSpans(2);
expectSpanHierarchy(SPAN_NAME_TEST_ROOT, SPAN_NAME_AGGREGATION_QUERY_GET);
expectSpanHasEvents(SPAN_NAME_AGGREGATION_QUERY_GET, [
'Firestore.runAggregationQuery: Start',
'Firestore.runAggregationQuery: First response received',
'Firestore.runAggregationQuery: Completed',
]);
});

it('collection reference add()', async () => {
Expand Down Expand Up @@ -852,6 +877,13 @@ describe('Tracing Tests', () => {
);
await waitForCompletedSpans(2);
expectSpanHierarchy(SPAN_NAME_TEST_ROOT, SPAN_NAME_QUERY_GET);
expectSpanHasEvents(SPAN_NAME_QUERY_GET, [
'RunQuery',
'Firestore.runQuery: Start',
'Firestore.runQuery: First response received',
'Firestore.runQuery: Received RunQueryResponse.Done.',
'Firestore.runQuery: Completed',
]);
});

it('firestore getAll()', async () => {
Expand All @@ -862,6 +894,11 @@ describe('Tracing Tests', () => {
);
await waitForCompletedSpans(2);
expectSpanHierarchy(SPAN_NAME_TEST_ROOT, SPAN_NAME_BATCH_GET_DOCUMENTS);
expectSpanHasEvents(SPAN_NAME_BATCH_GET_DOCUMENTS, [
'Firestore.batchGetDocuments: Start',
'Firestore.batchGetDocuments: First response received',
'Firestore.batchGetDocuments: Completed',
]);
});

it('transaction', async () => {
Expand Down Expand Up @@ -920,6 +957,7 @@ describe('Tracing Tests', () => {
await runFirestoreOperationInRootSpan(async () => {
const query = firestore.collectionGroup('foo');
let numPartitions = 0;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for await (const partition of query.getPartitions(3)) {
numPartitions++;
}
Expand Down
Loading
Loading