@prairielearn/opentelemetry 1.5.1 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -1,289 +1,6 @@
1
- import { Metadata, credentials } from '@grpc/grpc-js';
2
-
3
- import { tracing } from '@opentelemetry/sdk-node';
4
- import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
5
- import {
6
- SpanExporter,
7
- ReadableSpan,
8
- SpanProcessor,
9
- SimpleSpanProcessor,
10
- ParentBasedSampler,
11
- TraceIdRatioBasedSampler,
12
- AlwaysOnSampler,
13
- AlwaysOffSampler,
14
- Sampler,
15
- } from '@opentelemetry/sdk-trace-base';
16
- import { detectResources, Resource } from '@opentelemetry/resources';
17
- import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
18
- import { ExpressLayerType } from '@opentelemetry/instrumentation-express';
19
- import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
20
- import { Span, SpanStatusCode, context, trace } from '@opentelemetry/api';
21
- import { hrTimeToMilliseconds } from '@opentelemetry/core';
22
-
23
- // Exporters go here.
24
- import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc';
25
- import { JaegerExporter } from '@opentelemetry/exporter-jaeger';
26
-
27
- // Instrumentations go here.
28
- import { AwsInstrumentation } from '@opentelemetry/instrumentation-aws-sdk';
29
- import { ConnectInstrumentation } from '@opentelemetry/instrumentation-connect';
30
- import { DnsInstrumentation } from '@opentelemetry/instrumentation-dns';
31
- import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express';
32
- import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
33
- import { PgInstrumentation } from '@opentelemetry/instrumentation-pg';
34
- import { RedisInstrumentation } from '@opentelemetry/instrumentation-redis';
35
-
36
- // Resource detectors go here.
37
- import { awsEc2Detector } from '@opentelemetry/resource-detector-aws';
38
- import { processDetector, envDetector } from '@opentelemetry/resources';
39
-
40
- /**
41
- * Extends `BatchSpanProcessor` to give it the ability to filter out spans
42
- * before they're queued up to send. This enhances our sampling process so
43
- * that we can filter spans _after_ they've been emitted.
44
- */
45
- class FilterBatchSpanProcessor extends BatchSpanProcessor {
46
- private filter: (span: ReadableSpan) => boolean;
47
-
48
- constructor(exporter: SpanExporter, filter: (span: ReadableSpan) => boolean) {
49
- super(exporter);
50
- this.filter = filter;
51
- }
52
-
53
- /**
54
- * This is invoked after a span is "finalized". `super.onEnd` will queue up
55
- * the span to be exported, but if we don't call that, we can just drop the
56
- * span and the parent will be none the wiser!
57
- */
58
- onEnd(span: ReadableSpan) {
59
- if (!this.filter(span)) return;
60
-
61
- super.onEnd(span);
62
- }
63
- }
64
-
65
- /**
66
- * This will be used with our {@link FilterBatchSpanProcessor} to filter out
67
- * events that we're not interested in. This helps reduce our event volume
68
- * but still gives us fine-grained control over which events we keep.
69
- */
70
- function filter(span: ReadableSpan) {
71
- if (span.name === 'pg-pool.connect') {
72
- // Looking at historical data, this generally happens in under a millisecond,
73
- // precisely because we maintain a pool of long-lived connections. The only
74
- // time obtaining a client should take longer than that is if we're
75
- // establishing a connection for the first time, which should happen only at
76
- // bootup, or if a connection errors out. Those are the cases we're
77
- // interested in, so we'll filter accordingly.
78
- return hrTimeToMilliseconds(span.duration) > 1;
79
- }
80
-
81
- // Always return true so that we default to including a span.
82
- return true;
83
- }
84
-
85
- const instrumentations = [
86
- new AwsInstrumentation(),
87
- new ConnectInstrumentation(),
88
- new DnsInstrumentation(),
89
- new ExpressInstrumentation({
90
- // We use a lot of middleware; it makes the traces way too noisy. If we
91
- // want telemetry on a particular middleware, we should instrument it
92
- // manually.
93
- ignoreLayersType: [ExpressLayerType.MIDDLEWARE],
94
- ignoreLayers: [
95
- // These don't provide useful information to us.
96
- 'router - /',
97
- 'request handler - /*',
98
- ],
99
- }),
100
- new HttpInstrumentation({
101
- ignoreIncomingPaths: [
102
- // socket.io requests are generally just long-polling; they don't add
103
- // useful information for us.
104
- /\/socket.io\//,
105
- // We get several of these per second; they just chew through our event quota.
106
- // They don't really do anything interesting anyways.
107
- /\/pl\/webhooks\/ping/,
108
- ],
109
- }),
110
- new PgInstrumentation(),
111
- new RedisInstrumentation(),
112
- ];
113
-
114
- // Enable all instrumentations now, even though we haven't configured our
115
- // span processors or trace exporters yet. We'll set those up later.
116
- instrumentations.forEach((i) => {
117
- i.enable();
118
- });
119
-
120
- let tracerProvider: NodeTracerProvider;
121
-
122
- export interface OpenTelemetryConfig {
123
- openTelemetryEnabled: boolean;
124
- openTelemetryExporter: 'console' | 'honeycomb' | 'jaeger' | SpanExporter;
125
- openTelemetrySamplerType: 'always-on' | 'always-off' | 'trace-id-ratio';
126
- openTelemetrySampleRate?: number;
127
- openTelemetrySpanProcessor?: 'batch' | 'simple';
128
- honeycombApiKey?: string;
129
- honeycombDataset?: string;
130
- serviceName?: string;
131
- }
132
-
133
- /**
134
- * Should be called once we've loaded our config; this will allow us to set up
135
- * the correct metadata for the Honeycomb exporter. We don't actually have that
136
- * information available until we've loaded our config.
137
- */
138
- export async function init(config: OpenTelemetryConfig) {
139
- if (!config.openTelemetryEnabled) {
140
- // If not enabled, do nothing. We used to disable the instrumentations, but
141
- // per maintainers, that can actually be problematic. See the comments on
142
- // https://github.com/open-telemetry/opentelemetry-js-contrib/issues/970
143
- // The Express instrumentation also logs a benign error, which can be
144
- // confusing to users. There's a fix in progress if we want to switch back
145
- // to disabling instrumentations in the future:
146
- // https://github.com/open-telemetry/opentelemetry-js-contrib/pull/972
147
- return;
148
- }
149
-
150
- let exporter: SpanExporter;
151
- if (typeof config.openTelemetryExporter === 'object') {
152
- exporter = config.openTelemetryExporter;
153
- } else {
154
- switch (config.openTelemetryExporter) {
155
- case 'console': {
156
- // Export spans to the console for testing purposes.
157
- exporter = new tracing.ConsoleSpanExporter();
158
- break;
159
- }
160
- case 'honeycomb': {
161
- // Create a Honeycomb exporter with the appropriate metadata from the
162
- // config we've been provided with.
163
- const metadata = new Metadata();
164
-
165
- metadata.set('x-honeycomb-team', config.honeycombApiKey);
166
- metadata.set('x-honeycomb-dataset', config.honeycombDataset);
167
-
168
- exporter = new OTLPTraceExporter({
169
- url: 'grpc://api.honeycomb.io:443/',
170
- credentials: credentials.createSsl(),
171
- metadata,
172
- });
173
- break;
174
- }
175
- case 'jaeger': {
176
- exporter = new JaegerExporter({
177
- // By default, the UDP sender will be used, but that causes issues
178
- // with packet sizes when Jaeger is running in Docker. We'll instead
179
- // configure it to use the HTTP sender, which shouldn't face those
180
- // same issues. We'll still allow the endpoint to be overridden via
181
- // environment variable if needed.
182
- endpoint:
183
- process.env.OTEL_EXPORTER_JAEGER_ENDPOINT ?? 'http://localhost:14268/api/traces',
184
- });
185
- break;
186
- }
187
- default:
188
- throw new Error(`Unknown OpenTelemetry exporter: ${config.openTelemetryExporter}`);
189
- }
190
- }
191
-
192
- let sampler: Sampler;
193
- switch (config.openTelemetrySamplerType ?? 'always-on') {
194
- case 'always-on': {
195
- sampler = new AlwaysOnSampler();
196
- break;
197
- }
198
- case 'always-off': {
199
- sampler = new AlwaysOffSampler();
200
- break;
201
- }
202
- case 'trace-id-ratio': {
203
- sampler = new ParentBasedSampler({
204
- root: new TraceIdRatioBasedSampler(config.openTelemetrySampleRate),
205
- });
206
- break;
207
- }
208
- default:
209
- throw new Error(`Unknown OpenTelemetry sampler type: ${config.openTelemetrySamplerType}`);
210
- }
211
-
212
- let spanProcessor: SpanProcessor;
213
- switch (config.openTelemetrySpanProcessor ?? 'batch') {
214
- case 'batch': {
215
- spanProcessor = new FilterBatchSpanProcessor(exporter, filter);
216
- break;
217
- }
218
- case 'simple': {
219
- spanProcessor = new SimpleSpanProcessor(exporter);
220
- break;
221
- }
222
- default: {
223
- throw new Error(`Unknown OpenTelemetry span processor: ${config.openTelemetrySpanProcessor}`);
224
- }
225
- }
226
-
227
- // Much of this functionality is copied from `@opentelemetry/sdk-node`, but
228
- // we can't use the SDK directly because of the fact that we load our config
229
- // asynchronously. We need to initialize our instrumentations first; only
230
- // then can we actually start requiring all of our code that loads our config
231
- // and ultimately tells us how to configure OpenTelemetry.
232
-
233
- let resource = await detectResources({
234
- detectors: [awsEc2Detector, processDetector, envDetector],
235
- });
236
-
237
- if (config.serviceName) {
238
- resource = resource.merge(
239
- new Resource({ [SemanticResourceAttributes.SERVICE_NAME]: config.serviceName })
240
- );
241
- }
242
-
243
- tracerProvider = new NodeTracerProvider({
244
- sampler,
245
- resource,
246
- });
247
- tracerProvider.addSpanProcessor(spanProcessor);
248
- tracerProvider.register();
249
-
250
- instrumentations.forEach((i) => i.setTracerProvider(tracerProvider));
251
- }
252
-
253
- /**
254
- * Gracefully shuts down the OpenTelemetry instrumentation. Should be called
255
- * when a `SIGTERM` signal is handled.
256
- */
257
- export async function shutdown(): Promise<void> {
258
- if (tracerProvider) {
259
- await tracerProvider.shutdown();
260
- tracerProvider = null;
261
- }
262
- }
263
-
264
- export async function instrumented<T>(
265
- name: string,
266
- fn: (span: Span) => Promise<T> | T
267
- ): Promise<T> {
268
- return trace
269
- .getTracer('default')
270
- .startActiveSpan<(span: Span) => Promise<T>>(name, async (span) => {
271
- try {
272
- const result = await fn(span);
273
- span.setStatus({ code: SpanStatusCode.OK });
274
- return result;
275
- } catch (e) {
276
- span.setStatus({
277
- code: SpanStatusCode.ERROR,
278
- message: e.message,
279
- });
280
- span.recordException(e);
281
- throw e;
282
- } finally {
283
- span.end();
284
- }
285
- });
286
- }
287
-
288
- export { trace, context, SpanStatusCode } from '@opentelemetry/api';
1
+ export { trace, metrics, context, SpanStatusCode, ValueType } from '@opentelemetry/api';
289
2
  export { suppressTracing } from '@opentelemetry/core';
