@aztec-labs/telemetry-client 6.0.0-nightly.20260829

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.
Files changed (105) hide show
  1. package/dest/attributes.d.ts +143 -0
  2. package/dest/attributes.d.ts.map +1 -0
  3. package/dest/attributes.js +91 -0
  4. package/dest/bench.d.ts +34 -0
  5. package/dest/bench.d.ts.map +1 -0
  6. package/dest/bench.js +115 -0
  7. package/dest/config.d.ts +21 -0
  8. package/dest/config.d.ts.map +1 -0
  9. package/dest/config.js +85 -0
  10. package/dest/index.d.ts +12 -0
  11. package/dest/index.d.ts.map +1 -0
  12. package/dest/index.js +11 -0
  13. package/dest/json_rpc_server_metrics.d.ts +27 -0
  14. package/dest/json_rpc_server_metrics.d.ts.map +1 -0
  15. package/dest/json_rpc_server_metrics.js +154 -0
  16. package/dest/l1_metrics.d.ts +17 -0
  17. package/dest/l1_metrics.d.ts.map +1 -0
  18. package/dest/l1_metrics.js +54 -0
  19. package/dest/lmdb_metrics.d.ts +18 -0
  20. package/dest/lmdb_metrics.d.ts.map +1 -0
  21. package/dest/lmdb_metrics.js +34 -0
  22. package/dest/metric-utils.d.ts +24 -0
  23. package/dest/metric-utils.d.ts.map +1 -0
  24. package/dest/metric-utils.js +59 -0
  25. package/dest/metrics.d.ts +338 -0
  26. package/dest/metrics.d.ts.map +1 -0
  27. package/dest/metrics.js +1702 -0
  28. package/dest/monitored_batch_span_processor.d.ts +33 -0
  29. package/dest/monitored_batch_span_processor.d.ts.map +1 -0
  30. package/dest/monitored_batch_span_processor.js +111 -0
  31. package/dest/nodejs_metrics_monitor.d.ts +19 -0
  32. package/dest/nodejs_metrics_monitor.d.ts.map +1 -0
  33. package/dest/nodejs_metrics_monitor.js +115 -0
  34. package/dest/noop.d.ts +19 -0
  35. package/dest/noop.d.ts.map +1 -0
  36. package/dest/noop.js +104 -0
  37. package/dest/otel.d.ts +43 -0
  38. package/dest/otel.d.ts.map +1 -0
  39. package/dest/otel.js +537 -0
  40. package/dest/otel_filter_metric_exporter.d.ts +20 -0
  41. package/dest/otel_filter_metric_exporter.d.ts.map +1 -0
  42. package/dest/otel_filter_metric_exporter.js +67 -0
  43. package/dest/otel_logger_provider.d.ts +4 -0
  44. package/dest/otel_logger_provider.d.ts.map +1 -0
  45. package/dest/otel_logger_provider.js +25 -0
  46. package/dest/otel_propagation.d.ts +6 -0
  47. package/dest/otel_propagation.d.ts.map +1 -0
  48. package/dest/otel_propagation.js +102 -0
  49. package/dest/otel_resource.d.ts +3 -0
  50. package/dest/otel_resource.d.ts.map +1 -0
  51. package/dest/otel_resource.js +53 -0
  52. package/dest/prom_otel_adapter.d.ts +176 -0
  53. package/dest/prom_otel_adapter.d.ts.map +1 -0
  54. package/dest/prom_otel_adapter.js +292 -0
  55. package/dest/start.d.ts +7 -0
  56. package/dest/start.d.ts.map +1 -0
  57. package/dest/start.js +25 -0
  58. package/dest/telemetry.d.ts +149 -0
  59. package/dest/telemetry.d.ts.map +1 -0
  60. package/dest/telemetry.js +139 -0
  61. package/dest/vendor/attributes.d.ts +6 -0
  62. package/dest/vendor/attributes.d.ts.map +1 -0
  63. package/dest/vendor/attributes.js +6 -0
  64. package/dest/vendor/otel-pino-stream.d.ts +40 -0
  65. package/dest/vendor/otel-pino-stream.d.ts.map +1 -0
  66. package/dest/vendor/otel-pino-stream.js +229 -0
  67. package/dest/with_tracer.d.ts +33 -0
  68. package/dest/with_tracer.d.ts.map +1 -0
  69. package/dest/with_tracer.js +32 -0
  70. package/dest/wrappers/fetch.d.ts +16 -0
  71. package/dest/wrappers/fetch.d.ts.map +1 -0
  72. package/dest/wrappers/fetch.js +42 -0
  73. package/dest/wrappers/index.d.ts +3 -0
  74. package/dest/wrappers/index.d.ts.map +1 -0
  75. package/dest/wrappers/index.js +2 -0
  76. package/dest/wrappers/json_rpc_server.d.ts +4 -0
  77. package/dest/wrappers/json_rpc_server.d.ts.map +1 -0
  78. package/dest/wrappers/json_rpc_server.js +16 -0
  79. package/package.json +95 -0
  80. package/src/attributes.ts +174 -0
  81. package/src/bench.ts +168 -0
  82. package/src/config.ts +135 -0
  83. package/src/index.ts +11 -0
  84. package/src/json_rpc_server_metrics.ts +150 -0
  85. package/src/l1_metrics.ts +64 -0
  86. package/src/lmdb_metrics.ts +45 -0
  87. package/src/metric-utils.ts +75 -0
  88. package/src/metrics.ts +1802 -0
  89. package/src/monitored_batch_span_processor.ts +147 -0
  90. package/src/nodejs_metrics_monitor.ts +135 -0
  91. package/src/noop.ts +152 -0
  92. package/src/otel.ts +482 -0
  93. package/src/otel_filter_metric_exporter.ts +80 -0
  94. package/src/otel_logger_provider.ts +31 -0
  95. package/src/otel_propagation.ts +107 -0
  96. package/src/otel_resource.ts +71 -0
  97. package/src/prom_otel_adapter.ts +450 -0
  98. package/src/start.ts +42 -0
  99. package/src/telemetry.ts +348 -0
  100. package/src/vendor/attributes.ts +6 -0
  101. package/src/vendor/otel-pino-stream.ts +279 -0
  102. package/src/with_tracer.ts +35 -0
  103. package/src/wrappers/fetch.ts +50 -0
  104. package/src/wrappers/index.ts +2 -0
  105. package/src/wrappers/json_rpc_server.ts +26 -0
