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: Open telemetry integration and span Id fix for nodejs logging library #1497

Merged
merged 30 commits into from
Jun 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
265a909
feat: OpenTelemetry integration
Feb 21, 2024
608c053
feat: add otel system testing with cloud tracing
Apr 11, 2024
54d4991
Add unit testing for otel context
Apr 11, 2024
d0be991
Add parseOtelContext test
Apr 12, 2024
9b1a5d3
testing entry metadata population
Apr 13, 2024
fd8eb30
Add system test suites
Apr 13, 2024
e5da43f
remove only mocha test keywords
Apr 13, 2024
809b940
Fix lint errors
Apr 15, 2024
73de242
fix lint issues
Apr 16, 2024
c254669
🦉 Updates from OwlBot post-processor
gcf-owl-bot[bot] Apr 16, 2024
1da8791
Add span end method in system tests
Apr 16, 2024
94e5f47
Merge branch 'main' into cindy/spanidfix
cindy-peng Apr 16, 2024
82a89bb
fix gts check
Apr 16, 2024
9977f11
🦉 Updates from OwlBot post-processor
gcf-owl-bot[bot] Apr 16, 2024
d2af395
Merge branch 'cindy/spanidfix' of https://github.com/googleapis/nodej…
gcf-owl-bot[bot] Apr 16, 2024
cfdff26
🦉 Updates from OwlBot post-processor
gcf-owl-bot[bot] Apr 16, 2024
5d926fd
Merge branch 'cindy/spanidfix' of https://github.com/googleapis/nodej…
gcf-owl-bot[bot] Apr 16, 2024
20e93c9
Remove comments
Apr 25, 2024
24be272
move otel packages to peer dependencies
Apr 25, 2024
2620b16
move otel packages to peer dependencies
Apr 25, 2024
e63c546
fix system test
Apr 25, 2024
7620bb5
Merge branch 'main' into cindy/spanidfix
cindy-peng Apr 26, 2024
91c3d6e
add bitmask for tracesampled flag
Apr 26, 2024
7260c2b
fix pack-n-play testing
Apr 29, 2024
dfe4202
revert pack-n-play sample configuration
Apr 29, 2024
567b953
Make otel api as dependency due to low npm version in kokoro infra
May 7, 2024
3342bb7
🦉 Updates from OwlBot post-processor
gcf-owl-bot[bot] May 7, 2024
f4dfbff
fix comments to return value directy
May 7, 2024
a411e8d
Merge branch 'main' into cindy/spanidfix
cindy-peng May 28, 2024
c7b4228
Merge branch 'main' into cindy/spanidfix
cindy-peng Jun 3, 2024
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
12 changes: 10 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@
"on-finished": "^2.3.0",
"pumpify": "^2.0.1",
"stream-events": "^1.0.5",
"uuid": "^9.0.0"
"uuid": "^9.0.0",
"@opentelemetry/api": "^1.7.0"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would peerDependency work better here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea there was some back and forth discussions here: #1497 (comment)

