@prairielearn/opentelemetry 1.0.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/.turbo/turbo-build.log +3 -0
- package/README.md +47 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +184 -0
- package/dist/index.js.map +1 -0
- package/package.json +31 -0
- package/src/index.ts +215 -0
- package/tsconfig.json +16 -0
package/README.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# `@prairielearn/opentelemetry`
|
|
2
|
+
|
|
3
|
+
Opinionated wrapper around various `@opentelemetry/*` packages.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
You should require this package as early as possible during application initialization and call `init()` once the application configuration is available.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { init } from '@prairielearn/opentelemetry';
|
|
11
|
+
|
|
12
|
+
// ...
|
|
13
|
+
|
|
14
|
+
await init({
|
|
15
|
+
openTelemetryEnabled: true,
|
|
16
|
+
openTelemetryExporter: 'honeycomb',
|
|
17
|
+
openTelemetrySamplerType: 'always-on',
|
|
18
|
+
openTelemetrySampleRate: 0.1,
|
|
19
|
+
honeycombApiKey: 'KEY',
|
|
20
|
+
honeycombDataset: 'DATASET',
|
|
21
|
+
});
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
This will automatically instrument a variety of commonly-used Node packages.
|
|
25
|
+
|
|
26
|
+
To manually instrument code, you can use the `trace` export:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { trace } from '@prairielearn/opentelemetry';
|
|
30
|
+
|
|
31
|
+
const tracer = trace.getTracer('lib-name');
|
|
32
|
+
await tracer.startActiveSpan('span-name', async (span) => {
|
|
33
|
+
try {
|
|
34
|
+
await doWork();
|
|
35
|
+
span.setStatus({ status: SpanStatusCode.OK });
|
|
36
|
+
} catch (err) {
|
|
37
|
+
span.recordException(err);
|
|
38
|
+
span.setStatus({
|
|
39
|
+
status: SpanStatusCode.ERROR,
|
|
40
|
+
message: err.message,
|
|
41
|
+
});
|
|
42
|
+
throw err;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
When using code from the OpenTelemetry libraries, make sure you import it via `@prairielearn/opentelemetry` instead of installing it separately to ensure that there is only one version of each OpenTelemetry package in use at once. If the desired functionality is not yet exported, please add it!
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface OpenTelemetryConfig {
|
|
2
|
+
openTelemetryEnabled: boolean;
|
|
3
|
+
openTelemetryExporter?: 'console' | 'honeycomb';
|
|
4
|
+
openTelemetrySamplerType?: 'always-on' | 'always-off' | 'trace-id-ratio';
|
|
5
|
+
openTelemetrySampleRate?: number;
|
|
6
|
+
honeycombApiKey?: string;
|
|
7
|
+
honeycombDataset?: string;
|
|
8
|
+
serviceName?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Should be called once we've loaded our config; this will allow us to set up
|
|
12
|
+
* the correct metadata for the Honeycomb exporter. We don't actually have that
|
|
13
|
+
* information available until we've loaded our config.
|
|
14
|
+
*/
|
|
15
|
+
export declare function init(config: OpenTelemetryConfig): Promise<void>;
|
|
16
|
+
export { trace, context, SpanStatusCode } from '@opentelemetry/api';
|
|
17
|
+
export { suppressTracing } from '@opentelemetry/core';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.suppressTracing = exports.SpanStatusCode = exports.context = exports.trace = exports.init = void 0;
|
|
7
|
+
const process_1 = __importDefault(require("process"));
|
|
8
|
+
const grpc_js_1 = require("@grpc/grpc-js");
|
|
9
|
+
const sdk_node_1 = require("@opentelemetry/sdk-node");
|
|
10
|
+
const sdk_trace_node_1 = require("@opentelemetry/sdk-trace-node");
|
|
11
|
+
const resources_1 = require("@opentelemetry/resources");
|
|
12
|
+
const semantic_conventions_1 = require("@opentelemetry/semantic-conventions");
|
|
13
|
+
const exporter_otlp_grpc_1 = require("@opentelemetry/exporter-otlp-grpc");
|
|
14
|
+
const instrumentation_express_1 = require("@opentelemetry/instrumentation-express");
|
|
15
|
+
const sdk_trace_base_1 = require("@opentelemetry/sdk-trace-base");
|
|
16
|
+
const core_1 = require("@opentelemetry/core");
|
|
17
|
+
// Instrumentations go here.
|
|
18
|
+
const instrumentation_aws_sdk_1 = require("@opentelemetry/instrumentation-aws-sdk");
|
|
19
|
+
const instrumentation_connect_1 = require("@opentelemetry/instrumentation-connect");
|
|
20
|
+
const instrumentation_dns_1 = require("@opentelemetry/instrumentation-dns");
|
|
21
|
+
const instrumentation_express_2 = require("@opentelemetry/instrumentation-express");
|
|
22
|
+
const instrumentation_http_1 = require("@opentelemetry/instrumentation-http");
|
|
23
|
+
const instrumentation_pg_1 = require("@opentelemetry/instrumentation-pg");
|
|
24
|
+
const instrumentation_redis_1 = require("@opentelemetry/instrumentation-redis");
|
|
25
|
+
// Resource detectors go here.
|
|
26
|
+
const resource_detector_aws_1 = require("@opentelemetry/resource-detector-aws");
|
|
27
|
+
const resources_2 = require("@opentelemetry/resources");
|
|
28
|
+
/**
|
|
29
|
+
* Extends `BatchSpanProcessor` to give it the ability to filter out spans
|
|
30
|
+
* before they're queued up to send. This enhances our samping process so
|
|
31
|
+
* that we can filter spans _after_ they've been emitted.
|
|
32
|
+
*/
|
|
33
|
+
class FilterBatchSpanProcessor extends sdk_trace_base_1.BatchSpanProcessor {
|
|
34
|
+
constructor(exporter, filter) {
|
|
35
|
+
super(exporter);
|
|
36
|
+
this.filter = filter;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* This is invoked after a span is "finalized". `super.onEnd` will queue up
|
|
40
|
+
* the span to be exported, but if we don't call that, we can just drop the
|
|
41
|
+
* span and the parent will be none the wiser!
|
|
42
|
+
*/
|
|
43
|
+
onEnd(span) {
|
|
44
|
+
if (!this.filter(span))
|
|
45
|
+
return;
|
|
46
|
+
super.onEnd(span);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* This will be used with our {@link FilterBatchSpanProcessor} to filter out
|
|
51
|
+
* events that we're not interested in. This helps reduce our event volume
|
|
52
|
+
* but still gives us fine-grained control over which events we keep.
|
|
53
|
+
*/
|
|
54
|
+
function filter(span) {
|
|
55
|
+
if (span.name === 'pg-pool.connect') {
|
|
56
|
+
// Looking at historical data, this generally happens in under a millisecond,
|
|
57
|
+
// precisely because we maintain a pool of long-lived connections. The only
|
|
58
|
+
// time obtaining a client should take longer than that is if we're
|
|
59
|
+
// establishing a connection for the first time, which should happen only at
|
|
60
|
+
// bootup, or if a connection errors out. Those are the cases we're
|
|
61
|
+
// interested in, so we'll filter accordingly.
|
|
62
|
+
return (0, core_1.hrTimeToMilliseconds)(span.duration) > 1;
|
|
63
|
+
}
|
|
64
|
+
// Always return true so that we default to including a span.
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
const instrumentations = [
|
|
68
|
+
new instrumentation_aws_sdk_1.AwsInstrumentation(),
|
|
69
|
+
new instrumentation_connect_1.ConnectInstrumentation(),
|
|
70
|
+
new instrumentation_dns_1.DnsInstrumentation(),
|
|
71
|
+
new instrumentation_express_2.ExpressInstrumentation({
|
|
72
|
+
// We use a lot of middleware; it makes the traces way too noisy. If we
|
|
73
|
+
// want telementry on a particular middleware, we should instrument it
|
|
74
|
+
// manually.
|
|
75
|
+
ignoreLayersType: [instrumentation_express_1.ExpressLayerType.MIDDLEWARE],
|
|
76
|
+
ignoreLayers: [
|
|
77
|
+
// These don't provide useful information to us.
|
|
78
|
+
'router - /',
|
|
79
|
+
'request handler - /*',
|
|
80
|
+
],
|
|
81
|
+
}),
|
|
82
|
+
new instrumentation_http_1.HttpInstrumentation({
|
|
83
|
+
ignoreIncomingPaths: [
|
|
84
|
+
// socket.io requests are generally just long-polling; they don't add
|
|
85
|
+
// useful information for us.
|
|
86
|
+
/\/socket.io\//,
|
|
87
|
+
// We get several of these per second; they just chew through our event quota.
|
|
88
|
+
// They don't really do anything interesting anyways.
|
|
89
|
+
/\/pl\/webhooks\/ping/,
|
|
90
|
+
],
|
|
91
|
+
}),
|
|
92
|
+
new instrumentation_pg_1.PgInstrumentation(),
|
|
93
|
+
new instrumentation_redis_1.RedisInstrumentation(),
|
|
94
|
+
];
|
|
95
|
+
// Enable all instrumentations now, even though we haven't configured our
|
|
96
|
+
// span processors or trace exporters yet. We'll set those up later.
|
|
97
|
+
instrumentations.forEach((i) => {
|
|
98
|
+
i.enable();
|
|
99
|
+
});
|
|
100
|
+
/**
|
|
101
|
+
* Should be called once we've loaded our config; this will allow us to set up
|
|
102
|
+
* the correct metadata for the Honeycomb exporter. We don't actually have that
|
|
103
|
+
* information available until we've loaded our config.
|
|
104
|
+
*/
|
|
105
|
+
async function init(config) {
|
|
106
|
+
if (!config.openTelemetryEnabled) {
|
|
107
|
+
// Just disable all of the OTEL instrumentations to avoid any unnecessary overhead.
|
|
108
|
+
instrumentations.forEach((i) => i.disable());
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
let exporter;
|
|
112
|
+
switch (config.openTelemetryExporter) {
|
|
113
|
+
case 'console': {
|
|
114
|
+
// Export spans to the console for testing purposes.
|
|
115
|
+
exporter = new sdk_node_1.tracing.ConsoleSpanExporter();
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
case 'honeycomb': {
|
|
119
|
+
// Create a Honeycomb exporter with the appropriate metadata from the
|
|
120
|
+
// config we've been provided with.
|
|
121
|
+
const metadata = new grpc_js_1.Metadata();
|
|
122
|
+
metadata.set('x-honeycomb-team', config.honeycombApiKey);
|
|
123
|
+
metadata.set('x-honeycomb-dataset', config.honeycombDataset);
|
|
124
|
+
exporter = new exporter_otlp_grpc_1.OTLPTraceExporter({
|
|
125
|
+
url: 'grpc://api.honeycomb.io:443/',
|
|
126
|
+
credentials: grpc_js_1.credentials.createSsl(),
|
|
127
|
+
metadata,
|
|
128
|
+
});
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
default:
|
|
132
|
+
throw new Error(`Unknown OpenTelemetry exporter: ${config.openTelemetryExporter}`);
|
|
133
|
+
}
|
|
134
|
+
let sampler;
|
|
135
|
+
switch (config.openTelemetrySamplerType) {
|
|
136
|
+
case 'always-on': {
|
|
137
|
+
sampler = new core_1.AlwaysOnSampler();
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
case 'always-off': {
|
|
141
|
+
sampler = new core_1.AlwaysOffSampler();
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
case 'trace-id-ratio': {
|
|
145
|
+
sampler = new core_1.ParentBasedSampler({
|
|
146
|
+
root: new core_1.TraceIdRatioBasedSampler(config.openTelemetrySampleRate),
|
|
147
|
+
});
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
default:
|
|
151
|
+
throw new Error(`Unknown OpenTelemetry sampler type: ${config.openTelemetrySamplerType}`);
|
|
152
|
+
}
|
|
153
|
+
// Much of this functionality is copied from `@opentelemetry/sdk-node`, but
|
|
154
|
+
// we can't use the SDK directly because of the fact that we load our config
|
|
155
|
+
// asynchronously. We need to initialize our instrumentations first; only
|
|
156
|
+
// then can we actually start requiring all of our code that loads our config
|
|
157
|
+
// and ultimately tells us how to configure OpenTelemetry.
|
|
158
|
+
let resource = await (0, resources_1.detectResources)({
|
|
159
|
+
detectors: [resource_detector_aws_1.awsEc2Detector, resources_2.processDetector, resources_2.envDetector],
|
|
160
|
+
});
|
|
161
|
+
if (config.serviceName) {
|
|
162
|
+
resource = resource.merge(new resources_1.Resource({ [semantic_conventions_1.SemanticResourceAttributes.SERVICE_NAME]: config.serviceName }));
|
|
163
|
+
}
|
|
164
|
+
const tracerProvider = new sdk_trace_node_1.NodeTracerProvider({
|
|
165
|
+
sampler,
|
|
166
|
+
resource,
|
|
167
|
+
});
|
|
168
|
+
const spanProcessor = new FilterBatchSpanProcessor(exporter, filter);
|
|
169
|
+
tracerProvider.addSpanProcessor(spanProcessor);
|
|
170
|
+
tracerProvider.register();
|
|
171
|
+
instrumentations.forEach((i) => i.setTracerProvider(tracerProvider));
|
|
172
|
+
// When the process starts shutting down, terminate OTEL stuff.
|
|
173
|
+
process_1.default.on('SIGTERM', () => {
|
|
174
|
+
tracerProvider.shutdown().catch((error) => console.error('Error terminating tracing', error));
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
exports.init = init;
|
|
178
|
+
var api_1 = require("@opentelemetry/api");
|
|
179
|
+
Object.defineProperty(exports, "trace", { enumerable: true, get: function () { return api_1.trace; } });
|
|
180
|
+
Object.defineProperty(exports, "context", { enumerable: true, get: function () { return api_1.context; } });
|
|
181
|
+
Object.defineProperty(exports, "SpanStatusCode", { enumerable: true, get: function () { return api_1.SpanStatusCode; } });
|
|
182
|
+
var core_2 = require("@opentelemetry/core");
|
|
183
|
+
Object.defineProperty(exports, "suppressTracing", { enumerable: true, get: function () { return core_2.suppressTracing; } });
|
|
184
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@prairielearn/opentelemetry",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"build": "tsc",
|
|
7
|
+
"dev": "tsc --watch --preserveWatchOutput"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"@grpc/grpc-js": "^1.5.5",
|
|
11
|
+
"@opentelemetry/api": "^1.0.4",
|
|
12
|
+
"@opentelemetry/core": "^1.0.1",
|
|
13
|
+
"@opentelemetry/exporter-otlp-grpc": "^0.26.0",
|
|
14
|
+
"@opentelemetry/instrumentation-aws-sdk": "^0.5.0",
|
|
15
|
+
"@opentelemetry/instrumentation-connect": "^0.27.1",
|
|
16
|
+
"@opentelemetry/instrumentation-dns": "^0.27.1",
|
|
17
|
+
"@opentelemetry/instrumentation-express": "^0.28.0",
|
|
18
|
+
"@opentelemetry/instrumentation-http": "^0.27.0",
|
|
19
|
+
"@opentelemetry/instrumentation-pg": "^0.28.0",
|
|
20
|
+
"@opentelemetry/instrumentation-redis": "^0.28.0",
|
|
21
|
+
"@opentelemetry/resource-detector-aws": "^1.0.3",
|
|
22
|
+
"@opentelemetry/resources": "^1.0.1",
|
|
23
|
+
"@opentelemetry/sdk-node": "^0.27.0",
|
|
24
|
+
"@opentelemetry/sdk-trace-base": "^1.0.1",
|
|
25
|
+
"@opentelemetry/sdk-trace-node": "^1.0.1",
|
|
26
|
+
"@opentelemetry/semantic-conventions": "^1.0.1"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"typescript": "^4.5.5"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import process from 'process';
|
|
2
|
+
import { Metadata, credentials } from '@grpc/grpc-js';
|
|
3
|
+
|
|
4
|
+
import { tracing } from '@opentelemetry/sdk-node';
|
|
5
|
+
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
|
|
6
|
+
import type { SpanExporter, ReadableSpan } from '@opentelemetry/sdk-trace-base';
|
|
7
|
+
import { detectResources, Resource } from '@opentelemetry/resources';
|
|
8
|
+
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
|
|
9
|
+
import { OTLPTraceExporter } from '@opentelemetry/exporter-otlp-grpc';
|
|
10
|
+
import { ExpressLayerType } from '@opentelemetry/instrumentation-express';
|
|
11
|
+
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
|
|
12
|
+
import { Sampler } from '@opentelemetry/api';
|
|
13
|
+
import {
|
|
14
|
+
ParentBasedSampler,
|
|
15
|
+
TraceIdRatioBasedSampler,
|
|
16
|
+
AlwaysOnSampler,
|
|
17
|
+
AlwaysOffSampler,
|
|
18
|
+
hrTimeToMilliseconds,
|
|
19
|
+
} from '@opentelemetry/core';
|
|
20
|
+
|
|
21
|
+
// Instrumentations go here.
|
|
22
|
+
import { AwsInstrumentation } from '@opentelemetry/instrumentation-aws-sdk';
|
|
23
|
+
import { ConnectInstrumentation } from '@opentelemetry/instrumentation-connect';
|
|
24
|
+
import { DnsInstrumentation } from '@opentelemetry/instrumentation-dns';
|
|
25
|
+
import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express';
|
|
26
|
+
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
|
|
27
|
+
import { PgInstrumentation } from '@opentelemetry/instrumentation-pg';
|
|
28
|
+
import { RedisInstrumentation } from '@opentelemetry/instrumentation-redis';
|
|
29
|
+
|
|
30
|
+
// Resource detectors go here.
|
|
31
|
+
import { awsEc2Detector } from '@opentelemetry/resource-detector-aws';
|
|
32
|
+
import { processDetector, envDetector } from '@opentelemetry/resources';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Extends `BatchSpanProcessor` to give it the ability to filter out spans
|
|
36
|
+
* before they're queued up to send. This enhances our samping process so
|
|
37
|
+
* that we can filter spans _after_ they've been emitted.
|
|
38
|
+
*/
|
|
39
|
+
class FilterBatchSpanProcessor extends BatchSpanProcessor {
|
|
40
|
+
private filter: (span: ReadableSpan) => boolean;
|
|
41
|
+
|
|
42
|
+
constructor(exporter: SpanExporter, filter: (span: ReadableSpan) => boolean) {
|
|
43
|
+
super(exporter);
|
|
44
|
+
this.filter = filter;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* This is invoked after a span is "finalized". `super.onEnd` will queue up
|
|
49
|
+
* the span to be exported, but if we don't call that, we can just drop the
|
|
50
|
+
* span and the parent will be none the wiser!
|
|
51
|
+
*/
|
|
52
|
+
onEnd(span: ReadableSpan) {
|
|
53
|
+
if (!this.filter(span)) return;
|
|
54
|
+
|
|
55
|
+
super.onEnd(span);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* This will be used with our {@link FilterBatchSpanProcessor} to filter out
|
|
61
|
+
* events that we're not interested in. This helps reduce our event volume
|
|
62
|
+
* but still gives us fine-grained control over which events we keep.
|
|
63
|
+
*/
|
|
64
|
+
function filter(span: ReadableSpan) {
|
|
65
|
+
if (span.name === 'pg-pool.connect') {
|
|
66
|
+
// Looking at historical data, this generally happens in under a millisecond,
|
|
67
|
+
// precisely because we maintain a pool of long-lived connections. The only
|
|
68
|
+
// time obtaining a client should take longer than that is if we're
|
|
69
|
+
// establishing a connection for the first time, which should happen only at
|
|
70
|
+
// bootup, or if a connection errors out. Those are the cases we're
|
|
71
|
+
// interested in, so we'll filter accordingly.
|
|
72
|
+
return hrTimeToMilliseconds(span.duration) > 1;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Always return true so that we default to including a span.
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const instrumentations = [
|
|
80
|
+
new AwsInstrumentation(),
|
|
81
|
+
new ConnectInstrumentation(),
|
|
82
|
+
new DnsInstrumentation(),
|
|
83
|
+
new ExpressInstrumentation({
|
|
84
|
+
// We use a lot of middleware; it makes the traces way too noisy. If we
|
|
85
|
+
// want telementry on a particular middleware, we should instrument it
|
|
86
|
+
// manually.
|
|
87
|
+
ignoreLayersType: [ExpressLayerType.MIDDLEWARE],
|
|
88
|
+
ignoreLayers: [
|
|
89
|
+
// These don't provide useful information to us.
|
|
90
|
+
'router - /',
|
|
91
|
+
'request handler - /*',
|
|
92
|
+
],
|
|
93
|
+
}),
|
|
94
|
+
new HttpInstrumentation({
|
|
95
|
+
ignoreIncomingPaths: [
|
|
96
|
+
// socket.io requests are generally just long-polling; they don't add
|
|
97
|
+
// useful information for us.
|
|
98
|
+
/\/socket.io\//,
|
|
99
|
+
// We get several of these per second; they just chew through our event quota.
|
|
100
|
+
// They don't really do anything interesting anyways.
|
|
101
|
+
/\/pl\/webhooks\/ping/,
|
|
102
|
+
],
|
|
103
|
+
}),
|
|
104
|
+
new PgInstrumentation(),
|
|
105
|
+
new RedisInstrumentation(),
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
// Enable all instrumentations now, even though we haven't configured our
|
|
109
|
+
// span processors or trace exporters yet. We'll set those up later.
|
|
110
|
+
instrumentations.forEach((i) => {
|
|
111
|
+
i.enable();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
export interface OpenTelemetryConfig {
|
|
115
|
+
openTelemetryEnabled: boolean;
|
|
116
|
+
openTelemetryExporter?: 'console' | 'honeycomb';
|
|
117
|
+
openTelemetrySamplerType?: 'always-on' | 'always-off' | 'trace-id-ratio';
|
|
118
|
+
openTelemetrySampleRate?: number;
|
|
119
|
+
honeycombApiKey?: string;
|
|
120
|
+
honeycombDataset?: string;
|
|
121
|
+
serviceName?: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Should be called once we've loaded our config; this will allow us to set up
|
|
126
|
+
* the correct metadata for the Honeycomb exporter. We don't actually have that
|
|
127
|
+
* information available until we've loaded our config.
|
|
128
|
+
*/
|
|
129
|
+
export async function init(config: OpenTelemetryConfig) {
|
|
130
|
+
if (!config.openTelemetryEnabled) {
|
|
131
|
+
// Just disable all of the OTEL instrumentations to avoid any unnecessary overhead.
|
|
132
|
+
instrumentations.forEach((i) => i.disable());
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let exporter: SpanExporter;
|
|
137
|
+
switch (config.openTelemetryExporter) {
|
|
138
|
+
case 'console': {
|
|
139
|
+
// Export spans to the console for testing purposes.
|
|
140
|
+
exporter = new tracing.ConsoleSpanExporter();
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
case 'honeycomb': {
|
|
144
|
+
// Create a Honeycomb exporter with the appropriate metadata from the
|
|
145
|
+
// config we've been provided with.
|
|
146
|
+
const metadata = new Metadata();
|
|
147
|
+
|
|
148
|
+
metadata.set('x-honeycomb-team', config.honeycombApiKey);
|
|
149
|
+
metadata.set('x-honeycomb-dataset', config.honeycombDataset);
|
|
150
|
+
|
|
151
|
+
exporter = new OTLPTraceExporter({
|
|
152
|
+
url: 'grpc://api.honeycomb.io:443/',
|
|
153
|
+
credentials: credentials.createSsl(),
|
|
154
|
+
metadata,
|
|
155
|
+
});
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
default:
|
|
159
|
+
throw new Error(`Unknown OpenTelemetry exporter: ${config.openTelemetryExporter}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
let sampler: Sampler;
|
|
163
|
+
switch (config.openTelemetrySamplerType) {
|
|
164
|
+
case 'always-on': {
|
|
165
|
+
sampler = new AlwaysOnSampler();
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
case 'always-off': {
|
|
169
|
+
sampler = new AlwaysOffSampler();
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
case 'trace-id-ratio': {
|
|
173
|
+
sampler = new ParentBasedSampler({
|
|
174
|
+
root: new TraceIdRatioBasedSampler(config.openTelemetrySampleRate),
|
|
175
|
+
});
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
default:
|
|
179
|
+
throw new Error(`Unknown OpenTelemetry sampler type: ${config.openTelemetrySamplerType}`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Much of this functionality is copied from `@opentelemetry/sdk-node`, but
|
|
183
|
+
// we can't use the SDK directly because of the fact that we load our config
|
|
184
|
+
// asynchronously. We need to initialize our instrumentations first; only
|
|
185
|
+
// then can we actually start requiring all of our code that loads our config
|
|
186
|
+
// and ultimately tells us how to configure OpenTelemetry.
|
|
187
|
+
|
|
188
|
+
let resource = await detectResources({
|
|
189
|
+
detectors: [awsEc2Detector, processDetector, envDetector],
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
if (config.serviceName) {
|
|
193
|
+
resource = resource.merge(
|
|
194
|
+
new Resource({ [SemanticResourceAttributes.SERVICE_NAME]: config.serviceName })
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const tracerProvider = new NodeTracerProvider({
|
|
199
|
+
sampler,
|
|
200
|
+
resource,
|
|
201
|
+
});
|
|
202
|
+
const spanProcessor = new FilterBatchSpanProcessor(exporter, filter);
|
|
203
|
+
tracerProvider.addSpanProcessor(spanProcessor);
|
|
204
|
+
tracerProvider.register();
|
|
205
|
+
|
|
206
|
+
instrumentations.forEach((i) => i.setTracerProvider(tracerProvider));
|
|
207
|
+
|
|
208
|
+
// When the process starts shutting down, terminate OTEL stuff.
|
|
209
|
+
process.on('SIGTERM', () => {
|
|
210
|
+
tracerProvider.shutdown().catch((error) => console.error('Error terminating tracing', error));
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export { trace, context, SpanStatusCode } from '@opentelemetry/api';
|
|
215
|
+
export { suppressTracing } from '@opentelemetry/core';
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"allowJs": true,
|
|
4
|
+
"declaration": true,
|
|
5
|
+
"esModuleInterop": true,
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "src/",
|
|
8
|
+
"sourceMap": true,
|
|
9
|
+
// This package will only be used server-side on Node 14+, so target the
|
|
10
|
+
// newest version of the ES spec that Node 14 supports.
|
|
11
|
+
"target": "ES2020",
|
|
12
|
+
// However, we don't yet make extensive use of ES Modules, so specifically
|
|
13
|
+
// compile `import`/`export` down to CommonJS.
|
|
14
|
+
"module": "CommonJS",
|
|
15
|
+
}
|
|
16
|
+
}
|