@sebspark/otel 0.1.0 → 0.2.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/README.md +144 -81
- package/dist/index.d.mts +8 -4
- package/dist/index.d.ts +8 -4
- package/dist/index.js +651 -85
- package/dist/index.mjs +656 -89
- package/package.json +4 -2
package/dist/index.mjs
CHANGED
|
@@ -1,79 +1,561 @@
|
|
|
1
1
|
// src/otel.ts
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
context,
|
|
4
|
+
DiagConsoleLogger,
|
|
5
|
+
DiagLogLevel,
|
|
6
|
+
diag
|
|
7
|
+
} from "@opentelemetry/api";
|
|
3
8
|
import { logs } from "@opentelemetry/api-logs";
|
|
4
9
|
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
|
|
10
|
+
import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-hooks";
|
|
11
|
+
import { NodeSDK } from "@opentelemetry/sdk-node";
|
|
12
|
+
|
|
13
|
+
// src/providers.ts
|
|
5
14
|
import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
|
|
6
15
|
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
|
|
7
16
|
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
8
|
-
import {
|
|
9
|
-
detectResources,
|
|
10
|
-
resourceFromAttributes
|
|
11
|
-
} from "@opentelemetry/resources";
|
|
12
17
|
import {
|
|
13
18
|
BatchLogRecordProcessor,
|
|
14
|
-
LoggerProvider
|
|
19
|
+
LoggerProvider,
|
|
20
|
+
SimpleLogRecordProcessor
|
|
15
21
|
} from "@opentelemetry/sdk-logs";
|
|
16
22
|
import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
|
|
17
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
BatchSpanProcessor
|
|
25
|
+
} from "@opentelemetry/sdk-trace-node";
|
|
26
|
+
|
|
27
|
+
// src/loggers/console-log-pretty-exporter.ts
|
|
28
|
+
import { ExportResultCode } from "@opentelemetry/core";
|
|
29
|
+
|
|
30
|
+
// src/consts.ts
|
|
31
|
+
var LOG_SEVERITY_MAP = {
|
|
32
|
+
TRACE: 1,
|
|
33
|
+
DEBUG: 5,
|
|
34
|
+
INFO: 9,
|
|
35
|
+
NOTICE: 10,
|
|
36
|
+
WARNING: 13,
|
|
37
|
+
WARN: 13,
|
|
38
|
+
ERROR: 17,
|
|
39
|
+
FATAL: 21,
|
|
40
|
+
CRITICAL: 21,
|
|
41
|
+
ALERT: 22,
|
|
42
|
+
EMERGENCY: 23
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// src/loggers/formatters/shared.ts
|
|
18
46
|
import {
|
|
19
47
|
ATTR_SERVICE_NAME,
|
|
20
48
|
ATTR_SERVICE_VERSION
|
|
21
49
|
} from "@opentelemetry/semantic-conventions";
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
import stringify from "fast-safe-stringify";
|
|
51
|
+
|
|
52
|
+
// src/loggers/formatters/style.ts
|
|
53
|
+
import kleur from "kleur";
|
|
54
|
+
var colors = {
|
|
55
|
+
gray: kleur.gray,
|
|
56
|
+
dim: kleur.dim,
|
|
57
|
+
cyan: kleur.cyan,
|
|
58
|
+
white: kleur.white,
|
|
59
|
+
green: kleur.green,
|
|
60
|
+
yellow: kleur.yellow,
|
|
61
|
+
red: kleur.red,
|
|
62
|
+
magenta: kleur.magenta
|
|
63
|
+
};
|
|
64
|
+
var levelColorMap = {
|
|
65
|
+
DEBUG: kleur.magenta,
|
|
66
|
+
INFO: kleur.green,
|
|
67
|
+
WARN: kleur.yellow,
|
|
68
|
+
ERROR: kleur.red,
|
|
69
|
+
FATAL: kleur.red
|
|
70
|
+
};
|
|
71
|
+
var levelIconMap = {
|
|
72
|
+
DEBUG: "\u{1F41B}",
|
|
73
|
+
INFO: "\u2139\uFE0F ",
|
|
74
|
+
WARN: "\u26A0\uFE0F ",
|
|
75
|
+
ERROR: "\u274C",
|
|
76
|
+
FATAL: "\u{1F480}"
|
|
77
|
+
};
|
|
78
|
+
var statusLabelMap = {
|
|
79
|
+
0: "UNSET",
|
|
80
|
+
1: "OK",
|
|
81
|
+
2: "ERROR"
|
|
82
|
+
};
|
|
83
|
+
var statusColorMap = {
|
|
84
|
+
0: colors.gray,
|
|
85
|
+
// UNSET
|
|
86
|
+
1: colors.green,
|
|
87
|
+
// OK
|
|
88
|
+
2: colors.red
|
|
89
|
+
// ERROR
|
|
90
|
+
};
|
|
91
|
+
var kindColorMap = {
|
|
92
|
+
0: colors.white,
|
|
93
|
+
// INTERNAL
|
|
94
|
+
1: colors.cyan,
|
|
95
|
+
// SERVER
|
|
96
|
+
2: colors.yellow,
|
|
97
|
+
// CLIENT
|
|
98
|
+
3: colors.magenta,
|
|
99
|
+
// PRODUCER
|
|
100
|
+
4: colors.green
|
|
101
|
+
// CONSUMER
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// src/loggers/formatters/shared.ts
|
|
105
|
+
function formatTimestamp(time) {
|
|
106
|
+
const date = new Date(hrTimeToMillis(time));
|
|
107
|
+
return colors.dim(date.toISOString().slice(11, 23));
|
|
108
|
+
}
|
|
109
|
+
function formatService(resource) {
|
|
110
|
+
const name = resource.attributes[ATTR_SERVICE_NAME] ?? "unknown-service";
|
|
111
|
+
const version = resource.attributes[ATTR_SERVICE_VERSION] ?? "1.0.0";
|
|
112
|
+
return colors.gray(`[${name}@${version}]`);
|
|
113
|
+
}
|
|
114
|
+
function formatLevel(record) {
|
|
115
|
+
const text = (record.severityText ?? "INFO").toUpperCase();
|
|
116
|
+
const colorFn = levelColorMap[text] ?? colors.white;
|
|
117
|
+
const icon = levelIconMap[text] ?? "\u2022";
|
|
118
|
+
return `${icon} ${colorFn(text.padEnd(5))}`;
|
|
119
|
+
}
|
|
120
|
+
function formatScope(resource, instrumentationScope) {
|
|
121
|
+
const component = resource.attributes["component.name"];
|
|
122
|
+
const { name, version } = instrumentationScope;
|
|
123
|
+
const scopeLabel = component || (name && name !== "unknown" ? name : void 0);
|
|
124
|
+
if (!scopeLabel) return "";
|
|
125
|
+
const versionLabel = version ? `@${version}` : "";
|
|
126
|
+
return colors.cyan(`${scopeLabel}${versionLabel} `);
|
|
127
|
+
}
|
|
128
|
+
function formatMessage(record) {
|
|
129
|
+
return typeof record.body === "string" ? record.body : stringify(record.body);
|
|
130
|
+
}
|
|
131
|
+
function formatAttributes(attrs) {
|
|
132
|
+
const keys = Object.keys(attrs).filter(
|
|
133
|
+
(k) => !k.startsWith("service.") && !k.startsWith("serviceContext.")
|
|
134
|
+
);
|
|
135
|
+
if (keys.length === 0) return "";
|
|
136
|
+
const formatted = keys.map((k) => {
|
|
137
|
+
const val = attrs[k];
|
|
138
|
+
return `${k}=${typeof val === "object" ? stringify(val) : val}`;
|
|
139
|
+
});
|
|
140
|
+
return ` ${colors.gray(formatted.join(" "))}`;
|
|
141
|
+
}
|
|
142
|
+
function hrTimeToMillis(hrTime) {
|
|
143
|
+
return hrTime[0] * 1e3 + Math.floor(hrTime[1] / 1e6);
|
|
144
|
+
}
|
|
145
|
+
function calculateDuration(span) {
|
|
146
|
+
const start = hrTimeToMillis(span.startTime);
|
|
147
|
+
const end = hrTimeToMillis(span.endTime);
|
|
148
|
+
return Math.max(0, Math.round(end - start));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// src/loggers/formatters/log-record.ts
|
|
152
|
+
function formatLogRecord(record) {
|
|
153
|
+
const timestamp = formatTimestamp(record.hrTime);
|
|
154
|
+
const service = formatService(record.resource);
|
|
155
|
+
const level = formatLevel(record);
|
|
156
|
+
const scope = formatScope(record.resource, record.instrumentationScope);
|
|
157
|
+
const message = formatMessage(record);
|
|
158
|
+
const attrs = formatAttributes(record.attributes);
|
|
159
|
+
return `${service} ${timestamp} ${level} ${scope}${message}${attrs}`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// src/loggers/formatters/metrics.ts
|
|
163
|
+
function formatMetrics(resourceMetrics) {
|
|
164
|
+
const { resource, scopeMetrics } = resourceMetrics;
|
|
165
|
+
return scopeMetrics.map((scopeMetric) => formatScopeMetric(scopeMetric, resource)).join("\n");
|
|
166
|
+
}
|
|
167
|
+
function formatScopeMetric(scopeMetric, resource) {
|
|
168
|
+
return scopeMetric.metrics.map(
|
|
169
|
+
(metric) => formatMetricData(metric, resource, scopeMetric.scope)
|
|
170
|
+
).join("\n");
|
|
171
|
+
}
|
|
172
|
+
function formatMetricData(metric, resource, scope) {
|
|
173
|
+
const scopeStr = formatScope(resource, scope);
|
|
174
|
+
const serviceStr = formatService(resource);
|
|
175
|
+
const lines = [];
|
|
176
|
+
for (const dp of metric.dataPoints) {
|
|
177
|
+
const ts = formatTimestamp(dp.startTime);
|
|
178
|
+
const value = extractMetricValue(dp);
|
|
179
|
+
const attrs = formatAttributes(dp.attributes ?? {});
|
|
180
|
+
lines.push(
|
|
181
|
+
`${serviceStr} ${ts} \u{1F4CA} ${scopeStr}${colors.white(metric.descriptor.name)} ${colors.dim(value)}${attrs}`
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
return lines.join("\n");
|
|
185
|
+
}
|
|
186
|
+
function extractMetricValue(dp) {
|
|
187
|
+
const value = dp.value;
|
|
188
|
+
if (typeof value === "number") {
|
|
189
|
+
return value.toString();
|
|
190
|
+
}
|
|
191
|
+
if (isHistogramLike(value)) {
|
|
192
|
+
return value.sum.toString();
|
|
193
|
+
}
|
|
194
|
+
return "[complex]";
|
|
195
|
+
}
|
|
196
|
+
function isHistogramLike(val) {
|
|
197
|
+
return typeof val === "object" && val !== null && "sum" in val && typeof val.sum === "number";
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// src/loggers/formatters/span.ts
|
|
201
|
+
import { SpanStatusCode } from "@opentelemetry/api";
|
|
202
|
+
function formatSpans(spans) {
|
|
203
|
+
const rootSpan = spans[0];
|
|
204
|
+
const rootStart = hrTimeToMillis(rootSpan.startTime);
|
|
205
|
+
const rootEnd = hrTimeToMillis(rootSpan.endTime);
|
|
206
|
+
const totalDuration = rootEnd - rootStart;
|
|
207
|
+
const service = formatService(rootSpan.resource);
|
|
208
|
+
const timestamp = formatTimestamp(rootSpan.startTime);
|
|
209
|
+
const lines = [`${service} ${timestamp}`];
|
|
210
|
+
for (const span of spans) {
|
|
211
|
+
const offset = hrTimeToMillis(span.startTime) - rootStart;
|
|
212
|
+
const depth = computeDepth(span, spans);
|
|
213
|
+
lines.push(
|
|
214
|
+
formatSpan(span, {
|
|
215
|
+
offsetMs: offset,
|
|
216
|
+
totalDurationMs: totalDuration,
|
|
217
|
+
depth
|
|
218
|
+
})
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
return lines.join("\n");
|
|
222
|
+
}
|
|
223
|
+
function formatSpan(span, opts) {
|
|
224
|
+
const label = formatLabel(span, opts.depth);
|
|
225
|
+
const bar = buildBar(span, opts?.offsetMs, opts?.totalDurationMs);
|
|
226
|
+
const barColor = span.status.code === SpanStatusCode.OK ? colors.green : span.status.code === SpanStatusCode.ERROR ? colors.red : colors.gray;
|
|
227
|
+
const desc = formatDescription(span);
|
|
228
|
+
const status = formatStatus(span);
|
|
229
|
+
const duration = formatDuration(span, opts?.offsetMs);
|
|
230
|
+
return `${label} ${barColor(bar)} ${desc} ${status} (${duration})`;
|
|
231
|
+
}
|
|
232
|
+
var LABEL_WIDTH = 25;
|
|
233
|
+
function formatLabel(span, depth) {
|
|
234
|
+
const indent = " ".repeat(depth);
|
|
235
|
+
const label = `${indent}\u2514\u2500 ${span.name}`;
|
|
236
|
+
return label.padEnd(LABEL_WIDTH);
|
|
237
|
+
}
|
|
238
|
+
var BAR_MIN_WIDTH = 1;
|
|
239
|
+
var BAR_MAX_WIDTH = 20;
|
|
240
|
+
function buildBar(span, offsetMs, totalDurationMs) {
|
|
241
|
+
const duration = calculateDuration(span);
|
|
242
|
+
if (typeof offsetMs !== "number" || typeof totalDurationMs !== "number" || totalDurationMs === 0) {
|
|
243
|
+
const capped = Math.min(duration, 1e3);
|
|
244
|
+
const barLength = Math.max(
|
|
245
|
+
BAR_MIN_WIDTH,
|
|
246
|
+
Math.round(capped / 1e3 * BAR_MAX_WIDTH)
|
|
247
|
+
);
|
|
248
|
+
return "\u2588".repeat(barLength).padEnd(BAR_MAX_WIDTH + 2);
|
|
249
|
+
}
|
|
250
|
+
const offsetRatio = Math.max(0, Math.min(offsetMs / totalDurationMs, 1));
|
|
251
|
+
const durationRatio = Math.max(0, Math.min(duration / totalDurationMs, 1));
|
|
252
|
+
const offsetChars = Math.floor(offsetRatio * BAR_MAX_WIDTH);
|
|
253
|
+
const barChars = Math.max(
|
|
254
|
+
BAR_MIN_WIDTH,
|
|
255
|
+
Math.round(durationRatio * BAR_MAX_WIDTH)
|
|
256
|
+
);
|
|
257
|
+
const empty = " ".repeat(offsetChars);
|
|
258
|
+
const bar = "\u2588".repeat(barChars);
|
|
259
|
+
return (empty + bar).padEnd(BAR_MAX_WIDTH + 2);
|
|
260
|
+
}
|
|
261
|
+
var DESCRIPTION_MAX_WIDTH = 20;
|
|
262
|
+
function formatDescription(span) {
|
|
263
|
+
const keyPriority = [
|
|
264
|
+
// HTTP
|
|
265
|
+
["http.method", "http.target"],
|
|
266
|
+
// → GET /users/123
|
|
267
|
+
["http.route"],
|
|
268
|
+
// → /users/:id
|
|
269
|
+
["http.url"],
|
|
270
|
+
// → https://...
|
|
271
|
+
// GraphQL
|
|
272
|
+
["graphql.operation.name"],
|
|
273
|
+
// → getUsers
|
|
274
|
+
["graphql.operation.type"],
|
|
275
|
+
// → query
|
|
276
|
+
["graphql.document"],
|
|
277
|
+
// → full query text (maybe too long)
|
|
278
|
+
// WebSocket
|
|
279
|
+
["ws.event"],
|
|
280
|
+
// → connection, message, disconnect
|
|
281
|
+
["ws.message_type"],
|
|
282
|
+
// → ping/pong/text/binary
|
|
283
|
+
["ws.url"],
|
|
284
|
+
// → wss://...
|
|
285
|
+
// Redis
|
|
286
|
+
["db.system", "db.statement"],
|
|
287
|
+
// → redis, "SET foo bar"
|
|
288
|
+
["db.operation"],
|
|
289
|
+
// → GET, SET
|
|
290
|
+
// Spanner
|
|
291
|
+
["db.statement"],
|
|
292
|
+
// → SELECT * FROM...
|
|
293
|
+
["db.operation"],
|
|
294
|
+
// → SELECT, INSERT
|
|
295
|
+
["db.name"],
|
|
296
|
+
// → projects/.../instances/.../databases/...
|
|
297
|
+
// OpenSearch
|
|
298
|
+
["db.operation"],
|
|
299
|
+
// → search, index, bulk
|
|
300
|
+
["db.statement"],
|
|
301
|
+
// → { query DSL... }
|
|
302
|
+
// Pub/Sub (GCP)
|
|
303
|
+
["messaging.operation"],
|
|
304
|
+
// → publish, receive
|
|
305
|
+
["messaging.destination"],
|
|
306
|
+
// → topic-a
|
|
307
|
+
["messaging.gcp_pubsub.topic"],
|
|
308
|
+
// → projects/x/topics/y
|
|
309
|
+
// General FaaS
|
|
310
|
+
["faas.invoked_name"],
|
|
311
|
+
// → myFunction
|
|
312
|
+
["faas.trigger"],
|
|
313
|
+
// → http, pubsub, etc.
|
|
314
|
+
// Custom or fallback
|
|
315
|
+
["otel.description"]
|
|
316
|
+
];
|
|
317
|
+
for (const keys of keyPriority) {
|
|
318
|
+
const parts = keys.map((k) => span.attributes[k]).filter((v) => v !== void 0 && v !== null).map((v) => String(v));
|
|
319
|
+
if (parts.length > 0) {
|
|
320
|
+
return truncate(parts.join(" "), DESCRIPTION_MAX_WIDTH - 1).padEnd(
|
|
321
|
+
DESCRIPTION_MAX_WIDTH
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return "".padEnd(DESCRIPTION_MAX_WIDTH);
|
|
326
|
+
}
|
|
327
|
+
function formatStatus(span) {
|
|
328
|
+
const code = span.status.code;
|
|
329
|
+
const label = statusLabelMap[code] ?? "UNSET";
|
|
330
|
+
const colorFn = statusColorMap[code] ?? colors.gray;
|
|
331
|
+
return colorFn(label).padEnd(6);
|
|
332
|
+
}
|
|
333
|
+
function formatDuration(span, offsetMs) {
|
|
334
|
+
const duration = calculateDuration(span);
|
|
335
|
+
const format = (ms) => ms >= 1e3 ? `${(ms / 1e3).toFixed(2)} s` : `${ms} ms`;
|
|
336
|
+
return `${format(offsetMs || 0)}\u2013${format(duration)}`;
|
|
337
|
+
}
|
|
338
|
+
function truncate(input, maxLength) {
|
|
339
|
+
const str = String(input ?? "");
|
|
340
|
+
return str.length > maxLength ? `${str.slice(0, maxLength - 1)}\u2026` : str;
|
|
341
|
+
}
|
|
342
|
+
function computeDepth(span, allSpans) {
|
|
343
|
+
let depth = 0;
|
|
344
|
+
let currentParentId = span.parentSpanContext?.spanId;
|
|
345
|
+
while (currentParentId) {
|
|
346
|
+
const parentSpan = allSpans.find(
|
|
347
|
+
(s) => s.spanContext().spanId === currentParentId
|
|
348
|
+
);
|
|
349
|
+
if (!parentSpan) break;
|
|
350
|
+
depth += 1;
|
|
351
|
+
currentParentId = parentSpan.parentSpanContext?.spanId;
|
|
352
|
+
}
|
|
353
|
+
return depth;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// src/loggers/console-log-pretty-exporter.ts
|
|
357
|
+
var ConsoleLogPrettyExporter = class {
|
|
358
|
+
logThreshold;
|
|
359
|
+
constructor() {
|
|
360
|
+
const defaultLogLevel = "INFO";
|
|
361
|
+
const env = process.env.LOG_LEVEL?.toUpperCase() ?? defaultLogLevel;
|
|
362
|
+
this.logThreshold = LOG_SEVERITY_MAP[env] ?? LOG_SEVERITY_MAP[defaultLogLevel];
|
|
363
|
+
}
|
|
364
|
+
export(logs3, resultCallback) {
|
|
365
|
+
this._sendLogRecords(logs3, resultCallback);
|
|
366
|
+
}
|
|
367
|
+
shutdown() {
|
|
368
|
+
return Promise.resolve();
|
|
369
|
+
}
|
|
370
|
+
forceFlush() {
|
|
371
|
+
return Promise.resolve();
|
|
372
|
+
}
|
|
373
|
+
_sendLogRecords(logRecords, done) {
|
|
374
|
+
for (const record of logRecords) {
|
|
375
|
+
if ((record.severityNumber ?? 0) >= this.logThreshold) {
|
|
376
|
+
console.log(formatLogRecord(record));
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
done?.({ code: ExportResultCode.SUCCESS });
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
// src/loggers/console-span-pretty-exporter.ts
|
|
384
|
+
import { SpanStatusCode as SpanStatusCode2 } from "@opentelemetry/api";
|
|
385
|
+
import { ExportResultCode as ExportResultCode2 } from "@opentelemetry/core";
|
|
386
|
+
var ConsoleSpanPrettyExporter = class {
|
|
387
|
+
allowedStatuses;
|
|
388
|
+
constructor() {
|
|
389
|
+
const env = process.env.SPAN_LEVEL?.toUpperCase();
|
|
390
|
+
if (!env) {
|
|
391
|
+
this.allowedStatuses = /* @__PURE__ */ new Set([
|
|
392
|
+
SpanStatusCode2.UNSET,
|
|
393
|
+
SpanStatusCode2.OK,
|
|
394
|
+
SpanStatusCode2.ERROR
|
|
395
|
+
]);
|
|
396
|
+
} else {
|
|
397
|
+
const map = {
|
|
398
|
+
UNSET: SpanStatusCode2.UNSET,
|
|
399
|
+
OK: SpanStatusCode2.OK,
|
|
400
|
+
ERROR: SpanStatusCode2.ERROR
|
|
401
|
+
};
|
|
402
|
+
this.allowedStatuses = new Set(
|
|
403
|
+
env.split(",").map((s) => s.trim()).map((s) => map[s]).filter((v) => typeof v === "number")
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
shouldExport(spans) {
|
|
408
|
+
if (this.allowedStatuses.size === 3) {
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
411
|
+
return spans.some((span) => this.allowedStatuses.has(span.status.code));
|
|
412
|
+
}
|
|
413
|
+
export(spans, resultCallback) {
|
|
414
|
+
if (this.shouldExport(spans)) {
|
|
415
|
+
console.log(formatSpans(spans));
|
|
416
|
+
}
|
|
417
|
+
resultCallback({ code: ExportResultCode2.SUCCESS });
|
|
418
|
+
}
|
|
419
|
+
shutdown() {
|
|
420
|
+
return Promise.resolve();
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
// src/loggers/console-metric-pretty-exporter.ts
|
|
425
|
+
import { ExportResultCode as ExportResultCode3 } from "@opentelemetry/core";
|
|
426
|
+
var ConsoleMetricPrettyExporter = class {
|
|
427
|
+
patterns;
|
|
428
|
+
constructor() {
|
|
429
|
+
const raw = process.env.METRIC_FILTER ?? "";
|
|
430
|
+
const entries = raw.split(",").map((e) => e.trim()).filter(Boolean);
|
|
431
|
+
this.patterns = entries.map(globToRegex);
|
|
432
|
+
}
|
|
433
|
+
filterMetrics(resourceMetrics) {
|
|
434
|
+
if (this.patterns.length === 0) return void 0;
|
|
435
|
+
const filteredScopes = resourceMetrics.scopeMetrics.map((scopeMetric) => {
|
|
436
|
+
const filteredMetrics = scopeMetric.metrics.filter(
|
|
437
|
+
(metric) => this.patterns.some((pattern) => pattern.test(metric.descriptor.name))
|
|
438
|
+
);
|
|
439
|
+
if (filteredMetrics.length === 0) return void 0;
|
|
440
|
+
return {
|
|
441
|
+
...scopeMetric,
|
|
442
|
+
metrics: filteredMetrics
|
|
443
|
+
};
|
|
444
|
+
}).filter((s) => s !== void 0);
|
|
445
|
+
if (filteredScopes.length === 0) return void 0;
|
|
446
|
+
return {
|
|
447
|
+
...resourceMetrics,
|
|
448
|
+
scopeMetrics: filteredScopes
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
export(metrics2, resultCallback) {
|
|
452
|
+
const filtered = this.filterMetrics(metrics2);
|
|
453
|
+
if (filtered) {
|
|
454
|
+
console.log(formatMetrics(filtered));
|
|
455
|
+
}
|
|
456
|
+
resultCallback({ code: ExportResultCode3.SUCCESS });
|
|
457
|
+
}
|
|
458
|
+
shutdown() {
|
|
459
|
+
return Promise.resolve();
|
|
460
|
+
}
|
|
461
|
+
forceFlush() {
|
|
462
|
+
return Promise.resolve();
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
function globToRegex(glob) {
|
|
466
|
+
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
467
|
+
const regex = `^${escaped.replace(/\*/g, ".*")}$`;
|
|
468
|
+
return new RegExp(regex);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// src/loggers/tree-span-processor.ts
|
|
472
|
+
var TreeSpanProcessor = class {
|
|
473
|
+
exporter;
|
|
474
|
+
orphans = /* @__PURE__ */ new Map();
|
|
475
|
+
constructor(exporter) {
|
|
476
|
+
this.exporter = exporter;
|
|
477
|
+
}
|
|
478
|
+
onStart() {
|
|
479
|
+
}
|
|
480
|
+
onEnd(span) {
|
|
481
|
+
const parentId = span.parentSpanContext?.spanId;
|
|
482
|
+
if (parentId) {
|
|
483
|
+
const siblings = this.orphans.get(parentId) || [];
|
|
484
|
+
this.orphans.set(parentId, [...siblings, span]);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
const children = this.getChildrenRecursively(span);
|
|
488
|
+
const sorted = [span, ...children].sort((s1, s2) => {
|
|
489
|
+
const [sec1, nano1] = s1.startTime;
|
|
490
|
+
const [sec2, nano2] = s2.startTime;
|
|
491
|
+
if (sec1 !== sec2) return sec1 - sec2;
|
|
492
|
+
return nano1 - nano2;
|
|
53
493
|
});
|
|
54
|
-
|
|
55
|
-
console.log(`[otel] Telemetry initialized for "${serviceName}"`);
|
|
56
|
-
process.on("SIGTERM", async () => {
|
|
57
|
-
console.log("[otel] Shutting down...");
|
|
58
|
-
await Promise.all([sdk.shutdown(), logProvider.shutdown()]);
|
|
59
|
-
console.log("[otel] Shutdown complete.");
|
|
60
|
-
process.exit(0);
|
|
494
|
+
this.exporter.export(sorted, () => {
|
|
61
495
|
});
|
|
62
|
-
} catch (err) {
|
|
63
|
-
console.error("[otel] Startup error:", err);
|
|
64
496
|
}
|
|
65
|
-
|
|
66
|
-
|
|
497
|
+
getChildrenRecursively(span) {
|
|
498
|
+
const spanId = span.spanContext().spanId;
|
|
499
|
+
const children = this.orphans.get(spanId) || [];
|
|
500
|
+
this.orphans.delete(spanId);
|
|
501
|
+
const result = [...children];
|
|
502
|
+
for (const child of children) {
|
|
503
|
+
result.push(...this.getChildrenRecursively(child));
|
|
504
|
+
}
|
|
505
|
+
return result;
|
|
506
|
+
}
|
|
507
|
+
shutdown() {
|
|
508
|
+
return this.exporter.shutdown();
|
|
509
|
+
}
|
|
510
|
+
async forceFlush() {
|
|
511
|
+
await this.exporter.forceFlush?.();
|
|
512
|
+
}
|
|
513
|
+
};
|
|
67
514
|
|
|
68
|
-
// src/
|
|
69
|
-
|
|
70
|
-
|
|
515
|
+
// src/providers.ts
|
|
516
|
+
var getLogProvider = (resource, otlpEndpoint) => {
|
|
517
|
+
const exporter = otlpEndpoint ? new OTLPLogExporter({ url: `${otlpEndpoint}/v1/logs` }) : new ConsoleLogPrettyExporter();
|
|
518
|
+
const processor = otlpEndpoint ? new BatchLogRecordProcessor(exporter) : new SimpleLogRecordProcessor(exporter);
|
|
519
|
+
const provider = new LoggerProvider({
|
|
520
|
+
resource,
|
|
521
|
+
processors: [processor]
|
|
522
|
+
});
|
|
523
|
+
return provider;
|
|
524
|
+
};
|
|
525
|
+
var getSpanProcessor = (otlpEndpoint) => {
|
|
526
|
+
const exporter = otlpEndpoint ? new OTLPTraceExporter({
|
|
527
|
+
url: `${otlpEndpoint}/v1/traces`
|
|
528
|
+
}) : new ConsoleSpanPrettyExporter();
|
|
529
|
+
const processor = otlpEndpoint ? new BatchSpanProcessor(exporter) : new TreeSpanProcessor(exporter);
|
|
530
|
+
return processor;
|
|
531
|
+
};
|
|
532
|
+
var getMetricReader = (otlpEndpoint) => {
|
|
533
|
+
const metricExporter = otlpEndpoint ? new OTLPMetricExporter({
|
|
534
|
+
url: `${otlpEndpoint}/v1/metrics`
|
|
535
|
+
}) : new ConsoleMetricPrettyExporter();
|
|
536
|
+
const metricReader = new PeriodicExportingMetricReader({
|
|
537
|
+
exporter: metricExporter
|
|
538
|
+
});
|
|
539
|
+
return metricReader;
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
// src/resource.ts
|
|
543
|
+
import {
|
|
544
|
+
detectResources,
|
|
545
|
+
resourceFromAttributes
|
|
546
|
+
} from "@opentelemetry/resources";
|
|
71
547
|
|
|
72
548
|
// src/otel-context.ts
|
|
73
|
-
|
|
549
|
+
import {
|
|
550
|
+
ATTR_SERVICE_NAME as ATTR_SERVICE_NAME2,
|
|
551
|
+
ATTR_SERVICE_VERSION as ATTR_SERVICE_VERSION2
|
|
552
|
+
} from "@opentelemetry/semantic-conventions";
|
|
553
|
+
function detectTelemetryContext(componentNameOverride) {
|
|
74
554
|
const {
|
|
75
555
|
OTEL_SERVICE_NAME,
|
|
556
|
+
// e.g. "UserSystem"
|
|
76
557
|
OTEL_SERVICE_VERSION,
|
|
558
|
+
// e.g. "1.2.3"
|
|
77
559
|
K_SERVICE,
|
|
78
560
|
K_REVISION,
|
|
79
561
|
K_CONFIGURATION,
|
|
@@ -83,13 +565,14 @@ function detectTelemetryContext(serviceOverride) {
|
|
|
83
565
|
GCP_PROJECT,
|
|
84
566
|
CLOUD_PROVIDER
|
|
85
567
|
} = process.env;
|
|
86
|
-
const
|
|
87
|
-
const
|
|
568
|
+
const systemName = OTEL_SERVICE_NAME || "unknown-service";
|
|
569
|
+
const systemVersion = OTEL_SERVICE_VERSION || "1.0.0";
|
|
570
|
+
const componentName = componentNameOverride || void 0;
|
|
88
571
|
const resourceAttributes = {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
"serviceContext.service":
|
|
92
|
-
"serviceContext.version":
|
|
572
|
+
[ATTR_SERVICE_NAME2]: systemName,
|
|
573
|
+
[ATTR_SERVICE_VERSION2]: systemVersion,
|
|
574
|
+
"serviceContext.service": systemName,
|
|
575
|
+
"serviceContext.version": systemVersion,
|
|
93
576
|
...K_SERVICE && { "cloud.run.service": K_SERVICE },
|
|
94
577
|
...K_REVISION && { "cloud.run.revision": K_REVISION },
|
|
95
578
|
...K_CONFIGURATION && { "cloud.run.configuration": K_CONFIGURATION },
|
|
@@ -97,28 +580,75 @@ function detectTelemetryContext(serviceOverride) {
|
|
|
97
580
|
...POD_NAMESPACE && { "k8s.namespace_name": POD_NAMESPACE },
|
|
98
581
|
...KUBERNETES_SERVICE_HOST && { "cloud.orchestrator": "kubernetes" },
|
|
99
582
|
...GCP_PROJECT && { "cloud.account.id": GCP_PROJECT },
|
|
100
|
-
...CLOUD_PROVIDER && { "cloud.provider": CLOUD_PROVIDER }
|
|
583
|
+
...CLOUD_PROVIDER && { "cloud.provider": CLOUD_PROVIDER },
|
|
584
|
+
...componentName && { "component.name": componentName }
|
|
101
585
|
};
|
|
102
586
|
return {
|
|
103
|
-
|
|
104
|
-
|
|
587
|
+
systemName,
|
|
588
|
+
systemVersion,
|
|
589
|
+
componentName,
|
|
105
590
|
resourceAttributes
|
|
106
591
|
};
|
|
107
592
|
}
|
|
108
593
|
|
|
594
|
+
// src/resource.ts
|
|
595
|
+
var getResource = async () => {
|
|
596
|
+
const baseRes = await detectResources();
|
|
597
|
+
const { resourceAttributes } = detectTelemetryContext();
|
|
598
|
+
const customRes = resourceFromAttributes(resourceAttributes);
|
|
599
|
+
const resource = baseRes.merge(customRes);
|
|
600
|
+
return resource;
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
// src/otel.ts
|
|
604
|
+
diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.ERROR);
|
|
605
|
+
async function initialize() {
|
|
606
|
+
try {
|
|
607
|
+
const serviceName = process.env.OTEL_SERVICE_NAME ?? "unknown-service";
|
|
608
|
+
const otlpEndpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
|
|
609
|
+
const resource = await getResource();
|
|
610
|
+
context.setGlobalContextManager(
|
|
611
|
+
new AsyncLocalStorageContextManager().enable()
|
|
612
|
+
);
|
|
613
|
+
const logProvider = getLogProvider(resource, otlpEndpoint);
|
|
614
|
+
logs.setGlobalLoggerProvider(logProvider);
|
|
615
|
+
const spanProcessor = getSpanProcessor(otlpEndpoint);
|
|
616
|
+
const metricReader = getMetricReader(otlpEndpoint);
|
|
617
|
+
const sdk = new NodeSDK({
|
|
618
|
+
spanProcessor,
|
|
619
|
+
metricReader,
|
|
620
|
+
instrumentations: [getNodeAutoInstrumentations()],
|
|
621
|
+
resource
|
|
622
|
+
});
|
|
623
|
+
await sdk.start();
|
|
624
|
+
console.log(`[otel] Telemetry initialized for "${serviceName}"`);
|
|
625
|
+
process.on("SIGTERM", async () => {
|
|
626
|
+
console.log("[otel] Shutting down...");
|
|
627
|
+
await Promise.all([sdk.shutdown(), logProvider.shutdown()]);
|
|
628
|
+
console.log("[otel] Shutdown complete.");
|
|
629
|
+
process.exit(0);
|
|
630
|
+
});
|
|
631
|
+
} catch (err) {
|
|
632
|
+
console.error("[otel] Startup error:", err);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
109
636
|
// src/logger.ts
|
|
637
|
+
import { context as context2, trace } from "@opentelemetry/api";
|
|
638
|
+
import { logs as logs2 } from "@opentelemetry/api-logs";
|
|
110
639
|
function getLogger(serviceOverride, extraAttrs = {}) {
|
|
111
|
-
const {
|
|
112
|
-
const logger = logs2.getLogger(
|
|
640
|
+
const { systemName, systemVersion, resourceAttributes } = detectTelemetryContext(serviceOverride);
|
|
641
|
+
const logger = logs2.getLogger(systemName, systemVersion);
|
|
113
642
|
const defaultAttrs = {
|
|
114
643
|
...resourceAttributes,
|
|
115
644
|
...extraAttrs
|
|
116
645
|
};
|
|
117
646
|
function emit(severityText, body, attrs = {}) {
|
|
118
|
-
const span = trace.getSpan(
|
|
647
|
+
const span = trace.getSpan(context2.active());
|
|
119
648
|
const spanContext = span?.spanContext();
|
|
120
649
|
logger.emit({
|
|
121
650
|
severityText,
|
|
651
|
+
severityNumber: LOG_SEVERITY_MAP[severityText],
|
|
122
652
|
body,
|
|
123
653
|
attributes: {
|
|
124
654
|
...defaultAttrs,
|
|
@@ -147,33 +677,43 @@ function getLogger(serviceOverride, extraAttrs = {}) {
|
|
|
147
677
|
|
|
148
678
|
// src/metrics.ts
|
|
149
679
|
import { metrics } from "@opentelemetry/api";
|
|
150
|
-
function getMeter(
|
|
151
|
-
const {
|
|
152
|
-
|
|
680
|
+
function getMeter(componentNameOverride) {
|
|
681
|
+
const { componentName, systemName, systemVersion } = detectTelemetryContext(
|
|
682
|
+
componentNameOverride
|
|
683
|
+
);
|
|
684
|
+
return metrics.getMeter(componentName ?? systemName, systemVersion);
|
|
153
685
|
}
|
|
154
686
|
|
|
155
687
|
// src/tracer.ts
|
|
156
688
|
import {
|
|
157
|
-
context as
|
|
158
|
-
SpanStatusCode,
|
|
689
|
+
context as context3,
|
|
690
|
+
SpanStatusCode as SpanStatusCode3,
|
|
159
691
|
trace as trace2
|
|
160
692
|
} from "@opentelemetry/api";
|
|
161
|
-
function getTracer(
|
|
162
|
-
const {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
693
|
+
function getTracer(componentNameOverride) {
|
|
694
|
+
const { componentName, systemName, systemVersion } = detectTelemetryContext(
|
|
695
|
+
componentNameOverride
|
|
696
|
+
);
|
|
697
|
+
const tracer = trace2.getTracer(
|
|
698
|
+
componentName ?? systemName,
|
|
699
|
+
systemVersion
|
|
700
|
+
);
|
|
701
|
+
const withTrace = async (name, spanOptionsSpanOrFunc, spanOrFunc, func) => {
|
|
702
|
+
const { options, parent, fn } = extractArgs(
|
|
703
|
+
spanOptionsSpanOrFunc,
|
|
704
|
+
spanOrFunc,
|
|
705
|
+
func
|
|
706
|
+
);
|
|
707
|
+
const parentContext = parent ? trace2.setSpan(context3.active(), parent) : context3.active();
|
|
708
|
+
const span = tracer.startSpan(name, options, parentContext);
|
|
709
|
+
return await context3.with(trace2.setSpan(parentContext, span), async () => {
|
|
170
710
|
try {
|
|
171
711
|
const result = await fn(span);
|
|
172
|
-
span.setStatus({ code:
|
|
712
|
+
span.setStatus({ code: SpanStatusCode3.OK });
|
|
173
713
|
return result;
|
|
174
714
|
} catch (err) {
|
|
175
715
|
const error = err;
|
|
176
|
-
span.setStatus({ code:
|
|
716
|
+
span.setStatus({ code: SpanStatusCode3.ERROR, message: error.message });
|
|
177
717
|
span.recordException?.(error);
|
|
178
718
|
throw err;
|
|
179
719
|
} finally {
|
|
@@ -181,19 +721,22 @@ function getTracer(serviceOverride) {
|
|
|
181
721
|
}
|
|
182
722
|
});
|
|
183
723
|
};
|
|
184
|
-
const withTraceSync = (name,
|
|
185
|
-
const options
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
724
|
+
const withTraceSync = (name, spanOptionsSpanOrFunc, spanOrFunc, func) => {
|
|
725
|
+
const { options, parent, fn } = extractArgs(
|
|
726
|
+
spanOptionsSpanOrFunc,
|
|
727
|
+
spanOrFunc,
|
|
728
|
+
func
|
|
729
|
+
);
|
|
730
|
+
const parentContext = parent ? trace2.setSpan(context3.active(), parent) : context3.active();
|
|
731
|
+
const span = tracer.startSpan(name, options, parentContext);
|
|
732
|
+
return context3.with(trace2.setSpan(parentContext, span), () => {
|
|
190
733
|
try {
|
|
191
734
|
const result = fn(span);
|
|
192
|
-
span.setStatus({ code:
|
|
735
|
+
span.setStatus({ code: SpanStatusCode3.OK });
|
|
193
736
|
return result;
|
|
194
737
|
} catch (err) {
|
|
195
738
|
const error = err;
|
|
196
|
-
span.setStatus({ code:
|
|
739
|
+
span.setStatus({ code: SpanStatusCode3.ERROR, message: error.message });
|
|
197
740
|
span.recordException?.(error);
|
|
198
741
|
throw err;
|
|
199
742
|
} finally {
|
|
@@ -205,6 +748,30 @@ function getTracer(serviceOverride) {
|
|
|
205
748
|
tracer.withTraceSync = withTraceSync;
|
|
206
749
|
return tracer;
|
|
207
750
|
}
|
|
751
|
+
function extractArgs(spanOptionsSpanOrFunc, spanOrFunc, func) {
|
|
752
|
+
let options = {};
|
|
753
|
+
let parent;
|
|
754
|
+
let fn;
|
|
755
|
+
if (typeof spanOptionsSpanOrFunc === "function") {
|
|
756
|
+
fn = spanOptionsSpanOrFunc;
|
|
757
|
+
} else if (typeof spanOrFunc === "function") {
|
|
758
|
+
const spanOrSpanOptions = spanOptionsSpanOrFunc;
|
|
759
|
+
if ("startTime" in spanOrSpanOptions || "attributes" in spanOrSpanOptions || "kind" in spanOrSpanOptions) {
|
|
760
|
+
options = spanOrSpanOptions;
|
|
761
|
+
} else {
|
|
762
|
+
parent = spanOrSpanOptions;
|
|
763
|
+
}
|
|
764
|
+
fn = spanOrFunc;
|
|
765
|
+
} else {
|
|
766
|
+
options = spanOptionsSpanOrFunc;
|
|
767
|
+
parent = spanOrFunc;
|
|
768
|
+
fn = func;
|
|
769
|
+
}
|
|
770
|
+
return { options, parent, fn };
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// src/index.ts
|
|
774
|
+
initialize();
|
|
208
775
|
export {
|
|
209
776
|
getLogger,
|
|
210
777
|
getMeter,
|