@aztec/telemetry-client 0.0.1-commit.b655e406 → 0.0.1-commit.b8a057fa

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 (87) hide show
  1. package/dest/attributes.d.ts +28 -2
  2. package/dest/attributes.d.ts.map +1 -1
  3. package/dest/attributes.js +14 -1
  4. package/dest/bench.d.ts +3 -1
  5. package/dest/bench.d.ts.map +1 -1
  6. package/dest/bench.js +24 -14
  7. package/dest/config.d.ts +4 -1
  8. package/dest/config.d.ts.map +1 -1
  9. package/dest/config.js +24 -10
  10. package/dest/index.d.ts +1 -1
  11. package/dest/l1_metrics.d.ts +2 -2
  12. package/dest/l1_metrics.d.ts.map +1 -1
  13. package/dest/l1_metrics.js +4 -20
  14. package/dest/lmdb_metrics.d.ts +3 -3
  15. package/dest/lmdb_metrics.d.ts.map +1 -1
  16. package/dest/lmdb_metrics.js +4 -20
  17. package/dest/metric-utils.d.ts +24 -0
  18. package/dest/metric-utils.d.ts.map +1 -0
  19. package/dest/metric-utils.js +59 -0
  20. package/dest/metrics.d.ts +314 -196
  21. package/dest/metrics.d.ts.map +1 -1
  22. package/dest/metrics.js +1607 -194
  23. package/dest/monitored_batch_span_processor.d.ts +29 -0
  24. package/dest/monitored_batch_span_processor.d.ts.map +1 -0
  25. package/dest/monitored_batch_span_processor.js +75 -0
  26. package/dest/nodejs_metrics_monitor.d.ts +3 -5
  27. package/dest/nodejs_metrics_monitor.d.ts.map +1 -1
  28. package/dest/nodejs_metrics_monitor.js +19 -40
  29. package/dest/noop.d.ts +6 -3
  30. package/dest/noop.d.ts.map +1 -1
  31. package/dest/noop.js +32 -1
  32. package/dest/otel.d.ts +10 -3
  33. package/dest/otel.d.ts.map +1 -1
  34. package/dest/otel.js +165 -9
  35. package/dest/otel_filter_metric_exporter.d.ts +1 -1
  36. package/dest/otel_filter_metric_exporter.d.ts.map +1 -1
  37. package/dest/otel_logger_provider.d.ts +1 -1
  38. package/dest/otel_propagation.d.ts +3 -1
  39. package/dest/otel_propagation.d.ts.map +1 -1
  40. package/dest/otel_propagation.js +49 -1
  41. package/dest/otel_resource.d.ts +1 -1
  42. package/dest/otel_resource.d.ts.map +1 -1
  43. package/dest/otel_resource.js +14 -2
  44. package/dest/prom_otel_adapter.d.ts +4 -4
  45. package/dest/prom_otel_adapter.d.ts.map +1 -1
  46. package/dest/prom_otel_adapter.js +19 -10
  47. package/dest/start.d.ts +3 -2
  48. package/dest/start.d.ts.map +1 -1
  49. package/dest/start.js +5 -4
  50. package/dest/telemetry.d.ts +38 -26
  51. package/dest/telemetry.d.ts.map +1 -1
  52. package/dest/telemetry.js +46 -23
  53. package/dest/vendor/attributes.d.ts +1 -1
  54. package/dest/vendor/otel-pino-stream.d.ts +1 -1
  55. package/dest/vendor/otel-pino-stream.d.ts.map +1 -1
  56. package/dest/with_tracer.d.ts +1 -1
  57. package/dest/with_tracer.d.ts.map +1 -1
  58. package/dest/wrappers/fetch.d.ts +3 -3
  59. package/dest/wrappers/fetch.d.ts.map +1 -1
  60. package/dest/wrappers/fetch.js +3 -2
  61. package/dest/wrappers/index.d.ts +1 -2
  62. package/dest/wrappers/index.d.ts.map +1 -1
  63. package/dest/wrappers/index.js +0 -1
  64. package/dest/wrappers/json_rpc_server.d.ts +1 -1
  65. package/package.json +9 -6
  66. package/src/attributes.ts +36 -1
  67. package/src/bench.ts +27 -15
  68. package/src/config.ts +38 -10
  69. package/src/l1_metrics.ts +5 -20
  70. package/src/lmdb_metrics.ts +5 -26
  71. package/src/metric-utils.ts +75 -0
  72. package/src/metrics.ts +1692 -237
  73. package/src/monitored_batch_span_processor.ts +100 -0
  74. package/src/nodejs_metrics_monitor.ts +18 -51
  75. package/src/noop.ts +61 -3
  76. package/src/otel.ts +161 -11
  77. package/src/otel_propagation.ts +42 -1
  78. package/src/otel_resource.ts +19 -2
  79. package/src/prom_otel_adapter.ts +16 -23
  80. package/src/start.ts +14 -5
  81. package/src/telemetry.ts +105 -67
  82. package/src/wrappers/fetch.ts +9 -3
  83. package/src/wrappers/index.ts +0 -1
  84. package/dest/wrappers/l2_block_stream.d.ts +0 -15
  85. package/dest/wrappers/l2_block_stream.d.ts.map +0 -1
  86. package/dest/wrappers/l2_block_stream.js +0 -26
  87. package/src/wrappers/l2_block_stream.ts +0 -37