3
+
4
+ export { init, shutdown } from './init';
5
+ export { instrumented } from './tracing';
6
+ export { instrumentedWithMetrics } from './metrics';
package/src/init.ts ADDED
@@ -0,0 +1,317 @@
1
+ import { Metadata, credentials } from '@grpc/grpc-js';
2
+
3
+ import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
4
+ import {
5
+ PeriodicExportingMetricReader,
6
+ MeterProvider,
7
+ PushMetricExporter,
8
+ ConsoleMetricExporter,
9
+ AggregationTemporality,
10
+ } from '@opentelemetry/sdk-metrics';
11
+ import {
12
+ SpanExporter,
13
+ ReadableSpan,
14
+ SpanProcessor,
15
+ SimpleSpanProcessor,
16
+ BatchSpanProcessor,
17
+ ParentBasedSampler,
18
+ TraceIdRatioBasedSampler,
19
+ AlwaysOnSampler,
20
+ AlwaysOffSampler,
21
+ Sampler,
22
+ ConsoleSpanExporter,
23
+ } from '@opentelemetry/sdk-trace-base';
24
+ import { detectResources, Resource } from '@opentelemetry/resources';
25
+ import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
26
+ import { ExpressLayerType } from '@opentelemetry/instrumentation-express';
27
+ import { metrics } from '@opentelemetry/api';
28
+ import { hrTimeToMilliseconds } from '@opentelemetry/core';
29
+
30
+ // Exporters go here.
31
+ import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc';
32
+ import { JaegerExporter } from '@opentelemetry/exporter-jaeger';
33
+ import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-grpc';
34
+
35
+ // Instrumentations go here.
36
+ import { AwsInstrumentation } from '@opentelemetry/instrumentation-aws-sdk';
37
+ import { ConnectInstrumentation } from '@opentelemetry/instrumentation-connect';
38
+ import { DnsInstrumentation } from '@opentelemetry/instrumentation-dns';
39
+ import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express';
40
+ import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
41
+ import { PgInstrumentation } from '@opentelemetry/instrumentation-pg';
42
+ import { RedisInstrumentation } from '@opentelemetry/instrumentation-redis';
43
+
44
+ // Resource detectors go here.
45
+ import { awsEc2Detector } from '@opentelemetry/resource-detector-aws';
46
+ import { processDetector, envDetector } from '@opentelemetry/resources';
47
+
48
+ /**
49
+ * Extends `BatchSpanProcessor` to give it the ability to filter out spans
50
+ * before they're queued up to send. This enhances our sampling process so
51
+ * that we can filter spans _after_ they've been emitted.
52
+ */
53
+ class FilterBatchSpanProcessor extends BatchSpanProcessor {
54
+ private filter: (span: ReadableSpan) => boolean;
55
+
56
+ constructor(exporter: SpanExporter, filter: (span: ReadableSpan) => boolean) {
57
+ super(exporter);
58
+ this.filter = filter;
59
+ }
60
+
61
+ /**
62
+ * This is invoked after a span is "finalized". `super.onEnd` will queue up
63
+ * the span to be exported, but if we don't call that, we can just drop the
64
+ * span and the parent will be none the wiser!
65
+ */
66
+ onEnd(span: ReadableSpan) {
67
+ if (!this.filter(span)) return;
68
+
69
+ super.onEnd(span);
70
+ }
71
+ }
72
+
73
+ /**
74
+ * This will be used with our {@link FilterBatchSpanProcessor} to filter out
75
+ * events that we're not interested in. This helps reduce our event volume
76
+ * but still gives us fine-grained control over which events we keep.
77
+ */
78
+ function filter(span: ReadableSpan) {
79
+ if (span.name === 'pg-pool.connect') {
80
+ // Looking at historical data, this generally happens in under a millisecond,
81
+ // precisely because we maintain a pool of long-lived connections. The only
82
+ // time obtaining a client should take longer than that is if we're
83
+ // establishing a connection for the first time, which should happen only at
84
+ // bootup, or if a connection errors out. Those are the cases we're
85
+ // interested in, so we'll filter accordingly.
86
+ return hrTimeToMilliseconds(span.duration) > 1;
87
+ }
88
+
89
+ // Always return true so that we default to including a span.
90
+ return true;
91
+ }
92
+
93
+ const instrumentations = [
94
+ new AwsInstrumentation(),
95
+ new ConnectInstrumentation(),
96
+ new DnsInstrumentation(),
97
+ new ExpressInstrumentation({
98
+ // We use a lot of middleware; it makes the traces way too noisy. If we
99
+ // want telemetry on a particular middleware, we should instrument it
100
+ // manually.
101
+ ignoreLayersType: [ExpressLayerType.MIDDLEWARE],
102
+ ignoreLayers: [
103
+ // These don't provide useful information to us.
104
+ 'router - /',
105
+ 'request handler - /*',
106
+ ],
107
+ }),
108
+ new HttpInstrumentation({
109
+ ignoreIncomingPaths: [
110
+ // socket.io requests are generally just long-polling; they don't add
111
+ // useful information for us.
112
+ /\/socket.io\//,
113
+ // We get several of these per second; they just chew through our event quota.
114
+ // They don't really do anything interesting anyways.
115
+ /\/pl\/webhooks\/ping/,
116
+ ],
117
+ }),
118
+ new PgInstrumentation(),
119
+ new RedisInstrumentation(),
120
+ ];
121
+
122
+ // Enable all instrumentations now, even though we haven't configured our
123
+ // span processors or trace exporters yet. We'll set those up later.
124
+ instrumentations.forEach((i) => {
125
+ i.enable();
126
+ });
127
+
128
+ let tracerProvider: NodeTracerProvider | null;
129
+
130
+ export interface OpenTelemetryConfig {
131
+ openTelemetryEnabled: boolean;
132
+ openTelemetryExporter: 'console' | 'honeycomb' | 'jaeger' | SpanExporter;
133
+ openTelemetryMetricExporter?: 'console' | 'honeycomb' | PushMetricExporter;
134
+ openTelemetryMetricExportIntervalMillis?: number;
135
+ openTelemetrySamplerType: 'always-on' | 'always-off' | 'trace-id-ratio';
136
+ openTelemetrySampleRate?: number;
137
+ openTelemetrySpanProcessor?: 'batch' | 'simple';
138
+ honeycombApiKey?: string;
139
+ honeycombDataset?: string;
140
+ serviceName?: string;
141
+ }
142
+
143
+ function getHoneycombMetadata(config: OpenTelemetryConfig, datasetSuffix = ''): Metadata {
144
+ if (!config.honeycombApiKey) throw new Error('Missing Honeycomb API key');
145
+ if (!config.honeycombDataset) throw new Error('Missing Honeycomb dataset');
146
+
147
+ const metadata = new Metadata();
148
+
149
+ metadata.set('x-honeycomb-team', config.honeycombApiKey);
150
+ metadata.set('x-honeycomb-dataset', config.honeycombDataset + datasetSuffix);
151
+
152
+ return metadata;
153
+ }
154
+
155
+ function getTraceExporter(config: OpenTelemetryConfig): SpanExporter {
156
+ if (typeof config.openTelemetryExporter === 'object') {
157
+ return config.openTelemetryExporter;
158
+ }
159
+
160
+ switch (config.openTelemetryExporter) {
161
+ case 'console':
162
+ return new ConsoleSpanExporter();
163
+ case 'honeycomb':
164
+ return new OTLPTraceExporter({
165
+ url: 'grpc://api.honeycomb.io:443/',
166
+ credentials: credentials.createSsl(),
167
+ metadata: getHoneycombMetadata(config),
168
+ });
169
+ break;
170
+ case 'jaeger':
171
+ return new JaegerExporter({
172
+ // By default, the UDP sender will be used, but that causes issues
173
+ // with packet sizes when Jaeger is running in Docker. We'll instead
174
+ // configure it to use the HTTP sender, which shouldn't face those
175
+ // same issues. We'll still allow the endpoint to be overridden via
176
+ // environment variable if needed.
177
+ endpoint: process.env.OTEL_EXPORTER_JAEGER_ENDPOINT ?? 'http://localhost:14268/api/traces',
178
+ });
179
+ default:
180
+ throw new Error(`Unknown OpenTelemetry exporter: ${config.openTelemetryExporter}`);
181
+ }
182
+ }
183
+
184
+ function getMetricExporter(config: OpenTelemetryConfig): PushMetricExporter | null {
185
+ if (!config.openTelemetryMetricExporter) return null;
186
+
187
+ if (typeof config.openTelemetryMetricExporter === 'object') {
188
+ return config.openTelemetryMetricExporter;
189
+ }
190
+
191
+ switch (config.openTelemetryMetricExporter) {
192
+ case 'console':
193
+ return new ConsoleMetricExporter();
194
+ case 'honeycomb':
195
+ return new OTLPMetricExporter({
196
+ url: 'grpc://api.honeycomb.io:443/',
197
+ credentials: credentials.createSsl(),
198
+ // Honeycomb recommends using a separate dataset for metrics, so we'll
199
+ // adopt the convention of appending '-metrics' to the dataset name.
200
+ metadata: getHoneycombMetadata(config, '-metrics'),
201
+ // Delta temporality means that sums, histograms, etc. will reset each
202
+ // time data is collected. This more closely matches how we want to
203
+ // observe our metrics than the default cumulative temporality.
204
+ temporalityPreference: AggregationTemporality.DELTA,
205
+ });
206
+ default:
207
+ throw new Error(
208
+ `Unknown OpenTelemetry metric exporter: ${config.openTelemetryMetricExporter}`
209
+ );
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Should be called once we've loaded our config; this will allow us to set up
215
+ * the correct metadata for the Honeycomb exporter. We don't actually have that
216
+ * information available until we've loaded our config.
217
+ */
218
+ export async function init(config: OpenTelemetryConfig) {
219
+ if (!config.openTelemetryEnabled) {
220
+ // If not enabled, do nothing. We used to disable the instrumentations, but
221
+ // per maintainers, that can actually be problematic. See the comments on
222
+ // https://github.com/open-telemetry/opentelemetry-js-contrib/issues/970
223
+ // The Express instrumentation also logs a benign error, which can be
224
+ // confusing to users. There's a fix in progress if we want to switch back
225
+ // to disabling instrumentations in the future:
226
+ // https://github.com/open-telemetry/opentelemetry-js-contrib/pull/972
227
+ return;
228
+ }
229
+
230
+ const traceExporter = getTraceExporter(config);
231
+ const metricExporter = getMetricExporter(config);
232
+
233
+ let sampler: Sampler;
234
+ switch (config.openTelemetrySamplerType ?? 'always-on') {
235
+ case 'always-on': {
236
+ sampler = new AlwaysOnSampler();
237
+ break;
238
+ }
239
+ case 'always-off': {
240
+ sampler = new AlwaysOffSampler();
241
+ break;
242
+ }
243
+ case 'trace-id-ratio': {
244
+ sampler = new ParentBasedSampler({
245
+ root: new TraceIdRatioBasedSampler(config.openTelemetrySampleRate),
246
+ });
247
+ break;
248
+ }
249
+ default:
250
+ throw new Error(`Unknown OpenTelemetry sampler type: ${config.openTelemetrySamplerType}`);
251
+ }
252
+
253
+ let spanProcessor: SpanProcessor;
254
+ switch (config.openTelemetrySpanProcessor ?? 'batch') {
255
+ case 'batch': {
256
+ spanProcessor = new FilterBatchSpanProcessor(traceExporter, filter);
257
+ break;
258
+ }
259
+ case 'simple': {
260
+ spanProcessor = new SimpleSpanProcessor(traceExporter);
261
+ break;
262
+ }
263
+ default: {
264
+ throw new Error(`Unknown OpenTelemetry span processor: ${config.openTelemetrySpanProcessor}`);
265
+ }
266
+ }
267
+
268
+ // Much of this functionality is copied from `@opentelemetry/sdk-node`, but
269
+ // we can't use the SDK directly because of the fact that we load our config
270
+ // asynchronously. We need to initialize our instrumentations first; only
271
+ // then can we actually start requiring all of our code that loads our config
272
+ // and ultimately tells us how to configure OpenTelemetry.
273
+
274
+ let resource = await detectResources({
275
+ detectors: [awsEc2Detector, processDetector, envDetector],
276
+ });
277
+
278
+ if (config.serviceName) {
279
+ resource = resource.merge(
280
+ new Resource({ [SemanticResourceAttributes.SERVICE_NAME]: config.serviceName })
281
+ );
282
+ }
283
+
284
+ // Set up tracing instrumentation.
285
+ const nodeTracerProvider = new NodeTracerProvider({
286
+ sampler,
287
+ resource,
288
+ });
289
+ nodeTracerProvider.addSpanProcessor(spanProcessor);
290
+ nodeTracerProvider.register();
291
+ instrumentations.forEach((i) => i.setTracerProvider(nodeTracerProvider));
292
+
293
+ // Save the provider so we can shut it down later.
294
+ tracerProvider = nodeTracerProvider;
295
+
296
+ // Set up metrics instrumentation if it's enabled.
297
+ if (metricExporter) {
298
+ const meterProvider = new MeterProvider({ resource });
299
+ metrics.setGlobalMeterProvider(meterProvider);
300
+ const metricReader = new PeriodicExportingMetricReader({
301
+ exporter: metricExporter,
302
+ exportIntervalMillis: config.openTelemetryMetricExportIntervalMillis ?? 30_000,
303
+ });
304
+ meterProvider.addMetricReader(metricReader);
305
+ }
306
+ }
307
+
308
+ /**
309
+ * Gracefully shuts down the OpenTelemetry instrumentation. Should be called
310
+ * when a `SIGTERM` signal is handled.
311
+ */
312
+ export async function shutdown(): Promise<void> {
313
+ if (tracerProvider) {
314
+ await tracerProvider.shutdown();
315
+ tracerProvider = null;
316
+ }
317
+ }
@@ -0,0 +1,86 @@
1
+ import {
2
+ InMemoryMetricExporter,
3
+ AggregationTemporality,
4
+ MeterProvider,
5
+ PeriodicExportingMetricReader,
6
+ Histogram,
7
+ } from '@opentelemetry/sdk-metrics';
8
+ import { Meter } from '@opentelemetry/api';
9
+ import chai, { assert } from 'chai';
10
+ import chaiAsPromised from 'chai-as-promised';
11
+
12
+ import { instrumentedWithMetrics } from './metrics';
13
+
14
+ chai.use(chaiAsPromised);
15
+
16
+ async function waitForMetricsExport(exporter: InMemoryMetricExporter) {
17
+ while (exporter.getMetrics().length === 0) {
18
+ await new Promise((resolve) => setTimeout(resolve, 50));
19
+ }
20
+ }
21
+
22
+ describe('instrumentedWithMetrics', () => {
23
+ let exporter: InMemoryMetricExporter;
24
+ let metricReader: PeriodicExportingMetricReader;
25
+ let meter: Meter;
26
+
27
+ beforeEach(async () => {
28
+ const meterProvider = new MeterProvider();
29
+ meter = meterProvider.getMeter('test');
30
+ exporter = new InMemoryMetricExporter(AggregationTemporality.DELTA);
31
+ metricReader = new PeriodicExportingMetricReader({
32
+ exporter: exporter,
33
+ exportIntervalMillis: 50,
34
+ });
35
+ meterProvider.addMetricReader(metricReader);
36
+ });
37
+
38
+ afterEach(async () => {
39
+ await exporter.shutdown();
40
+ await metricReader.shutdown();
41
+ });
42
+
43
+ it('records a histogram for the function duration', async () => {
44
+ await instrumentedWithMetrics(meter, 'test', async () => {});
45
+
46
+ await waitForMetricsExport(exporter);
47
+ const exportedMetrics = exporter.getMetrics();
48
+ const { scope, metrics } = exportedMetrics[0].scopeMetrics[0];
49
+ const [counterMetric, histogramMetric] = metrics;
50
+
51
+ assert.equal(scope.name, 'test');
52
+
53
+ assert.ok(counterMetric);
54
+ assert.equal(counterMetric.descriptor.name, 'test.error');
55
+ assert.lengthOf(counterMetric.dataPoints, 0);
56
+
57
+ assert.ok(histogramMetric);
58
+ assert.equal(histogramMetric.descriptor.name, 'test.duration');
59
+ assert.equal((histogramMetric.dataPoints[0].value as Histogram).count, 1);
60
+ });
61
+
62
+ it('records an error count', async () => {
63
+ await assert.isRejected(
64
+ instrumentedWithMetrics(meter, 'test', async () => {
65
+ throw new Error('error for test');
66
+ }),
67
+ 'error for test'
68
+ );
69
+
70
+ await waitForMetricsExport(exporter);
71
+ const exportedMetrics = exporter.getMetrics();
72
+ const { metrics, scope } = exportedMetrics[0].scopeMetrics[0];
73
+ const [counterMetric, histogramMetric] = metrics;
74
+
75
+ assert.ok(scope);
76
+ assert.equal(scope.name, 'test');
77
+
78
+ assert.ok(counterMetric);
79
+ assert.equal(counterMetric.descriptor.name, 'test.error');
80
+ assert.equal(counterMetric.dataPoints[0].value, 1);
81
+
82
+ assert.ok(histogramMetric);
83
+ assert.equal(histogramMetric.descriptor.name, 'test.duration');
84
+ assert.equal((histogramMetric.dataPoints[0].value as Histogram).count, 1);
85
+ });
86
+ });