Skip to content

Commit ded1901

Browse files
add open telemetry tracing (#170)
Closes: #149
2 parents ba31660 + 6052bdb commit ded1901

File tree

20 files changed

+2713
-2387
lines changed

20 files changed

+2713
-2387
lines changed

package.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@nitric/sdk",
33
"description": "Nitric NodeJS client sdk",
4-
"nitric": "v0.20.0",
4+
"nitric": "v0.22.0",
55
"author": "Nitric <https://github.com/nitrictech>",
66
"repository": "https://github.com/nitrictech/node-sdk",
77
"main": "lib/index.js",
@@ -31,6 +31,14 @@
3131
],
3232
"dependencies": {
3333
"@grpc/grpc-js": "1.8.1",
34+
"@opentelemetry/api": "^1.4.1",
35+
"@opentelemetry/exporter-trace-otlp-http": "^0.36.1",
36+
"@opentelemetry/instrumentation": "^0.36.1",
37+
"@opentelemetry/instrumentation-grpc": "^0.36.1",
38+
"@opentelemetry/instrumentation-http": "^0.36.1",
39+
"@opentelemetry/resources": "^1.10.1",
40+
"@opentelemetry/sdk-trace-node": "^1.10.1",
41+
"@opentelemetry/semantic-conventions": "^1.10.1",
3442
"google-protobuf": "3.14.0",
3543
"tslib": "^2.1.0"
3644
},