@@ -0,0 +1,100 @@
1
+ import type { Logger } from '@aztec/foundation/log';
2
+
3
+ import { type Context, SpanStatusCode } from '@opentelemetry/api';
4
+ import { hrTimeToMilliseconds } from '@opentelemetry/core';
5
+ import type { SpanExporter } from '@opentelemetry/sdk-trace-base';
6
+ import { BatchSpanProcessor, type BufferConfig, type ReadableSpan, type Span } from '@opentelemetry/sdk-trace-node';
7
+
8
+ /** Minimum interval between drop warnings to avoid log spam. */
9
+ const DROP_WARNING_INTERVAL_MS = 30_000;
10
+
11
+ const DEFAULT_MIN_TRACE_DURATION_MS = 10;
12
+
13
+ const DEFAULT_MAX_QUEUE_SIZE = 16384;
14
+
15
+ /** Cap on the per-export batch size, so a large queue can actually be drained instead of dribbling out
16
+ * at the SDK default of 512 spans per scheduled export. Kept <= maxQueueSize per the BatchSpanProcessor contract. */
17
+ const DEFAULT_MAX_EXPORT_BATCH_SIZE = 2048;
18
+
19
+ export type MonitoredBatchSpanProcessorConfig = BufferConfig & {
20
+ minTraceDurationMs?: number;
21
+ };
22
+
23
+ /**
24
+ * Wraps BatchSpanProcessor to emit warnings when spans are dropped due to a full queue.
25
+ * The standard BatchSpanProcessor silently discards spans when its internal queue reaches
26
+ * maxQueueSize, making telemetry data loss invisible to operators.
27
+ */
28
+ export class MonitoredBatchSpanProcessor extends BatchSpanProcessor {
29
+ private readonly maxQueueSize: number;
30
+ private readonly minTraceDurationMs: number;
31
+ private readonly log: Logger;
32
+
33
+ private approxQueueSize = 0;
34
+ private droppedSinceLastWarning = 0;
35
+ private totalDropped = 0;
36
+ private lastWarningTime = 0;
37
+
38
+ constructor(exporter: SpanExporter, log: Logger, config?: MonitoredBatchSpanProcessorConfig) {
39
+ const maxQueueSize = config?.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE;
40
+ const maxExportBatchSize = Math.min(config?.maxExportBatchSize ?? DEFAULT_MAX_EXPORT_BATCH_SIZE, maxQueueSize);
41
+ super(exporter, { ...config, maxQueueSize, maxExportBatchSize });
42
+ this.maxQueueSize = maxQueueSize;
43
+ this.minTraceDurationMs = Math.max(0, config?.minTraceDurationMs ?? DEFAULT_MIN_TRACE_DURATION_MS);
44
+ this.log = log;
45
+ }
46
+
47
+ override onStart(span: Span, parentContext: Context): void {
48
+ super.onStart(span, parentContext);
49
+ }
50
+
51
+ override onEnd(span: ReadableSpan): void {
52
+ if (this.shouldDropShortSpan(span)) {
53
+ return;
54
+ }
55
+
56
+ if (this.approxQueueSize >= this.maxQueueSize) {
57
+ this.droppedSinceLastWarning++;
58
+ this.totalDropped++;
59
+ this.maybeLogDropWarning();
60
+ } else {
61
+ this.approxQueueSize++;
62
+ }
63
+ super.onEnd(span);
64
+ }
65
+
66
+ override async forceFlush(): Promise<void> {
67
+ await super.forceFlush();
68
+ this.approxQueueSize = 0;
69
+ }
70
+
71
+ override async shutdown(): Promise<void> {
72
+ if (this.totalDropped > 0) {
73
+ this.log.warn(`BatchSpanProcessor shutting down with ${this.totalDropped} total spans dropped`, {
74
+ totalDropped: this.totalDropped,
75
+ });
76
+ }
77
+ await super.shutdown();
78
+ }
79
+
80
+ private shouldDropShortSpan(span: ReadableSpan): boolean {
81
+ return (
82
+ this.minTraceDurationMs > 0 &&
83
+ span.status.code !== SpanStatusCode.ERROR &&
84
+ hrTimeToMilliseconds(span.duration) < this.minTraceDurationMs
85
+ );
86
+ }
87
+
88
+ private maybeLogDropWarning(): void {
89
+ const now = Date.now();
90
+ if (now - this.lastWarningTime >= DROP_WARNING_INTERVAL_MS) {
91
+ this.log.warn(
92
+ `BatchSpanProcessor dropping spans: queue full (maxQueueSize=${this.maxQueueSize}). ` +
93
+ `${this.droppedSinceLastWarning} dropped since last warning, ${this.totalDropped} total.`,
94
+ { droppedSinceLastWarning: this.droppedSinceLastWarning, totalDropped: this.totalDropped },
95
+ );
96
+ this.droppedSinceLastWarning = 0;
97
+ this.lastWarningTime = now;
98
+ }
99
+ }
100
+ }
@@ -2,18 +2,11 @@ import type { Observable } from '@opentelemetry/api';
2
2
  import { type EventLoopUtilization, type IntervalHistogram, monitorEventLoopDelay, performance } from 'node:perf_hooks';
