@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.
Files changed (51) hide show
  1. package/dest/attributes.js +66 -0
  2. package/dest/aztec_resource_detector.js +18 -0
  3. package/dest/bench.js +98 -0
  4. package/dest/config.js +66 -0
  5. package/dest/event_loop_monitor.js +93 -0
  6. package/dest/histogram_utils.js +47 -0
  7. package/dest/index.js +10 -0
  8. package/dest/lmdb_metrics.js +42 -0
  9. package/dest/metrics.js +123 -0
  10. package/dest/noop.js +71 -0
  11. package/dest/otel.js +237 -0
  12. package/dest/otel_filter_metric_exporter.js +33 -0
  13. package/dest/otel_logger_provider.js +25 -0
  14. package/dest/otel_propagation.js +44 -0
  15. package/dest/otel_resource.js +16 -0
  16. package/dest/prom_otel_adapter.js +183 -0
  17. package/dest/start.js +24 -0
  18. package/dest/telemetry.js +116 -0
  19. package/dest/vendor/attributes.js +5 -0
  20. package/dest/vendor/otel-pino-stream.js +229 -0
  21. package/dest/with_tracer.js +32 -0
  22. package/dest/wrappers/fetch.js +39 -0
  23. package/dest/wrappers/index.js +3 -0
  24. package/dest/wrappers/json_rpc_server.js +11 -0
  25. package/dest/wrappers/l2_block_stream.js +26 -0
  26. package/package.json +92 -0
  27. package/src/attributes.ts +113 -0
  28. package/src/aztec_resource_detector.ts +28 -0
  29. package/src/bench.ts +153 -0
  30. package/src/config.ts +89 -0
  31. package/src/event_loop_monitor.ts +119 -0
  32. package/src/histogram_utils.ts +50 -0
  33. package/src/index.ts +10 -0
  34. package/src/lmdb_metrics.ts +45 -0
  35. package/src/metrics.ts +151 -0
  36. package/src/noop.ts +91 -0
  37. package/src/otel.ts +299 -0
  38. package/src/otel_filter_metric_exporter.ts +38 -0
  39. package/src/otel_logger_provider.ts +31 -0
  40. package/src/otel_propagation.ts +50 -0
  41. package/src/otel_resource.ts +26 -0
  42. package/src/prom_otel_adapter.ts +326 -0
  43. package/src/start.ts +33 -0
  44. package/src/telemetry.ts +267 -0
  45. package/src/vendor/attributes.ts +5 -0
  46. package/src/vendor/otel-pino-stream.ts +282 -0
  47. package/src/with_tracer.ts +35 -0
  48. package/src/wrappers/fetch.ts +52 -0
  49. package/src/wrappers/index.ts +3 -0
  50. package/src/wrappers/json_rpc_server.ts +15 -0
  51. 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
+ }