src/faas/v0/context.test.ts

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -146,9 +146,8 @@ describe('NitricTriggger.toGrpcTriggerResponse', () => {
146146
let response: TriggerResponse;
147147

148148
beforeEach(() => {
149-
const ctx: TriggerContext = TriggerContext.fromGrpcTriggerRequest(
150-
request
151-
);
149+
const ctx: TriggerContext =
150+
TriggerContext.fromGrpcTriggerRequest(request);
152151
ctx.http.res.body = 'test';
153152
response = HttpContext.toGrpcTriggerResponse(ctx);
154153
});
@@ -168,9 +167,8 @@ describe('NitricTriggger.toGrpcTriggerResponse', () => {
168167
let response: TriggerResponse;
169168

170169
beforeEach(() => {
171-
const ctx: TriggerContext = TriggerContext.fromGrpcTriggerRequest(
172-
request
173-
);
170+
const ctx: TriggerContext =
171+
TriggerContext.fromGrpcTriggerRequest(request);
174172
ctx.http.res.body = { any: 'object' };
175173
response = HttpContext.toGrpcTriggerResponse(ctx);
176174
});
@@ -190,9 +188,8 @@ describe('NitricTriggger.toGrpcTriggerResponse', () => {
190188
let response: TriggerResponse;
191189

192190
beforeEach(() => {
193-
const ctx: TriggerContext = TriggerContext.fromGrpcTriggerRequest(
194-
request
195-
);
191+
const ctx: TriggerContext =
192+
TriggerContext.fromGrpcTriggerRequest(request);
196193
ctx.http.res.body = new TextEncoder().encode('response text');
197194
response = HttpContext.toGrpcTriggerResponse(ctx);
198195
});
@@ -214,9 +211,8 @@ describe('NitricTriggger.toGrpcTriggerResponse', () => {
214211
let response: TriggerResponse;
215212

216213
beforeEach(() => {
217-
const ctx: TriggerContext = TriggerContext.fromGrpcTriggerRequest(
218-
request
219-
);
214+
const ctx: TriggerContext =
215+
TriggerContext.fromGrpcTriggerRequest(request);
220216
ctx.http.res.headers['Content-Type'] = ['application/json'];
221217
ctx.http.res.body = new TextEncoder().encode(
222218
'{"json":"which is already text"}'

src/faas/v0/context.ts

Lines changed: 54 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ import {
1717
TopicResponseContext,
1818
HttpResponseContext,
1919
HeaderValue,
20+
TraceContext,
2021
} from '@nitric/api/proto/faas/v1/faas_pb';
22+
import * as api from '@opentelemetry/api';
2123
import { jsonResponse } from './json';
2224

2325
export abstract class TriggerContext<
@@ -89,9 +91,11 @@ export abstract class TriggerContext<
8991

9092
export abstract class AbstractRequest {
9193
readonly data: string | Uint8Array;
94+
readonly traceContext: api.Context;
9295

93-
protected constructor(data: string | Uint8Array) {
96+
protected constructor(data: string | Uint8Array, traceContext: api.Context) {
9497
this.data = data;
98+
this.traceContext = traceContext;
9599
}
96100

97101
text(): string {
@@ -122,6 +126,7 @@ interface HttpRequestArgs {
122126
params: Record<string, string>;
123127
query: Record<string, string[]>;
124128
headers: Record<string, string[]>;
129+
traceContext?: api.Context;
125130
}
126131

127132
export class HttpRequest extends AbstractRequest {
@@ -131,8 +136,16 @@ export class HttpRequest extends AbstractRequest {
131136
public readonly query: Record<string, string[]>;
132137
public readonly headers: Record<string, string[] | string>;
133138

134-
constructor({ data, method, path, params, query, headers }: HttpRequestArgs) {
135-
super(data);
139+
constructor({
140+
data,
141+
method,
142+
path,
143+
params,
144+
query,
145+
headers,
146+
traceContext,
147+
}: HttpRequestArgs) {
148+
super(data, traceContext);
136149
this.method = method;
137150
this.path = path;
138151
this.params = params;
@@ -174,12 +187,28 @@ export class HttpResponse {
174187
export class EventRequest extends AbstractRequest {
175188
public readonly topic: string;
176189

177-
constructor(data: string | Uint8Array, topic: string) {
178-
super(data);
190+
constructor(
191+
data: string | Uint8Array,
192+
topic: string,
193+
traceContext: api.Context
194+
) {
195+
super(data, traceContext);
179196
this.topic = topic;
180197
}
181198
}
182199

200+
// Propagate the context to the root context
201+
const getTraceContext = (traceContext: TraceContext): api.Context => {
202+
const traceContextObject: Record<string, string> = traceContext
203+
? traceContext
204+
.getValuesMap()
205+
.toObject()
206+
.reduce((prev, [k, v]) => (prev[k] = v), {})
207+
: {};
208+
209+
return api.propagation.extract(api.context.active(), traceContextObject);
210+
};
211+
183212
export class HttpContext extends TriggerContext<HttpRequest, HttpResponse> {
184213
public get http(): HttpContext {
185214
return this;
@@ -189,23 +218,27 @@ export class HttpContext extends TriggerContext<HttpRequest, HttpResponse> {
189218
const http = trigger.getHttp();
190219
const ctx = new HttpContext();
191220

192-
const headers = ((http
193-
.getHeadersMap()
194-
// getEntryList claims to return [string, faas.HeaderValue][], but really returns [string, string[][]][]
195-
// we force the type to match the real return type.
196-
.getEntryList() as unknown) as [string, string[][]][]).reduce(
221+
const headers = (
222+
http
223+
.getHeadersMap()
224+
// getEntryList claims to return [string, faas.HeaderValue][], but really returns [string, string[][]][]
225+
// we force the type to match the real return type.
226+
.getEntryList() as unknown as [string, string[][]][]
227+
).reduce(
197228
(acc, [key, [val]]) => ({
198229
...acc,
199230
[key.toLowerCase()]: val.length === 1 ? val[0] : val,
200231
}),
201232
{}
202233
);
203234

204-
const query = ((http
205-
.getQueryParamsMap()
206-
// getEntryList claims to return [string, faas.HeaderValue][], but really returns [string, string[][]][]
207-
// we force the type to match the real return type.
208-
.getEntryList() as unknown) as [string, string[][]][]).reduce(
235+
const query = (
236+
http
237+
.getQueryParamsMap()
238+
// getEntryList claims to return [string, faas.HeaderValue][], but really returns [string, string[][]][]
239+
// we force the type to match the real return type.
240+
.getEntryList() as unknown as [string, string[][]][]
241+
).reduce(
209242
(acc, [key, [val]]) => ({
210243
...acc,
211244
[key]: val.length === 1 ? val[0] : val,
@@ -259,6 +292,7 @@ export class HttpContext extends TriggerContext<HttpRequest, HttpResponse> {
259292
// check for old headers if new headers is unpopulated. This is for backwards compatibility.
260293
headers: Object.keys(headers).length ? headers : oldHeaders,
261294
method: http.getMethod(),
295+
traceContext: getTraceContext(trigger.getTraceContext()),
262296
});
263297

264298
ctx.response = new HttpResponse({
@@ -330,7 +364,11 @@ export class EventContext extends TriggerContext<EventRequest, EventResponse> {
330364
const topic = trigger.getTopic();
331365
const ctx = new EventContext();
332366

333-
ctx.request = new EventRequest(trigger.getData_asU8(), topic.getTopic());
367+
ctx.request = new EventRequest(
368+
trigger.getData_asU8(),
369+
topic.getTopic(),
370+
getTraceContext(trigger.getTraceContext())
371+
);
334372

335373
ctx.response = {
336374
success: true,

src/faas/v0/json.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,11 @@ export const json = (): HttpMiddleware => (ctx: HttpContext, next) => {
3636
* @param ctx HttpContext
3737
* @returns HttpContext with body property set with an encoded JSON string and json headers set.
3838
*/
39-
export const jsonResponse = (ctx: HttpContext) => (
40-
data: string | number | boolean | Record<string, any>
41-
) => {
42-
ctx.res.body = new TextEncoder().encode(JSON.stringify(data));
43-
ctx.res.headers['Content-Type'] = ['application/json'];
39+
export const jsonResponse =
40+
(ctx: HttpContext) =>
41+
(data: string | number | boolean | Record<string, any>) => {
42+
ctx.res.body = new TextEncoder().encode(JSON.stringify(data));
43+
ctx.res.headers['Content-Type'] = ['application/json'];
4444

45-
return ctx;
46-
};
45+
return ctx;
46+
};

src/faas/v0/start.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import {
1818
ClientMessage,
1919
} from '@nitric/api/proto/faas/v1/faas_pb';
2020

21+
jest.mock('./traceProvider');
22+
2123
// We only need to handle half of the duplex stream
2224
class MockClientStream<Req, Resp> {
2325
public receivedMessages: Req[] = [];
@@ -59,6 +61,7 @@ describe('faas.start', () => {
5961
streamSpy = jest
6062
.spyOn(FaasServiceClient.prototype, 'triggerStream')
6163
.mockReturnValueOnce(mockStream as any);
64+
6265
const startPromise = start(f);
6366
mockStream.emit('end', 'EOF');
6467

src/faas/v0/start.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
14-
import * as grpc from '@grpc/grpc-js';
1514
import { SERVICE_BIND } from '../../constants';
1615

1716
import { FaasServiceClient } from '@nitric/api/proto/faas/v1/faas_grpc_pb';
@@ -36,19 +35,22 @@ import {
3635
createHandler,
3736
EventMiddleware,
3837
GenericMiddleware,
39-
HttpContext,
4038
HttpMiddleware,
4139
TriggerContext,
4240
TriggerMiddleware,
4341
} from '.';
4442

43+
import newTracerProvider from './traceProvider';
44+
4545
import {
4646
ApiWorkerOptions,
4747
CronWorkerOptions,
4848
RateWorkerOptions,
4949
SubscriptionWorkerOptions,
5050
} from '../../resources';
5151

52+
import * as grpc from '@grpc/grpc-js';
53+
5254
class FaasWorkerOptions {}
5355

5456
type FaasClientOptions =
@@ -117,6 +119,8 @@ export class Faas {
117119
* @returns a promise that resolves when the server terminates
118120
*/
119121
async start(...handlers: TriggerMiddleware[]): Promise<void> {
122+
const provider = newTracerProvider();
123+
120124
this.anyHandler = handlers.length && createHandler(...handlers);
121125
if (!this.httpHandler && !this.eventHandler && !this.anyHandler) {
122126
throw new Error('A handler function must be provided.');
@@ -151,10 +155,12 @@ export class Faas {
151155
let triggerType = 'Unknown';
152156
if (ctx.http) {
153157
triggerType = 'HTTP';
154-
handler = this.getHttpHandler() as GenericMiddleware<TriggerContext>;
158+
handler =
159+
this.getHttpHandler() as GenericMiddleware<TriggerContext>;
155160
} else if (ctx.event) {
156161
triggerType = 'Event';
157-
handler = this.getEventHandler() as GenericMiddleware<TriggerContext>;
162+
handler =
163+
this.getEventHandler() as GenericMiddleware<TriggerContext>;
158164
} else {
159165
console.error(
160166
`received an unexpected trigger type, are you using an outdated version of the SDK?`
@@ -261,6 +267,9 @@ export class Faas {
261267
res();
262268
});
263269
});
270+
271+
// Shutdown the trace provider, flushing the stream and stopping listeners
272+
await provider?.shutdown();
264273
}
265274
}
266275

src/faas/v0/traceProvider.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright 2021, Nitric Technologies Pty Ltd.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
import {
15+
ConsoleSpanExporter,
16+
BatchSpanProcessor,
17+
NodeTracerProvider,
18+
} from '@opentelemetry/sdk-trace-node';
19+
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
20+
import { Resource } from '@opentelemetry/resources';
21+
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
22+
import { GrpcInstrumentation } from '@opentelemetry/instrumentation-grpc';
23+
import { registerInstrumentations } from '@opentelemetry/instrumentation';
24+
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
25+
import { TraceIdRatioBasedSampler } from '@opentelemetry/sdk-trace-node';
26+
27+
/**
28+
* Creates a new node tracer provider
29+
* If it is a local run, it will output to the console. If it is run on the cloud it will output to localhost:4317
30+
* @returns a tracer provider
31+
*/
32+
const newTracerProvider = (): NodeTracerProvider => {
33+
// Add trace provider
34+
const localRun = !process.env.OTELCOL_BIN;
35+
const samplePercentage = localRun
36+
? 100 // local default to 100
37+
: Number.parseInt(process.env.NITRIC_TRACE_SAMPLE_PERCENT) || 0;
38+
39+
const provider = new NodeTracerProvider({
40+
resource: new Resource({
41+
[SemanticResourceAttributes.SERVICE_NAME]: process.env.NITRIC_STACK ?? '',
42+
[SemanticResourceAttributes.SERVICE_VERSION]:
43+
process.env.npm_package_version ?? '0.0.1',
44+
}),
45+
sampler: new TraceIdRatioBasedSampler(samplePercentage),
46+
});
47+
48+
registerInstrumentations({
49+
instrumentations: [new HttpInstrumentation(), new GrpcInstrumentation()],
50+
tracerProvider: provider,
51+
});
52+
53+
const traceExporter = localRun // If running locally
54+
? new ConsoleSpanExporter()
55+
: new OTLPTraceExporter({
56+
url: 'http://localhost:4317',
57+
});
58+
59+
const processor = new BatchSpanProcessor(traceExporter);
60+
61+
provider.addSpanProcessor(processor);
62+
provider.register();
63+
64+
return provider;
65+
};
66+
67+
export default newTracerProvider;

src/gen/proto/document/v1/document_pb.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,13 @@
1313

1414
var jspb = require('google-protobuf');
1515
var goog = jspb;
16-
var global = Function('return this')();
16+
var global = (function() {
17+
if (this) { return this; }
18+
if (typeof window !== 'undefined') { return window; }
19+
if (typeof global !== 'undefined') { return global; }
20+
if (typeof self !== 'undefined') { return self; }
21+
return Function('return this')();
22+
}.call(null));
1723

1824
var google_protobuf_struct_pb = require('google-protobuf/google/protobuf/struct_pb.js');
1925
goog.object.extend(proto, google_protobuf_struct_pb);

0 commit comments

Comments
 (0)