package/src/otel.ts ADDED
@@ -0,0 +1,482 @@
1
+ import { type LogData, type Logger, addLogDataHandler } from '@aztec-labs/foundation/log';
2
+ import {
3
+ type Context,
4
+ DiagConsoleLogger,
5
+ DiagLogLevel,
6
+ type Meter as OtelMeter,
7
+ ROOT_CONTEXT,
8
+ type Tracer,
9
+ type TracerProvider,
10
+ context,
11
+ diag,
12
+ isSpanContextValid,
13
+ propagation,
14
+ trace,
15
+ } from '@opentelemetry/api';
16
+ import { W3CTraceContextPropagator } from '@opentelemetry/core';
17
+ import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';
18
+ import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
19
+ import { HostMetrics } from '@opentelemetry/host-metrics';
20
+ import type { IResource } from '@opentelemetry/resources';
21
+ import type { LoggerProvider } from '@opentelemetry/sdk-logs';
22
+ import {
23
+ ExplicitBucketHistogramAggregation,
24
+ InstrumentType,
25
+ MeterProvider,
26
+ PeriodicExportingMetricReader,
27
+ type PeriodicExportingMetricReaderOptions,
28
+ View,
29
+ } from '@opentelemetry/sdk-metrics';
30
+ import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
31
+ import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';
32
+
33
+ import type { TelemetryClientConfig } from './config.js';
34
+ import { toMetricOptions } from './metric-utils.js';
35
+ import type { MetricDefinition } from './metrics.js';
36
+ import { MonitoredBatchSpanProcessor } from './monitored_batch_span_processor.js';
37
+ import { NodejsMetricsMonitor } from './nodejs_metrics_monitor.js';
38
+ import { OtelFilterMetricExporter, PublicOtelFilterMetricExporter } from './otel_filter_metric_exporter.js';
39
+ import { registerOtelLoggerProvider } from './otel_logger_provider.js';
40
+ import { getOtelResource } from './otel_resource.js';
41
+ import type {
42
+ Gauge,
43
+ Histogram,
44
+ Meter,
45
+ ObservableGauge,
46
+ ObservableUpDownCounter,
47
+ TelemetryClient,
48
+ UpDownCounter,
49
+ } from './telemetry.js';
50
+
51
+ /** Wraps an OpenTelemetry Meter to implement our custom Meter interface */
52
+ class WrappedMeter implements Meter {
53
+ constructor(private otelMeter: OtelMeter) {}
54
+
55
+ createGauge(metric: MetricDefinition): Gauge {
56
+ return this.otelMeter.createGauge(metric.name, toMetricOptions(metric));
57
+ }
58
+
59
+ createObservableGauge(metric: MetricDefinition): ObservableGauge {
60
+ return this.otelMeter.createObservableGauge(metric.name, toMetricOptions(metric));
61
+ }
62
+
63
+ createHistogram(metric: MetricDefinition, extraOptions?: Parameters<Meter['createHistogram']>[1]): Histogram {
64
+ return this.otelMeter.createHistogram(metric.name, { ...toMetricOptions(metric), ...extraOptions });
65
+ }
66
+
67
+ createUpDownCounter(metric: MetricDefinition): UpDownCounter {
68
+ return this.otelMeter.createUpDownCounter(metric.name, toMetricOptions(metric));
69
+ }
70
+
71
+ createObservableUpDownCounter(metric: MetricDefinition): ObservableUpDownCounter {
72
+ return this.otelMeter.createObservableUpDownCounter(metric.name, toMetricOptions(metric));
73
+ }
74
+
75
+ addBatchObservableCallback(
76
+ callback: Parameters<Meter['addBatchObservableCallback']>[0],
77
+ observables: Parameters<Meter['addBatchObservableCallback']>[1],
78
+ ): void {
79
+ this.otelMeter.addBatchObservableCallback(callback, observables);
80
+ }
81
+
82
+ removeBatchObservableCallback(
83
+ callback: Parameters<Meter['removeBatchObservableCallback']>[0],
84
+ observables: Parameters<Meter['removeBatchObservableCallback']>[1],
85
+ ): void {
86
+ this.otelMeter.removeBatchObservableCallback(callback, observables);
87
+ }
88
+ }
89
+
90
+ export type OpenTelemetryClientFactory = (resource: IResource, log: Logger) => OpenTelemetryClient;
91
+
92
+ export class OpenTelemetryClient implements TelemetryClient {
93
+ hostMetrics: HostMetrics | undefined;
94
+ nodejsMetricsMonitor: NodejsMetricsMonitor | undefined;
95
+ private meters: Map<string, WrappedMeter> = new Map<string, WrappedMeter>();
96
+ private tracers: Map<string, Tracer> = new Map<string, Tracer>();
97
+
98
+ /** Memoized shutdown promise. The telemetry client is shared between the aztec-node and an embedded prover-node,
99
+ * so stop() can be invoked more than once; the providers throw "shutdown may only be called once" and
100
+ * "invalid attempt to force flush after shutdown" if that happens. Guarding here makes stop()/flush() idempotent. */
101
+ private stopPromise: Promise<void> | undefined;
102
+
103
+ protected constructor(
104
+ private resource: IResource,
105
+ private meterProvider: MeterProvider,
106
+ private traceProvider: TracerProvider,
107
+ private loggerProvider: LoggerProvider | undefined,
108
+ private publicMetricExporter: PublicOtelFilterMetricExporter | undefined,
109
+ private log: Logger,
110
+ ) {}
111
+
112
+ setExportedPublicTelemetry(metrics: string[]): void {
113
+ this.publicMetricExporter?.setMetricPrefixes(metrics);
114
+ }
115
+
116
+ setPublicTelemetryCollectFrom(roles: string[]): void {
117
+ this.publicMetricExporter?.setAllowedRoles(roles);
118
+ }
119
+
120
+ getMeter(name: string): Meter {
121
+ let meter = this.meters.get(name);
122
+ if (!meter) {
123
+ const otelMeter = this.meterProvider.getMeter(name, this.resource.attributes[ATTR_SERVICE_VERSION] as string);
124
+ meter = new WrappedMeter(otelMeter);
125
+ this.meters.set(name, meter);
126
+ }
127
+ return meter;
128
+ }
129
+
130
+ getTracer(name: string): Tracer {
131
+ let tracer = this.tracers.get(name);
132
+ if (!tracer) {
133
+ tracer = this.traceProvider.getTracer(name, this.resource.attributes[ATTR_SERVICE_VERSION] as string);
134
+ this.tracers.set(name, tracer);
135
+ }
136
+ return tracer;
137
+ }
138
+
139
+ public start() {
140
+ this.log.info('Starting OpenTelemetry client');
141
+ diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.INFO);
142
+
143
+ // Add a callback to the logger to set context data from current trace
144
+ // Adapted from open-telemetry/opentelemetry-js-contrib PinoInstrumentation._getMixinFunction
145
+ addLogDataHandler((data: LogData) => {
146
+ const spanContext = trace.getSpan(context.active())?.spanContext();
147
+ return spanContext && isSpanContextValid(spanContext)
148
+ ? {
149
+ ...data,
150
+ ['trace_id']: spanContext.traceId,
151
+ ['span_id']: spanContext.spanId,
152
+ ['trace_flags']: `0${spanContext.traceFlags.toString(16)}`,
153
+ }
154
+ : data;
155
+ });
156
+
157
+ this.hostMetrics = new HostMetrics({
158
+ name: this.resource.attributes[ATTR_SERVICE_NAME] as string,
159
+ meterProvider: this.meterProvider,
160
+ });
161
+
162
+ const nodejsMeter = new WrappedMeter(
163
+ this.meterProvider.getMeter(this.resource.attributes[ATTR_SERVICE_NAME] as string),
164
+ );
165
+ this.nodejsMetricsMonitor = new NodejsMetricsMonitor(nodejsMeter);
166
+
167
+ this.hostMetrics.start();
168
+ this.nodejsMetricsMonitor.start();
169
+ }
170
+
171
+ public isEnabled() {
172
+ return true;
173
+ }
174
+
175
+ public async flush() {
176
+ // Flushing after the providers have been shut down throws "invalid attempt to force flush after shutdown".
177
+ if (this.stopPromise) {
178
+ return;
179
+ }
180
+ await Promise.all([
181
+ this.meterProvider.forceFlush(),
182
+ this.loggerProvider?.forceFlush(),
183
+ this.traceProvider instanceof NodeTracerProvider ? this.traceProvider.forceFlush() : Promise.resolve(),
184
+ ]);
185
+ }
186
+
187
+ public stop() {
188
+ return (this.stopPromise ??= this.doStop());
189
+ }
190
+
191
+ private async doStop() {
192
+ this.nodejsMetricsMonitor?.stop();
193
+
194
+ const flushAndShutdown = async (provider?: { forceFlush: () => Promise<void>; shutdown: () => Promise<void> }) => {
195
+ if (!provider) {
196
+ return;
197
+ }
198
+ await provider.forceFlush();
199
+ await provider.shutdown();
200
+ };
201
+
202
+ await Promise.all([
203
+ flushAndShutdown(this.meterProvider),
204
+ flushAndShutdown(this.loggerProvider),
205
+ this.traceProvider instanceof NodeTracerProvider ? flushAndShutdown(this.traceProvider) : Promise.resolve(),
206
+ ]);
207
+ }
208
+
209
+ public getTraceContext(): string | undefined {
210
+ const carrier: Record<string, string> = {};
211
+ propagation.inject(context.active(), carrier);
212
+ return carrier['traceparent'];
213
+ }
214
+
215
+ public extractPropagatedContext(traceContext: string): Context {
216
+ const extractedContext = propagation.extract(ROOT_CONTEXT, {
217
+ traceparent: traceContext,
218
+ });
219
+ return extractedContext;
220
+ }
221
+
222
+ public static createMeterProvider(
223
+ resource: IResource,
224
+ exporters: Array<PeriodicExportingMetricReaderOptions>,
225
+ ): MeterProvider {
226
+ return new MeterProvider({
227
+ resource,
228
+ readers: exporters.map(options => new PeriodicExportingMetricReader(options)),
229
+
230
+ views: [
231
+ // Every histogram matching the selector (type + unit) gets these custom buckets assigned
232
+ new View({
233
+ instrumentType: InstrumentType.HISTOGRAM,
234
+ instrumentUnit: 'Mmana',
235
+ aggregation: new ExplicitBucketHistogramAggregation(
236
+ [0.1, 0.5, 1, 2, 4, 8, 10, 25, 50, 100, 500, 1000, 5000, 10000],
237
+ true,
238
+ ),
239
+ }),
240
+ new View({
241
+ instrumentType: InstrumentType.HISTOGRAM,
242
+ instrumentUnit: 'tx',
243
+ aggregation: new ExplicitBucketHistogramAggregation(
244
+ // TPS
245
+ [0.1 * 36, 0.2 * 36, 0.5 * 36, 1 * 36, 2 * 36, 5 * 36, 10 * 36, 15 * 36].map(Math.ceil),
246
+ true,
247
+ ),
248
+ }),
249
+ new View({
250
+ instrumentType: InstrumentType.HISTOGRAM,
251
+ instrumentUnit: 's',
252
+ aggregation: new ExplicitBucketHistogramAggregation(
253
+ [1, 2, 4, 6, 10, 15, 30, 60, 90, 120, 180, 240, 300, 480, 600, 900, 1200],
254
+ true,
255
+ ),
256
+ }),
257
+ // Pending-to-mined delay routinely exceeds the 1-minute ceiling of the generic `ms`
258
+ // view below under load, so it would saturate at 60s. Give this one metric wider
259
+ // buckets (1s to 10min). This must precede the generic `ms` view: when multiple views
260
+ // match an instrument, the SDK keeps the first-registered compatible storage, so the
261
+ // first view in this list wins the bucket boundaries.
262
+ new View({
263
+ instrumentType: InstrumentType.HISTOGRAM,
264
+ instrumentName: 'aztec.mempool.tx_mined_delay',
265
+ instrumentUnit: 'ms',
266
+ aggregation: new ExplicitBucketHistogramAggregation(
267
+ [
268
+ 1_000, 2_500, 5_000, 7_500, 10_000, 15_000, 30_000, 45_000, 60_000, 90_000, 120_000, 180_000, 300_000,
269
+ 600_000,
270
+ ],
271
+ true,
272
+ ),
273
+ }),
274
+ new View({
275
+ instrumentType: InstrumentType.HISTOGRAM,
276
+ instrumentUnit: 'ms',
277
+ aggregation: new ExplicitBucketHistogramAggregation(
278
+ // 10ms to 1 minute
279
+ [10, 20, 35, 50, 75, 100, 250, 500, 750, 1_000, 2_500, 5_000, 7_500, 10_000, 15_000, 30_000, 60_000],
280
+ true,
281
+ ),
282
+ }),
283
+ new View({
284
+ instrumentType: InstrumentType.HISTOGRAM,
285
+ instrumentUnit: 'us',
286
+ aggregation: new ExplicitBucketHistogramAggregation(
287
+ // 1us to 1s
288
+ [
289
+ 5, 10, 25, 50, 75, 100, 250, 500, 750, 1_000, 2_500, 5_000, 7_500, 10_000, 25_000, 50_000, 100_000,
290
+ 250_000, 500_000, 1_000_000,
291
+ ],
292
+ true,
293
+ ),
294
+ }),
295
+ new View({
296
+ instrumentType: InstrumentType.HISTOGRAM,
297
+ instrumentUnit: 'By',
298
+ aggregation: new ExplicitBucketHistogramAggregation(
299
+ // from 32 bytes to 2MB
300
+ [
301
+ 32,
302
+ 64,
303
+ 128,
304
+ 256,
305
+ 512,
306
+ 1024, // 1kb
307
+ 2048,
308
+ 4096,
309
+ 8192,
310
+ 16384,
311
+ 32768,
312
+ 65536,
313
+ 131072,
314
+ 262144,
315
+ 524288,
316
+ 1048576, // 1mb
317
+ 1572864,
318
+ 2097152, // 2mb
319
+ ],
320
+ true,
321
+ ),
322
+ }),
323
+ new View({
324
+ instrumentType: InstrumentType.HISTOGRAM,
325
+ instrumentUnit: 'gas/s',
326
+ aggregation: new ExplicitBucketHistogramAggregation(
327
+ [
328
+ 1_000, 5_000, 10_000, 25_000, 50_000, 100_000, 250_000, 500_000, 750_000, 1_000_000, 2_000_000, 4_000_000,
329
+ 8_000_000, 10_000_000, 15_000_000, 30_000_000,
330
+ ],
331
+ true,
332
+ ),
333
+ }),
334
+ new View({
335
+ instrumentType: InstrumentType.HISTOGRAM,
336
+ instrumentUnit: 'mana/s',
337
+ aggregation: new ExplicitBucketHistogramAggregation(
338
+ [
339
+ 1_000, 5_000, 10_000, 25_000, 50_000, 100_000, 250_000, 500_000, 750_000, 1_000_000, 2_000_000, 4_000_000,
340
+ 8_000_000, 10_000_000, 15_000_000, 30_000_000,
341
+ ],
342
+ true,
343
+ ),
344
+ }),
345
+ new View({
346
+ instrumentType: InstrumentType.HISTOGRAM,
347
+ instrumentUnit: 'gas/block',
348
+ aggregation: new ExplicitBucketHistogramAggregation(
349
+ [
350
+ 1_000, 5_000, 10_000, 25_000, 50_000, 100_000, 250_000, 500_000, 750_000, 1_000_000, 2_000_000, 4_000_000,
351
+ 8_000_000, 10_000_000, 15_000_000, 30_000_000,
352
+ ],
353
+ true,
354
+ ),
355
+ }),
356
+ new View({
357
+ instrumentType: InstrumentType.HISTOGRAM,
358
+ instrumentUnit: 'gas/tx',
359
+ aggregation: new ExplicitBucketHistogramAggregation(
360
+ [
361
+ 25_000, 50_000, 100_000, 250_000, 500_000, 750_000, 1_000_000, 2_000_000, 4_000_000, 8_000_000,
362
+ 10_000_000, 15_000_000, 30_000_000,
363
+ ],
364
+ true,
365
+ ),
366
+ }),
367
+ // L1 gas prices in gwei: priority fees ~0.01-10, base fees ~1-500, spikes to 1000+
368
+ new View({
369
+ instrumentType: InstrumentType.HISTOGRAM,
370
+ instrumentUnit: 'gwei',
371
+ aggregation: new ExplicitBucketHistogramAggregation(
372
+ [0.1, 0.5, 1, 2, 5, 10, 20, 50, 100, 200, 500, 1_000],
373
+ true,
374
+ ),
375
+ }),
376
+ // L1 gas consumption: tx gas 100k-30M, calldata/blob gas varies
377
+ new View({
378
+ instrumentType: InstrumentType.HISTOGRAM,
379
+ instrumentUnit: 'gas',
380
+ aggregation: new ExplicitBucketHistogramAggregation(
381
+ [
382
+ 10_000, 50_000, 100_000, 250_000, 500_000, 1_000_000, 2_000_000, 5_000_000, 10_000_000, 15_000_000,
383
+ 30_000_000,
384
+ ],
385
+ true,
386
+ ),
387
+ }),
388
+ // L1 tx total fee in ETH: typically 0.001 - 1 ETH
389
+ new View({
390
+ instrumentType: InstrumentType.HISTOGRAM,
391
+ instrumentUnit: 'eth',
392
+ aggregation: new ExplicitBucketHistogramAggregation(
393
+ [0.0001, 0.0005, 0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 5, 10],
394
+ true,
395
+ ),
396
+ }),
397
+ ],
398
+ });
399
+ }
400
+
401
+ private static getCustomClientFactory(config: TelemetryClientConfig): OpenTelemetryClientFactory {
402
+ return (resource: IResource, log: Logger) => {
403
+ const tracerProvider = new NodeTracerProvider({
404
+ resource,
405
+ spanProcessors: config.tracesCollectorUrl
406
+ ? [
407
+ new MonitoredBatchSpanProcessor(new OTLPTraceExporter({ url: config.tracesCollectorUrl.href }), log, {
408
+ maxQueueSize: config.otelBspMaxQueueSize,
409
+ maxExportBatchSize: config.otelBspMaxExportBatchSize,
410
+ scheduledDelayMillis: config.otelBspScheduleDelayMs,
411
+ minTraceDurationMs: config.otelMinTraceDurationMs,
412
+ }),
413
+ ]
414
+ : [],
415
+ });
416
+
417
+ tracerProvider.register({
418
+ propagator: new W3CTraceContextPropagator(),
419
+ });
420
+
421
+ const exporters: PeriodicExportingMetricReaderOptions[] = [];
422
+ if (config.metricsCollectorUrl) {
423
+ // Default to a blacklist that is empty (allow all metrics)
424
+ let filter: string[] = [];
425
+ let mode: 'allow' | 'deny' = 'deny';
426
+ if (config.otelExcludeMetrics.length > 0) {
427
+ // Implement a blacklist as specified in config
428
+ log.info(`Excluding metrics from export: ${config.otelExcludeMetrics}`);
429
+ filter = config.otelExcludeMetrics;
430
+ mode = 'deny';
431
+ } else if (config.otelIncludeMetrics.length > 0) {
432
+ // Implement a whitelist as specified in config
433
+ log.info(`Including only specified metrics for export: ${config.otelIncludeMetrics}`);
434
+ filter = config.otelIncludeMetrics;
435
+ mode = 'allow';
436
+ }
437
+ exporters.push({
438
+ exporter: new OtelFilterMetricExporter(
439
+ new OTLPMetricExporter({ url: config.metricsCollectorUrl.href }),
440
+ filter,
441
+ mode,
442
+ ),
443
+ exportTimeoutMillis: config.otelExportTimeoutMs,
444
+ exportIntervalMillis: config.otelCollectIntervalMs,
445
+ });
446
+ }
447
+
448
+ let publicExporter: PublicOtelFilterMetricExporter | undefined;
449
+ if (config.publicMetricsCollectorUrl && !config.publicMetricsOptOut) {
450
+ log.info(`Exporting public metrics: ${config.publicIncludeMetrics}`, {
451
+ publicMetrics: config.publicIncludeMetrics,
452
+ collectorUrl: config.publicMetricsCollectorUrl,
453
+ });
454
+ publicExporter = new PublicOtelFilterMetricExporter(
455
+ config.publicMetricsCollectFrom,
456
+ new OTLPMetricExporter({ url: config.publicMetricsCollectorUrl.href }),
457
+ config.publicIncludeMetrics,
458
+ );
459
+ exporters.push({
460
+ exporter: publicExporter,
461
+ exportTimeoutMillis: config.otelExportTimeoutMs,
462
+ exportIntervalMillis: config.otelCollectIntervalMs,
463
+ });
464
+ }
465
+
466
+ const meterProvider = OpenTelemetryClient.createMeterProvider(resource, exporters);
467
+ const loggerProvider = registerOtelLoggerProvider(resource, config.logsCollectorUrl);
468
+
469
+ return new OpenTelemetryClient(resource, meterProvider, tracerProvider, loggerProvider, publicExporter, log);
470
+ };
471
+ }
472
+
473
+ public static createAndStart(config: TelemetryClientConfig, log: Logger): OpenTelemetryClient {
474
+ const resource = getOtelResource();
475
+ const factory = OpenTelemetryClient.getCustomClientFactory(config);
476
+
477
+ const service = factory(resource, log);
478
+ service.start();
479
+
480
+ return service;
481
+ }
482
+ }
@@ -0,0 +1,80 @@
1
+ import { type ExportResult, ExportResultCode } from '@opentelemetry/core';
2
+ import type { MetricData, PushMetricExporter, ResourceMetrics } from '@opentelemetry/sdk-metrics';
3
+
4
+ import { AZTEC_NODE_ROLE } from './attributes.js';
5
+
6
+ export class OtelFilterMetricExporter implements PushMetricExporter {
7
+ constructor(
8
+ private readonly exporter: PushMetricExporter,
9
+ private metricPrefix: string[],
10
+ private readonly filter: 'allow' | 'deny' = 'deny',
11
+ ) {
12
+ if (exporter.selectAggregation) {
13
+ (this as PushMetricExporter).selectAggregation = exporter.selectAggregation.bind(exporter);
14
+ }
15
+ if (exporter.selectAggregationTemporality) {
16
+ (this as PushMetricExporter).selectAggregationTemporality = exporter.selectAggregationTemporality.bind(exporter);
17
+ }
18
+ }
19
+
20
+ public export(metrics: ResourceMetrics, resultCallback: (result: ExportResult) => void): void {
21
+ const filteredMetrics: ResourceMetrics = {
22
+ resource: metrics.resource,
23
+ scopeMetrics: metrics.scopeMetrics
24
+ .map(({ scope, metrics }) => ({ scope, metrics: this.filterMetrics(metrics) }))
25
+ .filter(({ metrics }) => metrics.length > 0),
26
+ };
27
+
28
+ this.exporter.export(filteredMetrics, resultCallback);
29
+ }
30
+
31
+ private filterMetrics(metrics: MetricData[]): MetricData[] {
32
+ return metrics.filter(metric => {
33
+ const matched = this.metricPrefix.some(prefix => metric.descriptor.name.startsWith(prefix));
34
+
35
+ if (this.filter === 'deny') {
36
+ return !matched;
37
+ }
38
+
39
+ if (this.filter === 'allow') {
40
+ return matched;
41
+ }
42
+ });
43
+ }
44
+
45
+ public forceFlush(): Promise<void> {
46
+ return this.exporter.forceFlush();
47
+ }
48
+
49
+ public shutdown(): Promise<void> {
50
+ return this.exporter.shutdown();
51
+ }
52
+
53
+ public setMetricPrefixes(metrics: string[]) {
54
+ this.metricPrefix = metrics;
55
+ }
56
+ }
57
+
58
+ export class PublicOtelFilterMetricExporter extends OtelFilterMetricExporter {
59
+ constructor(
60
+ private allowedRoles: string[],
61
+ exporter: PushMetricExporter,
62
+ metricPrefix: string[],
63
+ ) {
64
+ super(exporter, metricPrefix, 'allow');
65
+ }
66
+
67
+ public override export(metrics: ResourceMetrics, resultCallback: (result: ExportResult) => void): void {
68
+ const role = String(metrics.resource.attributes[AZTEC_NODE_ROLE] ?? '');
69
+ if (!role || !this.allowedRoles.includes(role)) {
70
+ // noop
71
+ return resultCallback({ code: ExportResultCode.SUCCESS });
72
+ }
73
+
74
+ super.export(metrics, resultCallback);
75
+ }
76
+
77
+ public setAllowedRoles(roles: string[]) {
78
+ this.allowedRoles = roles;
79
+ }
80
+ }
@@ -0,0 +1,31 @@
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 type { IResource } from '@opentelemetry/resources';
5
+ import { BatchLogRecordProcessor, LoggerProvider } from '@opentelemetry/sdk-logs';
6
+
7
+ export function registerOtelLoggerProvider(resource: IResource, otelLogsUrl?: URL) {
8
+ const loggerProvider = new LoggerProvider({ resource });
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(
19
+ new BatchLogRecordProcessor(logExporter, {
20
+ /** The maximum batch size of every export. It must be smaller or equal to
21
+ * maxQueueSize. */
22
+ maxExportBatchSize: 1024,
23
+ /** The maximum queue size. After the size is reached log records are dropped. */
24
+ maxQueueSize: 4096,
25
+ }),
26
+ );
27
+
28
+ otelLogs.setGlobalLoggerProvider(loggerProvider);
29
+
30
+ return loggerProvider;
31
+ }
@@ -0,0 +1,107 @@
1
+ import type { DiagnosticsMiddleware } from '@aztec-labs/foundation/json-rpc/server';
2
+ import { Timer } from '@aztec-labs/foundation/timer';
3
+ import { ROOT_CONTEXT, type Span, SpanKind, SpanStatusCode, propagation } from '@opentelemetry/api';
4
+ import type Koa from 'koa';
5
+
6
+ import { getJsonRpcServerMetrics, splitJsonRpcMethod } from './json_rpc_server_metrics.js';
7
+ import { getTelemetryClient } from './start.js';
8
+ import {
9
+ ATTR_JSONRPC_ERROR_CODE,
10
+ ATTR_JSONRPC_ERROR_MSG,
11
+ ATTR_JSONRPC_METHOD,
12
+ ATTR_JSONRPC_REQUEST_ID,
13
+ ATTR_JSONRPC_SERVICE,
14
+ } from './vendor/attributes.js';
15
+
16
+ export function getOtelJsonRpcPropagationMiddleware(
17
+ scope = 'JsonRpcServer',
18
+ ): (ctx: Koa.Context, next: () => Promise<void>) => Promise<void> {
19
+ return function otelJsonRpcPropagation(ctx: Koa.Context, next: () => Promise<void>) {
20
+ const tracer = getTelemetryClient().getTracer(scope);
21
+ const context = propagation.extract(ROOT_CONTEXT, ctx.request.headers);
22
+ return tracer.startActiveSpan(
23
+ `JsonRpcServer`,
24
+ { kind: SpanKind.SERVER },
25
+ context,
26
+ async (span: Span): Promise<void> => {
27
+ if (ctx.id) {
28
+ span.setAttribute(ATTR_JSONRPC_REQUEST_ID, ctx.id);
29
+ }
30
+
31
+ try {
32
+ await next();
33
+ const requestBody = (ctx.request as { body?: unknown }).body;
34
+ if (
35
+ requestBody &&
36
+ typeof requestBody === 'object' &&
37
+ !Array.isArray(requestBody) &&
38
+ 'method' in requestBody
39
+ ) {
40
+ const fullMethod = requestBody.method;
41
+ if (typeof fullMethod === 'string') {
42
+ const [service, method] = splitJsonRpcMethod(fullMethod);
43
+ span.updateName(`JsonRpcServer.${service ? `${service}.` : ''}${method}`);
44
+ span.setAttribute(ATTR_JSONRPC_METHOD, method);
45
+ if (service) {
46
+ span.setAttribute(ATTR_JSONRPC_SERVICE, service);
47
+ }
48
+ }
49
+ } else if (Array.isArray(requestBody)) {
50
+ span.updateName(`JsonRpcServer.batch`);
51
+ }
52
+ const err = (ctx.body as any).error?.message;
53
+ const code = (ctx.body as any).error?.code;
54
+ if (err) {
55
+ span.setStatus({ code: SpanStatusCode.ERROR, message: err });
56
+ span.setAttribute(ATTR_JSONRPC_ERROR_CODE, code);
57
+ span.setAttribute(ATTR_JSONRPC_ERROR_MSG, err);
58
+ } else {
59
+ span.setStatus({ code: SpanStatusCode.OK });
60
+ }
61
+ } catch (err) {
62
+ span.setStatus({ code: SpanStatusCode.ERROR, message: String(err) });
63
+ } finally {
64
+ span.end();
65
+ }
66
+ },
67
+ );
68
+ };
69
+ }
70
+
71
+ export function getOtelJsonRpcDiagnosticsMiddleware(metricsProvider = getJsonRpcServerMetrics): DiagnosticsMiddleware {
72
+ return function otelJsonRpcDiagnostics(ctx, next) {
73
+ const [service, method] = splitJsonRpcMethod(ctx.method);
74
+ const scope = service ?? 'UnknownHandler';
75
+ const tracer = getTelemetryClient().getTracer(scope);
76
+ const attributes = {
77
+ ...(service === undefined ? {} : { [ATTR_JSONRPC_SERVICE]: service }),
78
+ [ATTR_JSONRPC_METHOD]: method,
79
+ };
80
+ return tracer.startActiveSpan(`${scope}.${method}`, { kind: SpanKind.INTERNAL, attributes }, async span => {
81
+ const timer = new Timer();
82
+ let ok = false;
83
+ if (ctx.id !== null) {
84
+ span.setAttribute(ATTR_JSONRPC_REQUEST_ID, ctx.id);
85
+ }
86
+
87
+ try {
88
+ await next();
89
+ ok = true;
90
+ span.setStatus({ code: SpanStatusCode.OK });
91
+ } catch (err) {
92
+ span.setStatus({ code: SpanStatusCode.ERROR, message: err instanceof Error ? err.message : String(err) });
93
+ if (typeof err === 'string' || err instanceof Error) {
94
+ span.recordException(err);
95
+ }
96
+ throw err;
97
+ } finally {
98
+ const metrics = metricsProvider();
99
+ metrics.recordRequest(ctx.method, timer.ms(), ok);
100
+ if (ctx.requestValidationDurationMs !== undefined && ctx.requestValidationSucceeded !== undefined) {
101
+ metrics.recordRequestValidation(ctx.method, ctx.requestValidationDurationMs, ctx.requestValidationSucceeded);
102
+ }
103
+ span.end();
104
+ }
105
+ });
106
+ };
107
+ }