Basically system test would fail for now if it is imported as peer dependency since our kokoro runs on NPM 6, which doesn't install peer dependency automatically. JS language team suggested us to import OpenTelemetry/api and re-export it for now to avoid test failure and guarantee compatibility : https://chat.google.com/room/AAAAW_Lho4o/ljx6XogZJMI
Once NPM version in Kokoro infra has upgraded, I can change this to peer dependency again.

},
"devDependencies": {
"@google-cloud/bigquery": "^7.0.0",
Expand Down Expand Up @@ -93,7 +94,14 @@
"typescript": "^5.1.6",
"uglify-js": "^3.13.5",
"webpack": "^5.0.0",
"webpack-cli": "^5.0.0"
"webpack-cli": "^5.0.0",
"@opentelemetry/api": "^1.7.0",
"@opentelemetry/sdk-trace-node": "^1.23.0",
"@opentelemetry/sdk-node": "^0.50.0",
"@opentelemetry/semantic-conventions": "^1.23.0",
"@opentelemetry/sdk-trace-base": "^1.23.0",
"@opentelemetry/resources": "^1.23.0",
"@google-cloud/opentelemetry-cloud-trace-exporter": "^2.1.0"
gkevinzheng marked this conversation as resolved.
Show resolved Hide resolved
},
"engines": {
"node": ">=14.0.0"
Expand Down
58 changes: 34 additions & 24 deletions src/entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ import {
RawHttpRequest,
isRawHttpRequest,
} from './utils/http-request';
import {CloudTraceContext, getOrInjectContext} from './utils/context';
import {
CloudTraceContext,
getContextFromOtelContext,
getOrInjectContext,
} from './utils/context';

const eventId = new EventId();

Expand Down Expand Up @@ -229,16 +233,17 @@ class Entry {
const req = this.metadata.httpRequest;
if (isRawHttpRequest(req)) {
entry.httpRequest = makeHttpRequestData(req);
// Format trace and span
const traceContext = this.extractTraceFromHeaders(projectId);
if (traceContext) {
if (!this.metadata.trace && traceContext.trace)
entry.trace = traceContext.trace;
if (!this.metadata.spanId && traceContext.spanId)
entry.spanId = traceContext.spanId;
if (this.metadata.traceSampled === undefined)
entry.traceSampled = traceContext.traceSampled;
}
}

// Format trace and span
const traceContext = this.extractTraceContext(projectId);
if (traceContext) {
if (!this.metadata.trace && traceContext.trace)
entry.trace = traceContext.trace;
if (!this.metadata.spanId && traceContext.spanId)
entry.spanId = traceContext.spanId;
if (this.metadata.traceSampled === undefined)
entry.traceSampled = traceContext.traceSampled;
}
return entry;
}
Expand Down Expand Up @@ -308,26 +313,31 @@ class Entry {
const req = meta.httpRequest;
if (isRawHttpRequest(req)) {
entry.httpRequest = makeHttpRequestData(req);
// Detected trace context from headers if applicable.
const traceContext = this.extractTraceFromHeaders(projectId);
if (traceContext) {
if (!entry[TRACE_KEY] && traceContext.trace)
entry[TRACE_KEY] = traceContext.trace;
if (!entry[SPAN_ID_KEY] && traceContext.spanId)
entry[SPAN_ID_KEY] = traceContext.spanId;
if (entry[TRACE_SAMPLED_KEY] === undefined)
entry[TRACE_SAMPLED_KEY] = traceContext.traceSampled;
}
}

// Detected trace context from OpenTelemetry context or http headers if applicable.
const traceContext = this.extractTraceContext(projectId);
if (traceContext) {
if (!entry[TRACE_KEY] && traceContext.trace)
entry[TRACE_KEY] = traceContext.trace;
if (!entry[SPAN_ID_KEY] && traceContext.spanId)
entry[SPAN_ID_KEY] = traceContext.spanId;
if (entry[TRACE_SAMPLED_KEY] === undefined)
entry[TRACE_SAMPLED_KEY] = traceContext.traceSampled;
}
return entry;
}

/**
* extractTraceFromHeaders extracts trace and span information from raw HTTP
* request headers only.
* extractTraceContext extracts trace and span information from OpenTelemetry
* span context or raw HTTP request headers.
* @private
*/
private extractTraceFromHeaders(projectId: string): CloudTraceContext | null {
private extractTraceContext(projectId: string): CloudTraceContext | null {
// Extract trace context from OpenTelemetry span context.
const otelContext = getContextFromOtelContext(projectId);
if (otelContext) return otelContext;
// Extract trace context from http request headers.
const rawReq = this.metadata.httpRequest;
if (rawReq && 'headers' in rawReq) {
return getOrInjectContext(rawReq, projectId, false);
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1604,3 +1604,4 @@ module.exports.v2 = v2;
import * as protos from '../protos/protos';
export {protos};
export {v2};
export * from '@opentelemetry/api';
31 changes: 31 additions & 0 deletions src/utils/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import * as http from 'http';
import * as uuid from 'uuid';
import * as crypto from 'crypto';
import {trace, isSpanContextValid} from '@opentelemetry/api';

/** Header that carries span context across Google infrastructure. */
export const X_CLOUD_TRACE_HEADER = 'x-cloud-trace-context';
Expand Down Expand Up @@ -98,7 +99,13 @@ export function getOrInjectContext(
inject?: boolean
): CloudTraceContext {
const defaultContext = toCloudTraceContext({}, projectId);

// Get trace context from OpenTelemetry span context.
const otelContext = getContextFromOtelContext(projectId);
if (otelContext) return otelContext;

const wrapper = makeHeaderWrapper(req);

if (wrapper) {
// Detect 'traceparent' header.
const traceContext = getContextFromTraceParent(wrapper, projectId);
Expand Down Expand Up @@ -149,6 +156,30 @@ function makeCloudTraceHeader(): string {
return `${trace}/${spanId}`;
}

/**
* getContextFromOtelContext looks for the active open telemetry span context
* per Open Telemetry specifications for tracing contexts.
*
* @param projectId
*/
export function getContextFromOtelContext(
projectId: string
): CloudTraceContext | null {
const spanContext = trace.getActiveSpan()?.spanContext();
const FLAG_SAMPLED = 1; // 00000001
if (spanContext !== undefined && isSpanContextValid(spanContext)) {
const otelSpanContext = {
trace: spanContext?.traceId,
spanId: spanContext?.spanId,
traceSampled: (spanContext.traceFlags & FLAG_SAMPLED) !== 0,
};

return toCloudTraceContext(otelSpanContext, projectId);
}

return null;
}

/**
* getContextFromXCloudTrace looks for the HTTP header 'x-cloud-trace-context'
* per Google Cloud specifications for Cloud Tracing.
Expand Down
187 changes: 180 additions & 7 deletions system-test/logging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,11 @@
import {Logging, Sink, Log, Entry, TailEntriesResponse} from '../src';
import * as http from 'http';
import * as instrumentation from '../src/utils/instrumentation';

// block all attempts to chat with the metadata server (kokoro runs on GCE)
import {trace} from '@opentelemetry/api';
import {Resource} from '@opentelemetry/resources';
import {SEMRESATTRS_SERVICE_NAME} from '@opentelemetry/semantic-conventions';
import {TraceExporter} from '@google-cloud/opentelemetry-cloud-trace-exporter';
import {NodeSDK} from '@opentelemetry/sdk-node';
nock(HOST_ADDRESS)
.get(() => true)
.replyWithError({code: 'ENOTFOUND'})
Expand Down Expand Up @@ -85,7 +88,7 @@
// Fixes: https://github.com/googleapis/nodejs-logging/issues/953
async function deleteBuckets() {
const [buckets] = await storage.getBuckets({prefix: TESTS_PREFIX});
const bucketsToDelete = buckets.filter((bucket: any) => {

Check warning on line 91 in system-test/logging.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
if (bucket.metadata.timeCreated) {
return new Date(bucket.metadata.timeCreated) < twoDaysAgo;
} else {
Expand Down Expand Up @@ -270,8 +273,8 @@
});

describe('logs', () => {
function getTestLog(loggingInstnce = null) {
const log = (loggingInstnce || logging).log(generateName());
function getTestLog(loggingInstance = null) {
const log = (loggingInstance || logging).log(generateName());

const logEntries = [
// string data
Expand Down Expand Up @@ -733,15 +736,185 @@
});
});

describe('logs with open telemetry context', () => {
let sdk: NodeSDK;
before(() => {
// initialize the SDK and register with the OpenTelemetry API
// this enables the API to record telemetry
sdk = new NodeSDK({
resource: new Resource({
[SEMRESATTRS_SERVICE_NAME]: TESTS_PREFIX,
}),
// Add cloud trace exporter as SDK trace exporter
traceExporter: new TraceExporter(),
});
sdk.start();
});

after(() => {
sdk.shutdown();
});

it('should not overwrite user defined trace and spans with OpenTelemetry context', done => {
trace.getTracer(TESTS_PREFIX).startActiveSpan('foo', span => {
const {log} = getTestLog();
const spanTestMessage = 'span test log message';
const metadata = {
trace: '1',
spanId: '1',
traceSampled: false,
};
const logEntry = log.entry(metadata, spanTestMessage);
log.write(logEntry, err => {
assert.ifError(err);
getEntriesFromLog(log, {numExpectedMessages: 1}, (err, entries) => {
assert.ifError(err);
const entry = entries![0];
assert.strictEqual(entry.data, spanTestMessage);
assert.strictEqual(entry.metadata.trace, metadata.trace);
assert.strictEqual(entry.metadata.spanId, metadata.spanId);
assert.strictEqual(
entry.metadata.traceSampled,
metadata.traceSampled
);
});
});
span.end();
});
done();
});

it('should write a log with trace and spans from OpenTelemetry context', done => {
trace.getTracer(TESTS_PREFIX).startActiveSpan('foo', span => {
const traceId = span.spanContext().traceId;
const spanId = span.spanContext().spanId;
const traceSampled = (span.spanContext().traceFlags & 1) !== 0;
const {log} = getTestLog();
const spanTestMessage = 'span test log message';
const logEntry = log.entry(spanTestMessage);
log.write(logEntry, err => {
assert.ifError(err);
getEntriesFromLog(log, {numExpectedMessages: 1}, (err, entries) => {
assert.ifError(err);
const entry = entries![0];
assert.strictEqual(entry.data, spanTestMessage);
assert.strictEqual(
entry.metadata.trace,
`projects/${PROJECT_ID}/traces/${traceId}`
);
assert.strictEqual(entry.metadata.spanId, spanId);
assert.strictEqual(entry.metadata.traceSampled, traceSampled);
});
});
span.end();
});
done();
});

it('should write a log with OpenTelemetry trace and spans and ignore http requests traceparent header', done => {
const {log} = getTestLog();
const URL = 'http://www.google.com';
trace.getTracer(TESTS_PREFIX).startActiveSpan('foo', span => {
// Use the response of a http request as the incomingmessage request obj.
http.get(URL, res => {
res.url = URL;
res.headers = {
traceparent:
'00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01',
};
const metadata = {httpRequest: res};
const logEntry = log.entry(metadata, 'some log message');

const traceId = span.spanContext().traceId;
const spanId = span.spanContext().spanId;
const traceSampled = (span.spanContext().traceFlags & 1) !== 0;

log.write(logEntry, err => {
assert.ifError(err);
getEntriesFromLog(
log,
{numExpectedMessages: 1},
(err, entries) => {
assert.ifError(err);
const entry = entries![0];
assert.strictEqual(entry.data, 'some log message');
assert.strictEqual(
entry.metadata.httpRequest?.requestUrl,
URL
);
assert.strictEqual(
entry.metadata.httpRequest?.protocol,
'http:'
);
assert.strictEqual(
entry.metadata.trace,
`projects/${PROJECT_ID}/traces/${traceId}`
);
assert.strictEqual(entry.metadata.spanId, spanId);
assert.strictEqual(entry.metadata.traceSampled, traceSampled);
}
);
});
});
span.end();
});
done();
});

it('should write a log with OpenTelemetry trace and spans and ignore http requests x-cloud-trace-context header', done => {
const {log} = getTestLog();
const URL = 'http://www.google.com';
trace.getTracer(TESTS_PREFIX).startActiveSpan('foo', span => {
// Use the response of a http request as the incomingmessage request obj.
http.get(URL, res => {
res.url = URL;
res.headers = {
'x-cloud-trace-context': '1/2;o=1',
};
const metadata = {httpRequest: res};
const logEntry = log.entry(metadata, 'some log message');
const traceId = span.spanContext().traceId;
const spanId = span.spanContext().spanId;
const traceSampled = (span.spanContext().traceFlags & 1) !== 0;
log.write(logEntry, err => {
assert.ifError(err);
getEntriesFromLog(
log,
{numExpectedMessages: 1},
(err, entries) => {
assert.ifError(err);
const entry = entries![0];
assert.strictEqual(entry.data, 'some log message');
assert.strictEqual(
entry.metadata.httpRequest?.requestUrl,
URL
);
assert.strictEqual(
entry.metadata.httpRequest?.protocol,
'http:'
);
assert.strictEqual(
entry.metadata.trace,
`projects/${PROJECT_ID}/traces/${traceId}`
);
assert.strictEqual(entry.metadata.spanId, spanId);
assert.strictEqual(entry.metadata.traceSampled, traceSampled);
}
);
});
});
span.end();
});
done();
});
});

it('should set the default resource', done => {
const {log} = getTestLog();

const text = 'entry-text';
const entry = log.entry(text);

log.write(entry, err => {
assert.ifError(err);

getEntriesFromLog(log, {numExpectedMessages: 1}, (err, entries) => {
assert.ifError(err);
const entry = entries![0];
Expand Down
Loading
Loading