3
3
 
4
4
  import * as Attributes from './attributes.js';
5
+ import { createUpDownCounterWithDefault } from './metric-utils.js';
5
6
  import * as Metrics from './metrics.js';
6
- import {
7
- type BatchObservableResult,
8
- type Meter,
9
- type ObservableGauge,
10
- type UpDownCounter,
11
- ValueType,
12
- } from './telemetry.js';
13
-
14
- /**
15
- * Detector for custom Aztec attributes
16
- */
7
+ import type { BatchObservableResult, Meter, ObservableGauge, UpDownCounter } from './telemetry.js';
8
+
9
+ /** Monitors Node.js runtime metrics */
17
10
  export class NodejsMetricsMonitor {
18
11
  private eventLoopDelayGauges: {
19
12
  min: ObservableGauge;
@@ -38,53 +31,27 @@ export class NodejsMetricsMonitor {
38
31
  private eventLoopDelay: IntervalHistogram;
39
32
 
40
33
  constructor(private meter: Meter) {
41
- const nsObsGauge = (name: (typeof Metrics)[keyof typeof Metrics], description: string) =>
42
- meter.createObservableGauge(name, {
43
- unit: 'ns',
44
- valueType: ValueType.INT,
45
- description,
46
- });
47
-
48
34
  this.eventLoopDelayGauges = {
49
- min: nsObsGauge(Metrics.NODEJS_EVENT_LOOP_DELAY_MIN, 'Minimum delay of the event loop'),
50
- mean: nsObsGauge(Metrics.NODEJS_EVENT_LOOP_DELAY_MEAN, 'Mean delay of the event loop'),
51
- max: nsObsGauge(Metrics.NODEJS_EVENT_LOOP_DELAY_MAX, 'Max delay of the event loop'),
52
- stddev: nsObsGauge(Metrics.NODEJS_EVENT_LOOP_DELAY_STDDEV, 'Stddev delay of the event loop'),
53
- p50: nsObsGauge(Metrics.NODEJS_EVENT_LOOP_DELAY_P50, 'P50 delay of the event loop'),
54
- p90: nsObsGauge(Metrics.NODEJS_EVENT_LOOP_DELAY_P90, 'P90 delay of the event loop'),
55
- p99: nsObsGauge(Metrics.NODEJS_EVENT_LOOP_DELAY_P99, 'P99 delay of the event loop'),
35
+ min: meter.createObservableGauge(Metrics.NODEJS_EVENT_LOOP_DELAY_MIN),
36
+ mean: meter.createObservableGauge(Metrics.NODEJS_EVENT_LOOP_DELAY_MEAN),
37
+ max: meter.createObservableGauge(Metrics.NODEJS_EVENT_LOOP_DELAY_MAX),
38
+ stddev: meter.createObservableGauge(Metrics.NODEJS_EVENT_LOOP_DELAY_STDDEV),
39
+ p50: meter.createObservableGauge(Metrics.NODEJS_EVENT_LOOP_DELAY_P50),
40
+ p90: meter.createObservableGauge(Metrics.NODEJS_EVENT_LOOP_DELAY_P90),
41
+ p99: meter.createObservableGauge(Metrics.NODEJS_EVENT_LOOP_DELAY_P99),
56
42
  };
57
43
 
58
- this.eventLoopUilization = meter.createObservableGauge(Metrics.NODEJS_EVENT_LOOP_UTILIZATION, {
59
- valueType: ValueType.DOUBLE,
60
- description: 'How busy is the event loop',
44
+ this.eventLoopUilization = meter.createObservableGauge(Metrics.NODEJS_EVENT_LOOP_UTILIZATION);
45
+ this.eventLoopTime = createUpDownCounterWithDefault(meter, Metrics.NODEJS_EVENT_LOOP_TIME, {
46
+ [Attributes.NODEJS_EVENT_LOOP_STATE]: ['idle', 'active'],
61
47
  });
62
-
63
- this.eventLoopTime = meter.createUpDownCounter(Metrics.NODEJS_EVENT_LOOP_TIME, {
64
- unit: 'ms',
65
- valueType: ValueType.INT,
66
- description: 'How much time the event loop has spent in a given state',
67
- });
68
-
69
48
  this.eventLoopDelay = monitorEventLoopDelay();
70
49
 
71
50
  this.memoryGauges = {
72
- heapUsed: meter.createObservableGauge(Metrics.NODEJS_MEMORY_HEAP_USAGE, {
73
- unit: 'By',
74
- description: 'Memory used by the V8 heap',
75
- }),
76
- heapTotal: meter.createObservableGauge(Metrics.NODEJS_MEMORY_HEAP_TOTAL, {
77
- unit: 'By',
78
- description: 'The max size the V8 heap can grow to',
79
- }),
80
- arrayBuffers: meter.createObservableGauge(Metrics.NODEJS_MEMORY_BUFFER_USAGE, {
81
- unit: 'By',
82
- description: 'Memory allocated for buffers (includes native memory used)',
83
- }),
84
- external: meter.createObservableGauge(Metrics.NODEJS_MEMORY_NATIVE_USAGE, {
85
- unit: 'By',
86
- description: 'Memory allocated for native C++ objects',
87
- }),
51
+ heapUsed: meter.createObservableGauge(Metrics.NODEJS_MEMORY_HEAP_USAGE),
52
+ heapTotal: meter.createObservableGauge(Metrics.NODEJS_MEMORY_HEAP_TOTAL),
53
+ arrayBuffers: meter.createObservableGauge(Metrics.NODEJS_MEMORY_BUFFER_USAGE),
54
+ external: meter.createObservableGauge(Metrics.NODEJS_MEMORY_NATIVE_USAGE),
88
55
  };
89
56
  }
90
57
 
package/src/noop.ts CHANGED
@@ -1,13 +1,63 @@
1
- import { type Meter, type Span, type SpanContext, type Tracer, createNoopMeter } from '@opentelemetry/api';
1
+ import { type Context, type Span, type SpanContext, type Tracer, createNoopMeter } from '@opentelemetry/api';
2
2
 
3
- import type { TelemetryClient } from './telemetry.js';
3
+ import type { MetricDefinition } from './metrics.js';
4
+ import type {
5
+ Gauge,
6
+ Histogram,
7
+ Meter,
8
+ ObservableGauge,
9
+ ObservableUpDownCounter,
10
+ TelemetryClient,
11
+ UpDownCounter,
12
+ } from './telemetry.js';
13
+
14
+ /** A no-op meter that implements our custom Meter interface */
15
+ class NoopMeter implements Meter {
16
+ private otelMeter = createNoopMeter();
17
+
18
+ createGauge(_metric: MetricDefinition): Gauge {
19
+ return this.otelMeter.createGauge('');
20
+ }
21
+
22
+ createObservableGauge(_metric: MetricDefinition): ObservableGauge {
23
+ return this.otelMeter.createObservableGauge('');
24
+ }
25
+
26
+ createHistogram(_metric: MetricDefinition, _extraOptions?: Parameters<Meter['createHistogram']>[1]): Histogram {
27
+ return this.otelMeter.createHistogram('');
28
+ }
29
+
30
+ createUpDownCounter(_metric: MetricDefinition): UpDownCounter {
31
+ return this.otelMeter.createUpDownCounter('');
32
+ }
33
+
34
+ createObservableUpDownCounter(_metric: MetricDefinition): ObservableUpDownCounter {
35
+ return this.otelMeter.createObservableUpDownCounter('');
36
+ }
37
+
38
+ addBatchObservableCallback(
39
+ callback: Parameters<Meter['addBatchObservableCallback']>[0],
40
+ observables: Parameters<Meter['addBatchObservableCallback']>[1],
41
+ ): void {
42
+ this.otelMeter.addBatchObservableCallback(callback, observables);
43
+ }
44
+
45
+ removeBatchObservableCallback(
46
+ callback: Parameters<Meter['removeBatchObservableCallback']>[0],
47
+ observables: Parameters<Meter['removeBatchObservableCallback']>[1],
48
+ ): void {
49
+ this.otelMeter.removeBatchObservableCallback(callback, observables);
50
+ }
51
+ }
4
52
 
5
53
  export class NoopTelemetryClient implements TelemetryClient {
54
+ private meter = new NoopMeter();
55
+
6
56
  setExportedPublicTelemetry(_prefixes: string[]): void {}
7
57
  setPublicTelemetryCollectFrom(_roles: string[]): void {}
8
58
 
9
59
  getMeter(): Meter {
10
- return createNoopMeter();
60
+ return this.meter;
11
61
  }
12
62
 
13
63
  getTracer(): Tracer {
@@ -25,6 +75,14 @@ export class NoopTelemetryClient implements TelemetryClient {
25
75
  isEnabled() {
26
76
  return false;
27
77
  }
78
+
79
+ getTraceContext(): string | undefined {
80
+ return undefined;
81
+ }
82
+
83
+ extractPropagatedContext(_traceContext: string): Context | undefined {
84
+ return undefined;
85
+ }
28
86
  }
29
87
 
30
88
  // @opentelemetry/api internally uses NoopTracer and NoopSpan but they're not exported
package/src/otel.ts CHANGED
@@ -1,16 +1,20 @@
1
1
  import { type LogData, type Logger, addLogDataHandler } from '@aztec/foundation/log';
2
2
 
3
3
  import {
4
+ type Context,
4
5
  DiagConsoleLogger,
5
6
  DiagLogLevel,
6
- type Meter,
7
+ type Meter as OtelMeter,
8
+ ROOT_CONTEXT,
7
9
  type Tracer,
8
10
  type TracerProvider,
9
11
  context,
10
12
  diag,
11
13
  isSpanContextValid,
14
+ propagation,
12
15
  trace,
13
16
  } from '@opentelemetry/api';
17
+ import { W3CTraceContextPropagator } from '@opentelemetry/core';
14
18
  import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';
15
19
  import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
16
20
  import { HostMetrics } from '@opentelemetry/host-metrics';
@@ -24,24 +28,79 @@ import {
24
28
  type PeriodicExportingMetricReaderOptions,
25
29
  View,
26
30
  } from '@opentelemetry/sdk-metrics';
27
- import { BatchSpanProcessor, NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
31
+ import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
28
32
  import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';
29
33
 
30
34
  import type { TelemetryClientConfig } from './config.js';
35
+ import { toMetricOptions } from './metric-utils.js';
36
+ import type { MetricDefinition } from './metrics.js';
37
+ import { MonitoredBatchSpanProcessor } from './monitored_batch_span_processor.js';
31
38
  import { NodejsMetricsMonitor } from './nodejs_metrics_monitor.js';
32
39
  import { OtelFilterMetricExporter, PublicOtelFilterMetricExporter } from './otel_filter_metric_exporter.js';
33
40
  import { registerOtelLoggerProvider } from './otel_logger_provider.js';
34
41
  import { getOtelResource } from './otel_resource.js';
35
- import type { TelemetryClient } from './telemetry.js';
42
+ import type {
43
+ Gauge,
44
+ Histogram,
45
+ Meter,
46
+ ObservableGauge,
47
+ ObservableUpDownCounter,
48
+ TelemetryClient,
49
+ UpDownCounter,
50
+ } from './telemetry.js';
51
+
52
+ /** Wraps an OpenTelemetry Meter to implement our custom Meter interface */
53
+ class WrappedMeter implements Meter {
54
+ constructor(private otelMeter: OtelMeter) {}
55
+
56
+ createGauge(metric: MetricDefinition): Gauge {
57
+ return this.otelMeter.createGauge(metric.name, toMetricOptions(metric));
58
+ }
59
+
60
+ createObservableGauge(metric: MetricDefinition): ObservableGauge {
61
+ return this.otelMeter.createObservableGauge(metric.name, toMetricOptions(metric));
62
+ }
63
+
64
+ createHistogram(metric: MetricDefinition, extraOptions?: Parameters<Meter['createHistogram']>[1]): Histogram {
65
+ return this.otelMeter.createHistogram(metric.name, { ...toMetricOptions(metric), ...extraOptions });
66
+ }
67
+
68
+ createUpDownCounter(metric: MetricDefinition): UpDownCounter {
69
+ return this.otelMeter.createUpDownCounter(metric.name, toMetricOptions(metric));
70
+ }
71
+
72
+ createObservableUpDownCounter(metric: MetricDefinition): ObservableUpDownCounter {
73
+ return this.otelMeter.createObservableUpDownCounter(metric.name, toMetricOptions(metric));
74
+ }
75
+
76
+ addBatchObservableCallback(
77
+ callback: Parameters<Meter['addBatchObservableCallback']>[0],
78
+ observables: Parameters<Meter['addBatchObservableCallback']>[1],
79
+ ): void {
80
+ this.otelMeter.addBatchObservableCallback(callback, observables);
81
+ }
82
+
83
+ removeBatchObservableCallback(
84
+ callback: Parameters<Meter['removeBatchObservableCallback']>[0],
85
+ observables: Parameters<Meter['removeBatchObservableCallback']>[1],
86
+ ): void {
87
+ this.otelMeter.removeBatchObservableCallback(callback, observables);
88
+ }
89
+ }
36
90
 
37
91
  export type OpenTelemetryClientFactory = (resource: IResource, log: Logger) => OpenTelemetryClient;
38
92
 
39
93
  export class OpenTelemetryClient implements TelemetryClient {
40
94
  hostMetrics: HostMetrics | undefined;
41
95
  nodejsMetricsMonitor: NodejsMetricsMonitor | undefined;
42
- private meters: Map<string, Meter> = new Map<string, Meter>();
96
+ private meters: Map<string, WrappedMeter> = new Map<string, WrappedMeter>();
43
97
  private tracers: Map<string, Tracer> = new Map<string, Tracer>();
44
98
 
99
+ /** Memoized shutdown promise. The telemetry client is shared between the aztec-node and an embedded prover-node,
100
+ * so stop() can be invoked more than once; the providers throw "shutdown may only be called once" and
101
+ * "invalid attempt to force flush after shutdown" if that happens. Guarding here makes stop()/flush() idempotent. */
102
+ private stopPromise: Promise<void> | undefined;
103
+
45
104
  protected constructor(
46
105
  private resource: IResource,
47
106
  private meterProvider: MeterProvider,
@@ -62,7 +121,8 @@ export class OpenTelemetryClient implements TelemetryClient {
62
121
  getMeter(name: string): Meter {
63
122
  let meter = this.meters.get(name);
64
123
  if (!meter) {
65
- meter = this.meterProvider.getMeter(name, this.resource.attributes[ATTR_SERVICE_VERSION] as string);
124
+ const otelMeter = this.meterProvider.getMeter(name, this.resource.attributes[ATTR_SERVICE_VERSION] as string);
125
+ meter = new WrappedMeter(otelMeter);
66
126
  this.meters.set(name, meter);
67
127
  }
68
128
  return meter;
@@ -100,9 +160,10 @@ export class OpenTelemetryClient implements TelemetryClient {
100
160
  meterProvider: this.meterProvider,
101
161
  });
102
162
 
103
- this.nodejsMetricsMonitor = new NodejsMetricsMonitor(
163
+ const nodejsMeter = new WrappedMeter(
104
164
  this.meterProvider.getMeter(this.resource.attributes[ATTR_SERVICE_NAME] as string),
105
165
  );
166
+ this.nodejsMetricsMonitor = new NodejsMetricsMonitor(nodejsMeter);
106
167
 
107
168
  this.hostMetrics.start();
108
169
  this.nodejsMetricsMonitor.start();
@@ -113,6 +174,10 @@ export class OpenTelemetryClient implements TelemetryClient {
113
174
  }
114
175
 
115
176
  public async flush() {
177
+ // Flushing after the providers have been shut down throws "invalid attempt to force flush after shutdown".
178
+ if (this.stopPromise) {
179
+ return;
180
+ }
116
181
  await Promise.all([
117
182
  this.meterProvider.forceFlush(),
118
183
  this.loggerProvider?.forceFlush(),
@@ -120,7 +185,11 @@ export class OpenTelemetryClient implements TelemetryClient {
120
185
  ]);
121
186
  }
122
187
 
123
- public async stop() {
188
+ public stop() {
189
+ return (this.stopPromise ??= this.doStop());
190
+ }
191
+
192
+ private async doStop() {
124
193
  this.nodejsMetricsMonitor?.stop();
125
194
 
126
195
  const flushAndShutdown = async (provider?: { forceFlush: () => Promise<void>; shutdown: () => Promise<void> }) => {
@@ -138,6 +207,19 @@ export class OpenTelemetryClient implements TelemetryClient {
138
207
  ]);
139
208
  }
140
209
 
210
+ public getTraceContext(): string | undefined {
211
+ const carrier: Record<string, string> = {};
212
+ propagation.inject(context.active(), carrier);
213
+ return carrier['traceparent'];
214
+ }
215
+
216
+ public extractPropagatedContext(traceContext: string): Context {
217
+ const extractedContext = propagation.extract(ROOT_CONTEXT, {
218
+ traceparent: traceContext,
219
+ });
220
+ return extractedContext;
221
+ }
222
+
141
223
  public static createMeterProvider(
142
224
  resource: IResource,
143
225
  exporters: Array<PeriodicExportingMetricReaderOptions>,
@@ -173,6 +255,23 @@ export class OpenTelemetryClient implements TelemetryClient {
173
255
  true,
174
256
  ),
175
257
  }),
258
+ // Pending-to-mined delay routinely exceeds the 1-minute ceiling of the generic `ms`
259
+ // view below under load, so it would saturate at 60s. Give this one metric wider
260
+ // buckets (1s to 10min). This must precede the generic `ms` view: when multiple views
261
+ // match an instrument, the SDK keeps the first-registered compatible storage, so the
262
+ // first view in this list wins the bucket boundaries.
263
+ new View({
264
+ instrumentType: InstrumentType.HISTOGRAM,
265
+ instrumentName: 'aztec.mempool.tx_mined_delay',
266
+ instrumentUnit: 'ms',
267
+ aggregation: new ExplicitBucketHistogramAggregation(
268
+ [
269
+ 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,
270
+ 600_000,
271
+ ],
272
+ true,
273
+ ),
274
+ }),
176
275
  new View({
177
276
  instrumentType: InstrumentType.HISTOGRAM,
178
277
  instrumentUnit: 'ms',
@@ -266,6 +365,36 @@ export class OpenTelemetryClient implements TelemetryClient {
266
365
  true,
267
366
  ),
268
367
  }),
368
+ // L1 gas prices in gwei: priority fees ~0.01-10, base fees ~1-500, spikes to 1000+
369
+ new View({
370
+ instrumentType: InstrumentType.HISTOGRAM,
371
+ instrumentUnit: 'gwei',
372
+ aggregation: new ExplicitBucketHistogramAggregation(
373
+ [0.1, 0.5, 1, 2, 5, 10, 20, 50, 100, 200, 500, 1_000],
374
+ true,
375
+ ),
376
+ }),
377
+ // L1 gas consumption: tx gas 100k-30M, calldata/blob gas varies
378
+ new View({
379
+ instrumentType: InstrumentType.HISTOGRAM,
380
+ instrumentUnit: 'gas',
381
+ aggregation: new ExplicitBucketHistogramAggregation(
382
+ [
383
+ 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,
384
+ 30_000_000,
385
+ ],
386
+ true,
387
+ ),
388
+ }),
389
+ // L1 tx total fee in ETH: typically 0.001 - 1 ETH
390
+ new View({
391
+ instrumentType: InstrumentType.HISTOGRAM,
392
+ instrumentUnit: 'eth',
393
+ aggregation: new ExplicitBucketHistogramAggregation(
394
+ [0.0001, 0.0005, 0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 5, 10],
395
+ true,
396
+ ),
397
+ }),
269
398
  ],
270
399
  });
271
400
  }
@@ -275,19 +404,40 @@ export class OpenTelemetryClient implements TelemetryClient {
275
404
  const tracerProvider = new NodeTracerProvider({
276
405
  resource,
277
406
  spanProcessors: config.tracesCollectorUrl
278
- ? [new BatchSpanProcessor(new OTLPTraceExporter({ url: config.tracesCollectorUrl.href }))]
407
+ ? [
408
+ new MonitoredBatchSpanProcessor(new OTLPTraceExporter({ url: config.tracesCollectorUrl.href }), log, {
409
+ maxQueueSize: config.otelBspMaxQueueSize,
410
+ minTraceDurationMs: config.otelMinTraceDurationMs,
411
+ }),
412
+ ]
279
413
  : [],
280
414
  });
281
415
 
282
- tracerProvider.register();
416
+ tracerProvider.register({
417
+ propagator: new W3CTraceContextPropagator(),
418
+ });
283
419
 
284
420
  const exporters: PeriodicExportingMetricReaderOptions[] = [];
285
421
  if (config.metricsCollectorUrl) {
422
+ // Default to a blacklist that is empty (allow all metrics)
423
+ let filter: string[] = [];
424
+ let mode: 'allow' | 'deny' = 'deny';
425
+ if (config.otelExcludeMetrics.length > 0) {
426
+ // Implement a blacklist as specified in config
427
+ log.info(`Excluding metrics from export: ${config.otelExcludeMetrics}`);
428
+ filter = config.otelExcludeMetrics;
429
+ mode = 'deny';
430
+ } else if (config.otelIncludeMetrics.length > 0) {
431
+ // Implement a whitelist as specified in config
432
+ log.info(`Including only specified metrics for export: ${config.otelIncludeMetrics}`);
433
+ filter = config.otelIncludeMetrics;
434
+ mode = 'allow';
435
+ }
286
436
  exporters.push({
287
437
  exporter: new OtelFilterMetricExporter(
288
438
  new OTLPMetricExporter({ url: config.metricsCollectorUrl.href }),
289
- config.otelExcludeMetrics,
290
- 'deny',
439
+ filter,
440
+ mode,
291
441
  ),
292
442
  exportTimeoutMillis: config.otelExportTimeoutMs,
293
443
  exportIntervalMillis: config.otelCollectIntervalMs,
@@ -1,3 +1,5 @@
1
+ import type { DiagnosticsMiddleware } from '@aztec/foundation/json-rpc/server';
2
+
1
3
  import { ROOT_CONTEXT, type Span, SpanKind, SpanStatusCode, propagation } from '@opentelemetry/api';
2
4
  import type Koa from 'koa';
3
5
 
@@ -17,7 +19,7 @@ export function getOtelJsonRpcPropagationMiddleware(
17
19
  const context = propagation.extract(ROOT_CONTEXT, ctx.request.headers);
18
20
  const method = (ctx.request.body as any)?.method;
19
21
  return tracer.startActiveSpan(
20
- `JsonRpcServer.${method ?? 'unknown'}`,
22
+ `JsonRpcServer.${method ?? 'batch'}`,
21
23
  { kind: SpanKind.SERVER },
22
24
  context,
23
25
  async (span: Span): Promise<void> => {
@@ -48,3 +50,42 @@ export function getOtelJsonRpcPropagationMiddleware(
48
50
  );
49
51
  };
50
52
  }
53
+
54
+ export function getOtelJsonRpcDiagnosticsMiddleware(): DiagnosticsMiddleware {
55
+ return function otelJsonRpcDiagnostics(ctx, next) {
56
+ const [namespace, method] = splitNamespace(ctx.method);
57
+ const scope = namespace ?? 'UnknownHandler';
58
+ const tracer = getTelemetryClient().getTracer(scope);
59
+ return tracer.startActiveSpan(
60
+ `${scope}.${method}`,
61
+ { kind: SpanKind.INTERNAL, attributes: { [ATTR_JSONRPC_METHOD]: ctx.method } },
62
+ async span => {
63
+ if (ctx.id !== null) {
64
+ span.setAttribute(ATTR_JSONRPC_REQUEST_ID, ctx.id);
65
+ }
66
+
67
+ try {
68
+ await next();
69
+ span.setStatus({ code: SpanStatusCode.OK });
70
+ } catch (err) {
71
+ span.setStatus({ code: SpanStatusCode.ERROR, message: err instanceof Error ? err.message : String(err) });
72
+ if (typeof err === 'string' || err instanceof Error) {
73
+ span.recordException(err);
74
+ }
75
+ throw err;
76
+ } finally {
77
+ span.end();
78
+ }
79
+ },
80
+ );
81
+ };
82
+ }
83
+
84
+ function splitNamespace(fullMethod: string): [namespace: string | undefined, method: string] {
85
+ const idx = fullMethod.indexOf('_');
86
+ if (idx > -1) {
87
+ return [fullMethod.slice(0, idx), fullMethod.slice(idx + 1)];
88
+ } else {
89
+ return [undefined, fullMethod];
90
+ }
91
+ }
@@ -7,10 +7,26 @@ import {
7
7
  osDetectorSync,
8
8
  serviceInstanceIdDetectorSync,
9
9
  } from '@opentelemetry/resources';
10
- import { SEMRESATTRS_SERVICE_NAME } from '@opentelemetry/semantic-conventions';
10
+ import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';
11
+ import { readFileSync } from 'fs';
12
+ import { dirname, resolve } from 'path';
13
+ import { fileURLToPath } from 'url';
11
14
 
12
15
  import { AZTEC_NODE_ROLE, AZTEC_REGISTRY_ADDRESS, AZTEC_ROLLUP_ADDRESS, AZTEC_ROLLUP_VERSION } from './attributes.js';
13
16
 
17
+ /** Reads the Aztec client version from the release manifest. */
18
+ function getAztecVersion(): string | undefined {
19
+ try {
20
+ const releasePleasePath = resolve(
21
+ dirname(fileURLToPath(import.meta.url)),
22
+ '../../../.release-please-manifest.json',
23
+ );
24
+ return JSON.parse(readFileSync(releasePleasePath, 'utf-8'))['.'];
25
+ } catch {
26
+ return undefined;
27
+ }
28
+ }
29
+
14
30
  export function getOtelResource(): IResource {
15
31
  const resource = detectResourcesSync({
16
32
  detectors: [
@@ -42,7 +58,8 @@ const aztecNetworkDetectorSync: DetectorSync = {
42
58
  }
43
59
  const aztecAttributes = {
44
60
  // this gets overwritten by OTEL_RESOURCE_ATTRIBUTES (if set)
45
- [SEMRESATTRS_SERVICE_NAME]: role ? `aztec-${role}` : undefined,
61
+ [ATTR_SERVICE_NAME]: role ? `aztec-${role}` : undefined,
62
+ [ATTR_SERVICE_VERSION]: getAztecVersion(),
46
63
  [AZTEC_NODE_ROLE]: role,
47
64
  [AZTEC_ROLLUP_VERSION]: process.env.ROLLUP_VERSION ?? 'canonical',
48
65
  [AZTEC_ROLLUP_ADDRESS]: process.env.ROLLUP_CONTRACT_ADDRESS,