@aztec/telemetry-client 0.75.0-commit.c03ba01a2a4122e43e90d5133ba017e54b90e9d2
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/dest/attributes.js +66 -0
- package/dest/aztec_resource_detector.js +18 -0
- package/dest/bench.js +98 -0
- package/dest/config.js +66 -0
- package/dest/event_loop_monitor.js +93 -0
- package/dest/histogram_utils.js +47 -0
- package/dest/index.js +10 -0
- package/dest/lmdb_metrics.js +42 -0
- package/dest/metrics.js +123 -0
- package/dest/noop.js +71 -0
- package/dest/otel.js +237 -0
- package/dest/otel_filter_metric_exporter.js +33 -0
- package/dest/otel_logger_provider.js +25 -0
- package/dest/otel_propagation.js +44 -0
- package/dest/otel_resource.js +16 -0
- package/dest/prom_otel_adapter.js +183 -0
- package/dest/start.js +24 -0
- package/dest/telemetry.js +116 -0
- package/dest/vendor/attributes.js +5 -0
- package/dest/vendor/otel-pino-stream.js +229 -0
- package/dest/with_tracer.js +32 -0
- package/dest/wrappers/fetch.js +39 -0
- package/dest/wrappers/index.js +3 -0
- package/dest/wrappers/json_rpc_server.js +11 -0
- package/dest/wrappers/l2_block_stream.js +26 -0
- package/package.json +92 -0
- package/src/attributes.ts +113 -0
- package/src/aztec_resource_detector.ts +28 -0
- package/src/bench.ts +153 -0
- package/src/config.ts +89 -0
- package/src/event_loop_monitor.ts +119 -0
- package/src/histogram_utils.ts +50 -0
- package/src/index.ts +10 -0
- package/src/lmdb_metrics.ts +45 -0
- package/src/metrics.ts +151 -0
- package/src/noop.ts +91 -0
- package/src/otel.ts +299 -0
- package/src/otel_filter_metric_exporter.ts +38 -0
- package/src/otel_logger_provider.ts +31 -0
- package/src/otel_propagation.ts +50 -0
- package/src/otel_resource.ts +26 -0
- package/src/prom_otel_adapter.ts +326 -0
- package/src/start.ts +33 -0
- package/src/telemetry.ts +267 -0
- package/src/vendor/attributes.ts +5 -0
- package/src/vendor/otel-pino-stream.ts +282 -0
- package/src/with_tracer.ts +35 -0
- package/src/wrappers/fetch.ts +52 -0
- package/src/wrappers/index.ts +3 -0
- package/src/wrappers/json_rpc_server.ts +15 -0
- package/src/wrappers/l2_block_stream.ts +37 -0
package/dest/otel.js
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { addLogDataHandler } from '@aztec/foundation/log';
|
|
2
|
+
import { MetricExporter as GoogleCloudMetricExporter } from '@google-cloud/opentelemetry-cloud-monitoring-exporter';
|
|
3
|
+
import { TraceExporter as GoogleCloudTraceExporter } from '@google-cloud/opentelemetry-cloud-trace-exporter';
|
|
4
|
+
import { DiagConsoleLogger, DiagLogLevel, context, diag, isSpanContextValid, trace } from '@opentelemetry/api';
|
|
5
|
+
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';
|
|
6
|
+
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
|
|
7
|
+
import { HostMetrics } from '@opentelemetry/host-metrics';
|
|
8
|
+
import { ExplicitBucketHistogramAggregation, InstrumentType, MeterProvider, PeriodicExportingMetricReader, View } from '@opentelemetry/sdk-metrics';
|
|
9
|
+
import { BatchSpanProcessor, NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
|
|
10
|
+
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';
|
|
11
|
+
import { EventLoopMonitor } from './event_loop_monitor.js';
|
|
12
|
+
import { linearBuckets } from './histogram_utils.js';
|
|
13
|
+
import { OtelFilterMetricExporter } from './otel_filter_metric_exporter.js';
|
|
14
|
+
import { registerOtelLoggerProvider } from './otel_logger_provider.js';
|
|
15
|
+
import { getOtelResource } from './otel_resource.js';
|
|
16
|
+
export class OpenTelemetryClient {
|
|
17
|
+
resource;
|
|
18
|
+
meterProvider;
|
|
19
|
+
traceProvider;
|
|
20
|
+
loggerProvider;
|
|
21
|
+
log;
|
|
22
|
+
hostMetrics;
|
|
23
|
+
eventLoopMonitor;
|
|
24
|
+
targetInfo;
|
|
25
|
+
meters;
|
|
26
|
+
tracers;
|
|
27
|
+
constructor(resource, meterProvider, traceProvider, loggerProvider, log){
|
|
28
|
+
this.resource = resource;
|
|
29
|
+
this.meterProvider = meterProvider;
|
|
30
|
+
this.traceProvider = traceProvider;
|
|
31
|
+
this.loggerProvider = loggerProvider;
|
|
32
|
+
this.log = log;
|
|
33
|
+
this.meters = new Map();
|
|
34
|
+
this.tracers = new Map();
|
|
35
|
+
}
|
|
36
|
+
getMeter(name) {
|
|
37
|
+
let meter = this.meters.get(name);
|
|
38
|
+
if (!meter) {
|
|
39
|
+
meter = this.meterProvider.getMeter(name, this.resource.attributes[ATTR_SERVICE_VERSION]);
|
|
40
|
+
this.meters.set(name, meter);
|
|
41
|
+
}
|
|
42
|
+
return meter;
|
|
43
|
+
}
|
|
44
|
+
getTracer(name) {
|
|
45
|
+
let tracer = this.tracers.get(name);
|
|
46
|
+
if (!tracer) {
|
|
47
|
+
tracer = this.traceProvider.getTracer(name, this.resource.attributes[ATTR_SERVICE_VERSION]);
|
|
48
|
+
this.tracers.set(name, tracer);
|
|
49
|
+
}
|
|
50
|
+
return tracer;
|
|
51
|
+
}
|
|
52
|
+
start() {
|
|
53
|
+
this.log.info('Starting OpenTelemetry client');
|
|
54
|
+
diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.INFO);
|
|
55
|
+
// Add a callback to the logger to set context data from current trace
|
|
56
|
+
// Adapted from open-telemetry/opentelemetry-js-contrib PinoInstrumentation._getMixinFunction
|
|
57
|
+
addLogDataHandler((data)=>{
|
|
58
|
+
const spanContext = trace.getSpan(context.active())?.spanContext();
|
|
59
|
+
return spanContext && isSpanContextValid(spanContext) ? {
|
|
60
|
+
...data,
|
|
61
|
+
['trace_id']: spanContext.traceId,
|
|
62
|
+
['span_id']: spanContext.spanId,
|
|
63
|
+
['trace_flags']: `0${spanContext.traceFlags.toString(16)}`
|
|
64
|
+
} : data;
|
|
65
|
+
});
|
|
66
|
+
this.hostMetrics = new HostMetrics({
|
|
67
|
+
name: this.resource.attributes[ATTR_SERVICE_NAME],
|
|
68
|
+
meterProvider: this.meterProvider
|
|
69
|
+
});
|
|
70
|
+
this.eventLoopMonitor = new EventLoopMonitor(this.meterProvider.getMeter(this.resource.attributes[ATTR_SERVICE_NAME]));
|
|
71
|
+
// See these two links for more information on providing target information:
|
|
72
|
+
// https://opentelemetry.io/docs/specs/otel/compatibility/prometheus_and_openmetrics/#resource-attributes
|
|
73
|
+
// https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#supporting-target-metadata-in-both-push-based-and-pull-based-systems
|
|
74
|
+
this.targetInfo = this.meterProvider.getMeter('target').createGauge('target_info', {
|
|
75
|
+
description: 'Target metadata'
|
|
76
|
+
});
|
|
77
|
+
this.targetInfo.record(1, this.resource.attributes);
|
|
78
|
+
this.hostMetrics.start();
|
|
79
|
+
this.eventLoopMonitor.start();
|
|
80
|
+
}
|
|
81
|
+
isEnabled() {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
async flush() {
|
|
85
|
+
await Promise.all([
|
|
86
|
+
this.meterProvider.forceFlush(),
|
|
87
|
+
this.loggerProvider?.forceFlush(),
|
|
88
|
+
this.traceProvider instanceof NodeTracerProvider ? this.traceProvider.forceFlush() : Promise.resolve()
|
|
89
|
+
]);
|
|
90
|
+
}
|
|
91
|
+
async stop() {
|
|
92
|
+
this.eventLoopMonitor?.stop();
|
|
93
|
+
const flushAndShutdown = async (provider)=>{
|
|
94
|
+
if (!provider) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
await provider.forceFlush();
|
|
98
|
+
await provider.shutdown();
|
|
99
|
+
};
|
|
100
|
+
await Promise.all([
|
|
101
|
+
flushAndShutdown(this.meterProvider),
|
|
102
|
+
flushAndShutdown(this.loggerProvider),
|
|
103
|
+
this.traceProvider instanceof NodeTracerProvider ? flushAndShutdown(this.traceProvider) : Promise.resolve()
|
|
104
|
+
]);
|
|
105
|
+
}
|
|
106
|
+
static createMeterProvider(resource, options) {
|
|
107
|
+
return new MeterProvider({
|
|
108
|
+
resource,
|
|
109
|
+
readers: options.exporter ? [
|
|
110
|
+
new PeriodicExportingMetricReader(options)
|
|
111
|
+
] : [],
|
|
112
|
+
views: [
|
|
113
|
+
// Every histogram matching the selector (type + unit) gets these custom buckets assigned
|
|
114
|
+
new View({
|
|
115
|
+
instrumentType: InstrumentType.HISTOGRAM,
|
|
116
|
+
instrumentUnit: 'ms',
|
|
117
|
+
aggregation: new ExplicitBucketHistogramAggregation(// 181 buckets between 5ms and 1hr
|
|
118
|
+
[
|
|
119
|
+
...linearBuckets(5, 100, 20),
|
|
120
|
+
...linearBuckets(100, 1_000, 20).slice(1),
|
|
121
|
+
...linearBuckets(1_000, 10_000, 20).slice(1),
|
|
122
|
+
...linearBuckets(10_000, 60_000, 20).slice(1),
|
|
123
|
+
...linearBuckets(60_000, 300_000, 20).slice(1),
|
|
124
|
+
...linearBuckets(300_000, 600_000, 20).slice(1),
|
|
125
|
+
...linearBuckets(600_000, 1_200_000, 20).slice(1),
|
|
126
|
+
...linearBuckets(1_200_000, 1_800_000, 20).slice(1),
|
|
127
|
+
...linearBuckets(1_800_000, 3_600_000, 20).slice(1)
|
|
128
|
+
], true)
|
|
129
|
+
}),
|
|
130
|
+
new View({
|
|
131
|
+
instrumentType: InstrumentType.HISTOGRAM,
|
|
132
|
+
instrumentUnit: 'us',
|
|
133
|
+
aggregation: new ExplicitBucketHistogramAggregation([
|
|
134
|
+
...linearBuckets(5, 100, 20),
|
|
135
|
+
...linearBuckets(100, 1_000, 20).slice(1),
|
|
136
|
+
...linearBuckets(1_000, 10_000, 90).slice(1),
|
|
137
|
+
...linearBuckets(10_000, 100_000, 20).slice(1),
|
|
138
|
+
...linearBuckets(100_000, 1_000_000, 20).slice(1),
|
|
139
|
+
...linearBuckets(1_000_000, 60_000_000, 20).slice(1)
|
|
140
|
+
], true)
|
|
141
|
+
}),
|
|
142
|
+
new View({
|
|
143
|
+
instrumentType: InstrumentType.HISTOGRAM,
|
|
144
|
+
instrumentUnit: 'By',
|
|
145
|
+
aggregation: new ExplicitBucketHistogramAggregation(// 143 buckets between 32 bytes and 2MB
|
|
146
|
+
[
|
|
147
|
+
...linearBuckets(2 ** 5, 2 ** 10, 31),
|
|
148
|
+
...linearBuckets(2 ** 10, 2 ** 15, 31).slice(1),
|
|
149
|
+
...linearBuckets(2 ** 15, 2 ** 18, 32).slice(1),
|
|
150
|
+
...linearBuckets(2 ** 18, 2 ** 20, 32).slice(1),
|
|
151
|
+
...linearBuckets(2 ** 20, 2 ** 21, 16).slice(1)
|
|
152
|
+
], true)
|
|
153
|
+
}),
|
|
154
|
+
new View({
|
|
155
|
+
instrumentType: InstrumentType.HISTOGRAM,
|
|
156
|
+
instrumentUnit: 'gas/s',
|
|
157
|
+
aggregation: new ExplicitBucketHistogramAggregation([
|
|
158
|
+
...linearBuckets(100_000, 10_000_000, 100),
|
|
159
|
+
...linearBuckets(10_000_000, 100_000_000, 100).slice(1)
|
|
160
|
+
], true)
|
|
161
|
+
}),
|
|
162
|
+
new View({
|
|
163
|
+
instrumentType: InstrumentType.HISTOGRAM,
|
|
164
|
+
instrumentUnit: 'mana/s',
|
|
165
|
+
aggregation: new ExplicitBucketHistogramAggregation([
|
|
166
|
+
...linearBuckets(100_000, 10_000_000, 100),
|
|
167
|
+
...linearBuckets(10_000_000, 100_000_000, 100).slice(1)
|
|
168
|
+
], true)
|
|
169
|
+
}),
|
|
170
|
+
new View({
|
|
171
|
+
instrumentType: InstrumentType.HISTOGRAM,
|
|
172
|
+
instrumentUnit: 'gas/block',
|
|
173
|
+
aggregation: new ExplicitBucketHistogramAggregation([
|
|
174
|
+
...linearBuckets(100_000, 10_000_000, 100),
|
|
175
|
+
...linearBuckets(10_000_000, 50_000_000, 50).slice(1)
|
|
176
|
+
], true)
|
|
177
|
+
}),
|
|
178
|
+
new View({
|
|
179
|
+
instrumentType: InstrumentType.HISTOGRAM,
|
|
180
|
+
instrumentUnit: 'gas/tx',
|
|
181
|
+
aggregation: new ExplicitBucketHistogramAggregation([
|
|
182
|
+
...linearBuckets(50_000, 1_000_000, 20),
|
|
183
|
+
...linearBuckets(1_000_000, 10_000_000, 100).slice(1)
|
|
184
|
+
], true)
|
|
185
|
+
})
|
|
186
|
+
]
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
static getGcloudClientFactory(config) {
|
|
190
|
+
return (resource, log)=>{
|
|
191
|
+
const tracerProvider = new NodeTracerProvider({
|
|
192
|
+
resource,
|
|
193
|
+
spanProcessors: [
|
|
194
|
+
new BatchSpanProcessor(new GoogleCloudTraceExporter({
|
|
195
|
+
resourceFilter: /.*/
|
|
196
|
+
}))
|
|
197
|
+
]
|
|
198
|
+
});
|
|
199
|
+
tracerProvider.register();
|
|
200
|
+
const meterProvider = OpenTelemetryClient.createMeterProvider(resource, {
|
|
201
|
+
exporter: new OtelFilterMetricExporter(new GoogleCloudMetricExporter(), config.otelExcludeMetrics ?? []),
|
|
202
|
+
exportTimeoutMillis: config.otelExportTimeoutMs,
|
|
203
|
+
exportIntervalMillis: config.otelCollectIntervalMs
|
|
204
|
+
});
|
|
205
|
+
return new OpenTelemetryClient(resource, meterProvider, tracerProvider, undefined, log);
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
static getCustomClientFactory(config) {
|
|
209
|
+
return (resource, log)=>{
|
|
210
|
+
const tracerProvider = new NodeTracerProvider({
|
|
211
|
+
resource,
|
|
212
|
+
spanProcessors: config.tracesCollectorUrl ? [
|
|
213
|
+
new BatchSpanProcessor(new OTLPTraceExporter({
|
|
214
|
+
url: config.tracesCollectorUrl.href
|
|
215
|
+
}))
|
|
216
|
+
] : []
|
|
217
|
+
});
|
|
218
|
+
tracerProvider.register();
|
|
219
|
+
const meterProvider = OpenTelemetryClient.createMeterProvider(resource, {
|
|
220
|
+
exporter: config.metricsCollectorUrl ? new OtelFilterMetricExporter(new OTLPMetricExporter({
|
|
221
|
+
url: config.metricsCollectorUrl.href
|
|
222
|
+
}), config.otelExcludeMetrics ?? []) : undefined,
|
|
223
|
+
exportTimeoutMillis: config.otelExportTimeoutMs,
|
|
224
|
+
exportIntervalMillis: config.otelCollectIntervalMs
|
|
225
|
+
});
|
|
226
|
+
const loggerProvider = registerOtelLoggerProvider(resource, config.logsCollectorUrl);
|
|
227
|
+
return new OpenTelemetryClient(resource, meterProvider, tracerProvider, loggerProvider, log);
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
static createAndStart(config, log) {
|
|
231
|
+
const resource = getOtelResource();
|
|
232
|
+
const factory = config.useGcloudObservability ? OpenTelemetryClient.getGcloudClientFactory(config) : OpenTelemetryClient.getCustomClientFactory(config);
|
|
233
|
+
const service = factory(resource, log);
|
|
234
|
+
service.start();
|
|
235
|
+
return service;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export class OtelFilterMetricExporter {
|
|
2
|
+
exporter;
|
|
3
|
+
excludeMetricPrefixes;
|
|
4
|
+
constructor(exporter, excludeMetricPrefixes){
|
|
5
|
+
this.exporter = exporter;
|
|
6
|
+
this.excludeMetricPrefixes = excludeMetricPrefixes;
|
|
7
|
+
if (exporter.selectAggregation) {
|
|
8
|
+
this.selectAggregation = exporter.selectAggregation.bind(exporter);
|
|
9
|
+
}
|
|
10
|
+
if (exporter.selectAggregationTemporality) {
|
|
11
|
+
this.selectAggregationTemporality = exporter.selectAggregationTemporality.bind(exporter);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export(metrics, resultCallback) {
|
|
15
|
+
const filteredMetrics = {
|
|
16
|
+
resource: metrics.resource,
|
|
17
|
+
scopeMetrics: metrics.scopeMetrics.map(({ scope, metrics })=>({
|
|
18
|
+
scope,
|
|
19
|
+
metrics: this.filterMetrics(metrics)
|
|
20
|
+
})).filter(({ metrics })=>metrics.length > 0)
|
|
21
|
+
};
|
|
22
|
+
this.exporter.export(filteredMetrics, resultCallback);
|
|
23
|
+
}
|
|
24
|
+
filterMetrics(metrics) {
|
|
25
|
+
return metrics.filter((metric)=>!this.excludeMetricPrefixes.some((prefix)=>metric.descriptor.name.startsWith(prefix)));
|
|
26
|
+
}
|
|
27
|
+
forceFlush() {
|
|
28
|
+
return this.exporter.forceFlush();
|
|
29
|
+
}
|
|
30
|
+
shutdown() {
|
|
31
|
+
return this.exporter.shutdown();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { logs as otelLogs } from '@opentelemetry/api-logs';
|
|
2
|
+
import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http';
|
|
3
|
+
import { CompressionAlgorithm } from '@opentelemetry/otlp-exporter-base';
|
|
4
|
+
import { BatchLogRecordProcessor, LoggerProvider } from '@opentelemetry/sdk-logs';
|
|
5
|
+
export function registerOtelLoggerProvider(resource, otelLogsUrl) {
|
|
6
|
+
const loggerProvider = new LoggerProvider({
|
|
7
|
+
resource
|
|
8
|
+
});
|
|
9
|
+
if (!otelLogsUrl) {
|
|
10
|
+
// If no URL provided, return it disconnected.
|
|
11
|
+
return loggerProvider;
|
|
12
|
+
}
|
|
13
|
+
const logExporter = new OTLPLogExporter({
|
|
14
|
+
compression: CompressionAlgorithm.GZIP,
|
|
15
|
+
url: otelLogsUrl.href
|
|
16
|
+
});
|
|
17
|
+
// Add a processor to the logger provider
|
|
18
|
+
loggerProvider.addLogRecordProcessor(new BatchLogRecordProcessor(logExporter, {
|
|
19
|
+
/** The maximum batch size of every export. It must be smaller or equal to
|
|
20
|
+
* maxQueueSize. */ maxExportBatchSize: 1024,
|
|
21
|
+
/** The maximum queue size. After the size is reached log records are dropped. */ maxQueueSize: 4096
|
|
22
|
+
}));
|
|
23
|
+
otelLogs.setGlobalLoggerProvider(loggerProvider);
|
|
24
|
+
return loggerProvider;
|
|
25
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { ROOT_CONTEXT, SpanKind, SpanStatusCode, propagation } from '@opentelemetry/api';
|
|
2
|
+
import { getTelemetryClient } from './start.js';
|
|
3
|
+
import { ATTR_JSONRPC_ERROR_CODE, ATTR_JSONRPC_ERROR_MSG, ATTR_JSONRPC_METHOD, ATTR_JSONRPC_REQUEST_ID } from './vendor/attributes.js';
|
|
4
|
+
export function getOtelJsonRpcPropagationMiddleware(scope = 'JsonRpcServer') {
|
|
5
|
+
return function otelJsonRpcPropagation(ctx, next) {
|
|
6
|
+
const tracer = getTelemetryClient().getTracer(scope);
|
|
7
|
+
const context = propagation.extract(ROOT_CONTEXT, ctx.request.headers);
|
|
8
|
+
const method = ctx.request.body?.method;
|
|
9
|
+
return tracer.startActiveSpan(`JsonRpcServer.${method ?? 'unknown'}`, {
|
|
10
|
+
kind: SpanKind.SERVER
|
|
11
|
+
}, context, async (span)=>{
|
|
12
|
+
if (ctx.id) {
|
|
13
|
+
span.setAttribute(ATTR_JSONRPC_REQUEST_ID, ctx.id);
|
|
14
|
+
}
|
|
15
|
+
if (method) {
|
|
16
|
+
span.setAttribute(ATTR_JSONRPC_METHOD, method);
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
await next();
|
|
20
|
+
const err = ctx.body.error?.message;
|
|
21
|
+
const code = ctx.body.error?.code;
|
|
22
|
+
if (err) {
|
|
23
|
+
span.setStatus({
|
|
24
|
+
code: SpanStatusCode.ERROR,
|
|
25
|
+
message: err
|
|
26
|
+
});
|
|
27
|
+
span.setAttribute(ATTR_JSONRPC_ERROR_CODE, code);
|
|
28
|
+
span.setAttribute(ATTR_JSONRPC_ERROR_MSG, err);
|
|
29
|
+
} else {
|
|
30
|
+
span.setStatus({
|
|
31
|
+
code: SpanStatusCode.OK
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
} catch (err) {
|
|
35
|
+
span.setStatus({
|
|
36
|
+
code: SpanStatusCode.ERROR,
|
|
37
|
+
message: String(err)
|
|
38
|
+
});
|
|
39
|
+
} finally{
|
|
40
|
+
span.end();
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { GcpDetectorSync } from '@google-cloud/opentelemetry-resource-util';
|
|
2
|
+
import { detectResourcesSync, envDetectorSync, osDetectorSync, processDetectorSync, serviceInstanceIdDetectorSync } from '@opentelemetry/resources';
|
|
3
|
+
import { aztecDetector } from './aztec_resource_detector.js';
|
|
4
|
+
export function getOtelResource() {
|
|
5
|
+
const resource = detectResourcesSync({
|
|
6
|
+
detectors: [
|
|
7
|
+
osDetectorSync,
|
|
8
|
+
envDetectorSync,
|
|
9
|
+
processDetectorSync,
|
|
10
|
+
serviceInstanceIdDetectorSync,
|
|
11
|
+
aztecDetector,
|
|
12
|
+
new GcpDetectorSync()
|
|
13
|
+
]
|
|
14
|
+
});
|
|
15
|
+
return resource;
|
|
16
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { createLogger } from '@aztec/foundation/log';
|
|
2
|
+
import { Registry } from 'prom-client';
|
|
3
|
+
export var MessageSource = /*#__PURE__*/ function(MessageSource) {
|
|
4
|
+
MessageSource["forward"] = "forward";
|
|
5
|
+
MessageSource["publish"] = "publish";
|
|
6
|
+
return MessageSource;
|
|
7
|
+
}({});
|
|
8
|
+
/**Otel MetricsType Adapters
|
|
9
|
+
*
|
|
10
|
+
* Some dependencies we use export metrics directly in a Prometheus format
|
|
11
|
+
* This adapter is used to convert those metrics to a format that we can use with OpenTelemetry
|
|
12
|
+
*
|
|
13
|
+
* Affected services include:
|
|
14
|
+
* - chainsafe/gossipsub
|
|
15
|
+
* - libp2p
|
|
16
|
+
*/ export class OtelGauge {
|
|
17
|
+
logger;
|
|
18
|
+
labelNames;
|
|
19
|
+
gauge;
|
|
20
|
+
currentValue;
|
|
21
|
+
labeledValues;
|
|
22
|
+
collectFns;
|
|
23
|
+
_collect;
|
|
24
|
+
get collect() {
|
|
25
|
+
return this._collect;
|
|
26
|
+
}
|
|
27
|
+
set collect(fn) {
|
|
28
|
+
this._collect = fn;
|
|
29
|
+
}
|
|
30
|
+
constructor(logger, meter, name, help, labelNames = []){
|
|
31
|
+
this.logger = logger;
|
|
32
|
+
this.labelNames = labelNames;
|
|
33
|
+
this.currentValue = 0;
|
|
34
|
+
this.labeledValues = new Map();
|
|
35
|
+
this.collectFns = [];
|
|
36
|
+
this._collect = ()=>{};
|
|
37
|
+
this.gauge = meter.createObservableGauge(name, {
|
|
38
|
+
description: help
|
|
39
|
+
});
|
|
40
|
+
// Only observe in the callback when collect() is called
|
|
41
|
+
this.gauge.addCallback(this.handleObservation.bind(this));
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Add a collect callback
|
|
45
|
+
* @param collectFn - Callback function
|
|
46
|
+
*/ addCollect(collectFn) {
|
|
47
|
+
this.collectFns.push(collectFn);
|
|
48
|
+
}
|
|
49
|
+
handleObservation(result) {
|
|
50
|
+
// Execute the main collect function if assigned
|
|
51
|
+
this._collect();
|
|
52
|
+
// Execute all the collect functions
|
|
53
|
+
for (const fn of this.collectFns){
|
|
54
|
+
fn(this);
|
|
55
|
+
}
|
|
56
|
+
// Report the current values
|
|
57
|
+
if (this.labelNames.length === 0) {
|
|
58
|
+
result.observe(this.currentValue);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
for (const [labelStr, value] of this.labeledValues.entries()){
|
|
62
|
+
const labels = this.parseLabelsSafely(labelStr);
|
|
63
|
+
if (labels) {
|
|
64
|
+
result.observe(value, labels);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
inc(labelsOrValue, value) {
|
|
69
|
+
if (typeof labelsOrValue === 'number') {
|
|
70
|
+
this.currentValue += labelsOrValue;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (labelsOrValue) {
|
|
74
|
+
this.validateLabels(labelsOrValue);
|
|
75
|
+
const labelKey = JSON.stringify(labelsOrValue);
|
|
76
|
+
const currentValue = this.labeledValues.get(labelKey) ?? 0;
|
|
77
|
+
this.labeledValues.set(labelKey, currentValue + (value ?? 1));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
this.currentValue += value ?? 1;
|
|
81
|
+
}
|
|
82
|
+
set(labelsOrValue, value) {
|
|
83
|
+
if (typeof labelsOrValue === 'number') {
|
|
84
|
+
this.currentValue = labelsOrValue;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
this.validateLabels(labelsOrValue);
|
|
88
|
+
const labelKey = JSON.stringify(labelsOrValue);
|
|
89
|
+
this.labeledValues.set(labelKey, value);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Decrements the gauge value
|
|
93
|
+
* @param labels - Optional labels object
|
|
94
|
+
*/ dec(labels) {
|
|
95
|
+
if (labels) {
|
|
96
|
+
this.validateLabels(labels);
|
|
97
|
+
const labelKey = JSON.stringify(labels);
|
|
98
|
+
const currentValue = this.labeledValues.get(labelKey) ?? 0;
|
|
99
|
+
this.labeledValues.set(labelKey, currentValue - 1);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
this.currentValue -= 1;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Resets the gauge to initial state
|
|
106
|
+
*/ reset() {
|
|
107
|
+
this.currentValue = 0;
|
|
108
|
+
this.labeledValues.clear();
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Validates that provided labels match the expected schema
|
|
112
|
+
* @param labels - Labels object to validate
|
|
113
|
+
* @throws Error if invalid labels are provided
|
|
114
|
+
*/ validateLabels(labels) {
|
|
115
|
+
if (this.labelNames.length === 0) {
|
|
116
|
+
throw new Error('Gauge was initialized without labels support');
|
|
117
|
+
}
|
|
118
|
+
for (const key of Object.keys(labels)){
|
|
119
|
+
if (!this.labelNames.includes(key)) {
|
|
120
|
+
throw new Error(`Invalid label key: ${key}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Safely parses label string back to object
|
|
126
|
+
* @param labelStr - Stringified labels object
|
|
127
|
+
* @returns Labels object or null if parsing fails
|
|
128
|
+
*/ parseLabelsSafely(labelStr) {
|
|
129
|
+
try {
|
|
130
|
+
return JSON.parse(labelStr);
|
|
131
|
+
} catch {
|
|
132
|
+
this.logger.error(`Failed to parse label string: ${labelStr}`);
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Noop implementation of a Historgram collec
|
|
139
|
+
*/ class NoopOtelHistogram {
|
|
140
|
+
logger;
|
|
141
|
+
constructor(logger, _meter, _name, _help, _buckets = [], _labelNames = []){
|
|
142
|
+
this.logger = logger;
|
|
143
|
+
}
|
|
144
|
+
observe(_valueOrLabels, _value) {}
|
|
145
|
+
startTimer(_labels) {
|
|
146
|
+
return ()=>0;
|
|
147
|
+
}
|
|
148
|
+
reset() {
|
|
149
|
+
// OpenTelemetry histograms cannot be reset, but we implement the interface
|
|
150
|
+
this.logger.warn('OpenTelemetry histograms cannot be reset');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Noop implementation of an AvgMinMax collector
|
|
155
|
+
*/ class NoopOtelAvgMinMax {
|
|
156
|
+
_logger;
|
|
157
|
+
constructor(_logger, _meter, _name, _help, _labelNames = []){
|
|
158
|
+
this._logger = _logger;
|
|
159
|
+
}
|
|
160
|
+
set(_valueOrLabels, _values) {}
|
|
161
|
+
reset() {}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Otel metrics Adapter
|
|
165
|
+
*
|
|
166
|
+
* Maps the PromClient based MetricsRegister from gossipsub and discv5 services to the Otel MetricsRegister
|
|
167
|
+
*/ export class OtelMetricsAdapter extends Registry {
|
|
168
|
+
logger;
|
|
169
|
+
meter;
|
|
170
|
+
constructor(telemetryClient, logger = createLogger('telemetry:otel-metrics-adapter')){
|
|
171
|
+
super(), this.logger = logger;
|
|
172
|
+
this.meter = telemetryClient.getMeter('metrics-adapter');
|
|
173
|
+
}
|
|
174
|
+
gauge(configuration) {
|
|
175
|
+
return new OtelGauge(this.logger, this.meter, configuration.name, configuration.help, configuration.labelNames);
|
|
176
|
+
}
|
|
177
|
+
histogram(configuration) {
|
|
178
|
+
return new NoopOtelHistogram(this.logger, this.meter, configuration.name, configuration.help, configuration.buckets, configuration.labelNames);
|
|
179
|
+
}
|
|
180
|
+
avgMinMax(configuration) {
|
|
181
|
+
return new NoopOtelAvgMinMax(this.logger, this.meter, configuration.name, configuration.help, configuration.labelNames);
|
|
182
|
+
}
|
|
183
|
+
}
|
package/dest/start.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createLogger } from '@aztec/foundation/log';
|
|
2
|
+
import { NoopTelemetryClient } from './noop.js';
|
|
3
|
+
import { OpenTelemetryClient } from './otel.js';
|
|
4
|
+
export * from './config.js';
|
|
5
|
+
let initialised = false;
|
|
6
|
+
let telemetry = new NoopTelemetryClient();
|
|
7
|
+
export function initTelemetryClient(config) {
|
|
8
|
+
const log = createLogger('telemetry:client');
|
|
9
|
+
if (initialised) {
|
|
10
|
+
log.warn('Telemetry client has already been initialized once');
|
|
11
|
+
return telemetry;
|
|
12
|
+
}
|
|
13
|
+
if (config.metricsCollectorUrl || config.useGcloudObservability) {
|
|
14
|
+
log.info(`Using OpenTelemetry client ${config.useGcloudObservability ? 'with GCP' : 'with custom collector'}`);
|
|
15
|
+
telemetry = OpenTelemetryClient.createAndStart(config, log);
|
|
16
|
+
} else {
|
|
17
|
+
log.info('Using NoopTelemetryClient');
|
|
18
|
+
}
|
|
19
|
+
initialised = true;
|
|
20
|
+
return telemetry;
|
|
21
|
+
}
|
|
22
|
+
export function getTelemetryClient() {
|
|
23
|
+
return telemetry;
|
|
24
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { SpanStatusCode } from '@opentelemetry/api';
|
|
2
|
+
import { isPromise } from 'node:util/types';
|
|
3
|
+
import { getTelemetryClient } from './start.js';
|
|
4
|
+
export { SpanStatusCode, ValueType } from '@opentelemetry/api';
|
|
5
|
+
/**
|
|
6
|
+
* Starts a new span whenever the decorated method is called.
|
|
7
|
+
* @param spanName - The name of the span to create. Can be a string or a function that returns a string.
|
|
8
|
+
* @param attributes - Initial attributes to set on the span. If a function is provided, it will be called with the arguments of the method.
|
|
9
|
+
* @param extraAttributes - Extra attributes to set on the span after the method is called. Will be called with the return value of the method. Note: if the function throws then this will not be called.
|
|
10
|
+
* @returns A decorator that wraps the method in a span.
|
|
11
|
+
*
|
|
12
|
+
* @privateRemarks
|
|
13
|
+
* This code looks complex but it's not that difficult:
|
|
14
|
+
* - decorators are functions that _replace_ a method with a different implementation
|
|
15
|
+
* - normal decorators can't take function arguments, but if we write a function that returns a decorator, we can pass arguments to that function
|
|
16
|
+
*
|
|
17
|
+
* The trackSpan function takes a span's name and some attributes and builds a decorator that wraps a method in a span with the given name and props
|
|
18
|
+
* The decorator can currently only be applied to methods on classes that have a `tracer` property. The compiler will enforce this.
|
|
19
|
+
*/ export function trackSpan(spanName, attributes, extraAttributes) {
|
|
20
|
+
// the return value of trackSpan is a decorator
|
|
21
|
+
return (originalMethod, _context)=>{
|
|
22
|
+
// the return value of the decorator replaces the original method
|
|
23
|
+
// in this wrapper method we start a span, call the original method, and then end the span
|
|
24
|
+
return async function replacementMethod(...args) {
|
|
25
|
+
const name = typeof spanName === 'function' ? spanName.call(this, ...args) : spanName;
|
|
26
|
+
const currentAttrs = typeof attributes === 'function' ? await attributes.call(this, ...args) : attributes;
|
|
27
|
+
// run originalMethod wrapped in an active span
|
|
28
|
+
// "active" means the span will be alive for the duration of the function execution
|
|
29
|
+
// and if any other spans are started during the execution of originalMethod, they will be children of this span
|
|
30
|
+
// behind the scenes this uses AsyncLocalStorage https://nodejs.org/dist/latest-v18.x/docs/api/async_context.html
|
|
31
|
+
return this.tracer.startActiveSpan(name, async (span)=>{
|
|
32
|
+
span.setAttributes(currentAttrs ?? {});
|
|
33
|
+
try {
|
|
34
|
+
const res = await originalMethod.call(this, ...args);
|
|
35
|
+
const extraAttrs = extraAttributes?.call(this, res);
|
|
36
|
+
span.setAttributes(extraAttrs ?? {});
|
|
37
|
+
return res;
|
|
38
|
+
} catch (err) {
|
|
39
|
+
span.setStatus({
|
|
40
|
+
code: SpanStatusCode.ERROR,
|
|
41
|
+
message: String(err)
|
|
42
|
+
});
|
|
43
|
+
throw err;
|
|
44
|
+
} finally{
|
|
45
|
+
span.end();
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Runs an event callback in a span. The span is started immediately and completes once the callback finishes running.
|
|
53
|
+
* The span will have two events added: 'callbackStart' and 'callbackEnd' to mark the start and end of the callback.
|
|
54
|
+
*
|
|
55
|
+
* @param tracer - The tracer instance to use
|
|
56
|
+
* @param spanName - The name of the span to create
|
|
57
|
+
* @param attributes - Initial attributes to set on the span
|
|
58
|
+
* @param callback - The callback to wrap in a span
|
|
59
|
+
*
|
|
60
|
+
* @returns - A new function that wraps the callback in a span
|
|
61
|
+
*/ export function wrapCallbackInSpan(tracer, spanName, attributes, callback) {
|
|
62
|
+
const span = tracer.startSpan(spanName, {
|
|
63
|
+
attributes
|
|
64
|
+
});
|
|
65
|
+
return async (...args)=>{
|
|
66
|
+
try {
|
|
67
|
+
span.addEvent('callbackStart');
|
|
68
|
+
const res = await callback(...args);
|
|
69
|
+
return res;
|
|
70
|
+
} catch (err) {
|
|
71
|
+
span.setStatus({
|
|
72
|
+
code: SpanStatusCode.ERROR,
|
|
73
|
+
message: String(err)
|
|
74
|
+
});
|
|
75
|
+
throw err;
|
|
76
|
+
} finally{
|
|
77
|
+
span.addEvent('callbackEnd');
|
|
78
|
+
span.end();
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
export function runInSpan(tracer, spanName, callback) {
|
|
83
|
+
return (...args)=>{
|
|
84
|
+
const actualTracer = typeof tracer === 'string' ? getTelemetryClient().getTracer(tracer) : tracer;
|
|
85
|
+
return actualTracer.startActiveSpan(spanName, (span)=>{
|
|
86
|
+
let deferSpanEnd = false;
|
|
87
|
+
try {
|
|
88
|
+
const res = callback(span, ...args);
|
|
89
|
+
if (isPromise(res)) {
|
|
90
|
+
deferSpanEnd = true;
|
|
91
|
+
return res.catch((err)=>{
|
|
92
|
+
span.setStatus({
|
|
93
|
+
code: SpanStatusCode.ERROR,
|
|
94
|
+
message: String(err)
|
|
95
|
+
});
|
|
96
|
+
throw err;
|
|
97
|
+
}).finally(()=>{
|
|
98
|
+
span.end();
|
|
99
|
+
});
|
|
100
|
+
} else {
|
|
101
|
+
return res;
|
|
102
|
+
}
|
|
103
|
+
} catch (err) {
|
|
104
|
+
span.setStatus({
|
|
105
|
+
code: SpanStatusCode.ERROR,
|
|
106
|
+
message: String(err)
|
|
107
|
+
});
|
|
108
|
+
throw err;
|
|
109
|
+
} finally{
|
|
110
|
+
if (!deferSpanEnd) {
|
|
111
|
+
span.end();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
}
|