diff --git a/Cargo.toml b/Cargo.toml index 88156a8acc..4f8802f35f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -289,7 +289,7 @@ zip = { version = "2.4.1", default-features = false, features = ["flate2"] } opentelemetry = "0.27.0" opentelemetry-http = "0.27.0" -opentelemetry-otlp = { version = "0.27.0", features = ["logs", "http-proto", "http-json", "populate-logs-event-name"] } +opentelemetry-otlp = { version = "0.27.0", features = ["logs", "http-proto", "http-json", "populate-logs-event-name", "grpc-tonic"] } opentelemetry-semantic-conventions = { version = "0.27.0", features = ["semconv_experimental"] } opentelemetry_sdk = { version = "0.27.0", features = ["rt-tokio", "trace"] } diff --git a/ext/telemetry/lib.rs b/ext/telemetry/lib.rs index cd03f1d776..07784a5630 100644 --- a/ext/telemetry/lib.rs +++ b/ext/telemetry/lib.rs @@ -870,10 +870,10 @@ pub fn init( // Parse the `OTEL_EXPORTER_OTLP_PROTOCOL` variable. The opentelemetry_* // crates don't do this automatically. - // TODO(piscisaureus): enable GRPC support. let protocol = match env::var("OTEL_EXPORTER_OTLP_PROTOCOL").as_deref() { Ok("http/protobuf") => Protocol::HttpBinary, Ok("http/json") => Protocol::HttpJson, + Ok("grpc") => Protocol::Grpc, Ok("") | Err(env::VarError::NotPresent) => Protocol::HttpBinary, Ok(protocol) => { return Err(deno_core::anyhow::anyhow!( @@ -930,45 +930,78 @@ pub fn init( // `OTEL_EXPORTER_OTLP_ENDPOINT` environment variable. Additional headers can // be specified using `OTEL_EXPORTER_OTLP_HEADERS`. - let client = hyper_client::HyperClient::new()?; + let (span_exporter, metric_exporter, log_exporter) = match protocol { + Protocol::Grpc => { + let span_exporter = opentelemetry_otlp::SpanExporter::builder() + .with_tonic() + .build()?; + let temporality_preference = + env::var("OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE") + .ok() + .map(|s| s.to_lowercase()); + let temporality = match temporality_preference.as_deref() { + None | Some("cumulative") => Temporality::Cumulative, + Some("delta") => Temporality::Delta, + Some("lowmemory") => Temporality::LowMemory, + Some(other) => { + return Err(deno_core::anyhow::anyhow!( + "Invalid value for OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: {}", + other + )); + } + }; + let metric_exporter = opentelemetry_otlp::MetricExporter::builder() + .with_tonic() + .with_temporality(temporality) + .build()?; + let log_exporter = opentelemetry_otlp::LogExporter::builder() + .with_tonic() + .build()?; + (span_exporter, metric_exporter, log_exporter) + } + _ => { + let client = hyper_client::HyperClient::new()?; + let span_exporter = HttpExporterBuilder::default() + .with_http_client(client.clone()) + .with_protocol(protocol) + .build_span_exporter()?; + let temporality_preference = + env::var("OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE") + .ok() + .map(|s| s.to_lowercase()); + let temporality = match temporality_preference.as_deref() { + None | Some("cumulative") => Temporality::Cumulative, + Some("delta") => Temporality::Delta, + Some("lowmemory") => Temporality::LowMemory, + Some(other) => { + return Err(deno_core::anyhow::anyhow!( + "Invalid value for OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: {}", + other + )); + } + }; + let metric_exporter = HttpExporterBuilder::default() + .with_http_client(client.clone()) + .with_protocol(protocol) + .build_metrics_exporter(temporality)?; + let log_exporter = HttpExporterBuilder::default() + .with_http_client(client) + .with_protocol(protocol) + .build_log_exporter()?; + (span_exporter, metric_exporter, log_exporter) + } + }; - let span_exporter = HttpExporterBuilder::default() - .with_http_client(client.clone()) - .with_protocol(protocol) - .build_span_exporter()?; let mut span_processor = BatchSpanProcessor::builder(span_exporter, OtelSharedRuntime).build(); span_processor.set_resource(&resource); - let temporality_preference = - env::var("OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE") - .ok() - .map(|s| s.to_lowercase()); - let temporality = match temporality_preference.as_deref() { - None | Some("cumulative") => Temporality::Cumulative, - Some("delta") => Temporality::Delta, - Some("lowmemory") => Temporality::LowMemory, - Some(other) => { - return Err(deno_core::anyhow::anyhow!( - "Invalid value for OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: {}", - other - )); - } - }; - let metric_exporter = HttpExporterBuilder::default() - .with_http_client(client.clone()) - .with_protocol(protocol) - .build_metrics_exporter(temporality)?; let metric_reader = DenoPeriodicReader::new(metric_exporter); let meter_provider = SdkMeterProvider::builder() .with_reader(metric_reader) .with_resource(resource.clone()) .build(); - let log_exporter = HttpExporterBuilder::default() - .with_http_client(client) - .with_protocol(protocol) - .build_log_exporter()?; let log_processor = BatchLogProcessor::builder(log_exporter, OtelSharedRuntime).build(); log_processor.set_resource(&resource); diff --git a/tests/specs/cli/otel_basic/.gitignore b/tests/specs/cli/otel_basic/.gitignore new file mode 100644 index 0000000000..02c915e200 --- /dev/null +++ b/tests/specs/cli/otel_basic/.gitignore @@ -0,0 +1 @@ +opentelemetry-proto/ diff --git a/tests/specs/cli/otel_basic/__test__.jsonc b/tests/specs/cli/otel_basic/__test__.jsonc index 31fe5e0a2b..c3158d021a 100644 --- a/tests/specs/cli/otel_basic/__test__.jsonc +++ b/tests/specs/cli/otel_basic/__test__.jsonc @@ -2,6 +2,17 @@ "tests": { "basic": { "args": "run -A main.ts basic.ts", + "envs": { + "OTEL_EXPORTER_OTLP_PROTOCOL": "http/json", + "OTEL_EXPORTER_OTLP_CERTIFICATE": "../../../testdata/tls/RootCA.crt" + }, + "output": "basic.out" + }, + "basic_grpc": { + "args": "run -A main.ts basic.ts", + "envs": { + "OTEL_EXPORTER_OTLP_PROTOCOL": "grpc" + }, "output": "basic.out" }, "basic_vsock": { @@ -26,6 +37,16 @@ }, "metric": { "envs": { + "OTEL_METRIC_EXPORT_INTERVAL": "1000", + "OTEL_EXPORTER_OTLP_PROTOCOL": "http/json", + "OTEL_EXPORTER_OTLP_CERTIFICATE": "../../../testdata/tls/RootCA.crt" + }, + "args": "run -A main.ts metric.ts", + "output": "metric.out" + }, + "metric_grpc": { + "envs": { + "OTEL_EXPORTER_OTLP_PROTOCOL": "grpc", "OTEL_METRIC_EXPORT_INTERVAL": "1000" }, "args": "run -A main.ts metric.ts", @@ -33,44 +54,104 @@ }, "fetch": { "args": "run -A main.ts fetch.ts", + "envs": { + "OTEL_EXPORTER_OTLP_PROTOCOL": "http/json", + "OTEL_EXPORTER_OTLP_CERTIFICATE": "../../../testdata/tls/RootCA.crt" + }, "output": "fetch.out" }, "http_metric": { "envs": { - "OTEL_METRIC_EXPORT_INTERVAL": "1000" + "OTEL_METRIC_EXPORT_INTERVAL": "1000", + "OTEL_EXPORTER_OTLP_PROTOCOL": "http/json", + "OTEL_EXPORTER_OTLP_CERTIFICATE": "../../../testdata/tls/RootCA.crt" }, "args": "run -A main.ts http_metric.ts", "output": "http_metric.out" }, "http_propagators": { "args": "run -A main.ts http_propagators.ts", + "envs": { + "OTEL_EXPORTER_OTLP_PROTOCOL": "http/json", + "OTEL_EXPORTER_OTLP_CERTIFICATE": "../../../testdata/tls/RootCA.crt" + }, "output": "http_propagators.out" }, "node_http_metric": { "envs": { - "OTEL_METRIC_EXPORT_INTERVAL": "1000" + "OTEL_METRIC_EXPORT_INTERVAL": "1000", + "OTEL_EXPORTER_OTLP_PROTOCOL": "http/json", + "OTEL_EXPORTER_OTLP_CERTIFICATE": "../../../testdata/tls/RootCA.crt" }, "args": "run -A main.ts node_http_metric.ts", "output": "node_http_metric.out" }, "links": { "args": "run -A main.ts links.ts", + "envs": { + "OTEL_EXPORTER_OTLP_PROTOCOL": "http/json", + "OTEL_EXPORTER_OTLP_CERTIFICATE": "../../../testdata/tls/RootCA.crt" + }, + "output": "links.out" + }, + "links_grpc": { + "args": "run -A main.ts links.ts", + "envs": { + "OTEL_EXPORTER_OTLP_PROTOCOL": "grpc" + }, "output": "links.out" }, "start_active_span": { "args": "run -A main.ts start_active_span.ts", + "envs": { + "OTEL_EXPORTER_OTLP_PROTOCOL": "http/json", + "OTEL_EXPORTER_OTLP_CERTIFICATE": "../../../testdata/tls/RootCA.crt" + }, + "output": "start_active_span.out" + }, + "start_active_span_grpc": { + "args": "run -A main.ts start_active_span.ts", + "envs": { + "OTEL_EXPORTER_OTLP_PROTOCOL": "grpc" + }, "output": "start_active_span.out" }, "node_http_request": { "args": "run -A main.ts node_http_request.ts", + "envs": { + "OTEL_EXPORTER_OTLP_PROTOCOL": "http/json", + "OTEL_EXPORTER_OTLP_CERTIFICATE": "../../../testdata/tls/RootCA.crt" + }, "output": "node_http_request.out" }, "events": { "args": "run -A main.ts events.ts", + "envs": { + "OTEL_EXPORTER_OTLP_PROTOCOL": "http/json", + "OTEL_EXPORTER_OTLP_CERTIFICATE": "../../../testdata/tls/RootCA.crt" + }, + "output": "events.out" + }, + "events_grpc": { + "args": "run -A main.ts events.ts", + "envs": { + "OTEL_EXPORTER_OTLP_PROTOCOL": "grpc" + }, "output": "events.out" }, "metric_temporality_delta": { "envs": { + "OTEL_METRIC_EXPORT_INTERVAL": "1000", + "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE": "delta", + "OTEL_EXPORTER_OTLP_PROTOCOL": "http/json", + "OTEL_EXPORTER_OTLP_CERTIFICATE": "../../../testdata/tls/RootCA.crt" + }, + "args": "run -A main.ts metric_temporality.ts", + "output": "metric_temporality_delta.out" + }, + "metric_temporality_delta_grpc": { + "envs": { + "OTEL_EXPORTER_OTLP_PROTOCOL": "grpc", "OTEL_METRIC_EXPORT_INTERVAL": "1000", "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE": "delta" }, @@ -79,6 +160,17 @@ }, "metric_temporality_cumulative": { "envs": { + "OTEL_METRIC_EXPORT_INTERVAL": "1000", + "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE": "cumulative", + "OTEL_EXPORTER_OTLP_PROTOCOL": "http/json", + "OTEL_EXPORTER_OTLP_CERTIFICATE": "../../../testdata/tls/RootCA.crt" + }, + "args": "run -A main.ts metric_temporality.ts", + "output": "metric_temporality_cumulative.out" + }, + "metric_temporality_cumulative_grpc": { + "envs": { + "OTEL_EXPORTER_OTLP_PROTOCOL": "grpc", "OTEL_METRIC_EXPORT_INTERVAL": "1000", "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE": "cumulative" }, diff --git a/tests/specs/cli/otel_basic/env_file b/tests/specs/cli/otel_basic/env_file index 5c1572df54..4447743dee 100644 --- a/tests/specs/cli/otel_basic/env_file +++ b/tests/specs/cli/otel_basic/env_file @@ -1,4 +1,2 @@ OTEL_DENO=true DENO_UNSTABLE_OTEL_DETERMINISTIC=0 -OTEL_EXPORTER_OTLP_PROTOCOL=http/json -OTEL_EXPORTER_OTLP_CERTIFICATE=../../../testdata/tls/RootCA.crt diff --git a/tests/specs/cli/otel_basic/main.ts b/tests/specs/cli/otel_basic/main.ts index 984ac685e3..184a19147e 100644 --- a/tests/specs/cli/otel_basic/main.ts +++ b/tests/specs/cli/otel_basic/main.ts @@ -1,11 +1,130 @@ // Copyright 2018-2025 the Deno authors. MIT license. +import grpc from "npm:@grpc/grpc-js"; +import protoLoader from "npm:@grpc/proto-loader"; + const data = { spans: [], logs: [], metrics: [], }; +// Download and load OTLP proto definitions (traces, metrics, logs) from GitHub if not present +// Try to match with what's cloned into opentelemetry-proto crate +const opentelemetryProtoTag = "1.3.2"; +async function ensureProtoFiles() { + if (await fileExists("./opentelemetry-proto")) return; + console.log("Downloading OpenTelemetry proto repo..."); + const repo = "open-telemetry/opentelemetry-proto"; + const url = + `https://github.com/${repo}/archive/refs/tags/v${opentelemetryProtoTag}.zip`; + const zipPath = "opentelemetry-proto.zip"; + const unzipDir = "opentelemetry-proto"; + // Download zip + const resp = await fetch(url); + if (!resp.ok) throw new Error(`Failed to download proto zip: ${resp.status}`); + const zipData = new Uint8Array(await resp.arrayBuffer()); + await Deno.writeFile(zipPath, zipData); + // Unzip + const p = Deno.run({ cmd: ["unzip", "-q", zipPath, "-d", unzipDir] }); + const status = await p.status(); + if (!status.success) throw new Error("Failed to unzip proto files"); + // Clean up + await Deno.remove(zipPath); +} + +async function fileExists(path) { + try { + await Deno.stat(path); + return true; + } catch (_) { + return false; + } +} + +function handleOtlpRequest(call, callback, type) { + // call.request is the OTLP protobuf message + // For test purposes, just push to data and return success + console.log( + `[grpc] Received ${type} export request`, + JSON.stringify(call.request), + ); + if (type === "traces") { + call.request.resourceSpans?.forEach((rSpans) => { + rSpans.scopeSpans.forEach((sSpans) => { + data.spans.push(...sSpans.spans); + }); + }); + } else if (type === "metrics") { + call.request.resourceMetrics?.forEach((rMetrics) => { + rMetrics.scopeMetrics.forEach((sMetrics) => { + data.metrics.push(...sMetrics.metrics); + }); + }); + } else if (type === "logs") { + call.request.resourceLogs?.forEach((rLogs) => { + rLogs.scopeLogs.forEach((sLogs) => { + data.logs.push(...sLogs.logRecords); + }); + }); + } + callback(null, { partialSuccess: {} }); +} + +async function startGrpcServer(port, onReady) { + // Ensure proto files are present before loading + let otlp = await ensureProtoFiles().then(() => { + const packageDefinition = protoLoader.loadSync([ + `opentelemetry/proto/collector/trace/v1/trace_service.proto`, + `opentelemetry/proto/collector/metrics/v1/metrics_service.proto`, + `opentelemetry/proto/collector/logs/v1/logs_service.proto`, + ], { + includeDirs: [ + `./opentelemetry-proto/opentelemetry-proto-${opentelemetryProtoTag}`, + ], + keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true, + }); + return grpc.loadPackageDefinition(packageDefinition).opentelemetry.proto; + }); + const server = new grpc.Server(); + // Register minimal OTLP services + server.addService(otlp.collector.trace.v1.TraceService.service, { + Export: (call, callback) => handleOtlpRequest(call, callback, "traces"), + }); + server.addService(otlp.collector.metrics.v1.MetricsService.service, { + Export: (call, callback) => handleOtlpRequest(call, callback, "metrics"), + }); + server.addService(otlp.collector.logs.v1.LogsService.service, { + Export: (call, callback) => handleOtlpRequest(call, callback, "logs"), + }); + /// Read TLS key and cert files (same as HTTP server) + // const key = Deno.readTextFileSync("../../../testdata/tls/localhost.key"); + // const cert = Deno.readTextFileSync("../../../testdata/tls/localhost.crt"); + // const keyBuf = Buffer.from(key); + // const certBuf = Buffer.from(cert); + // const creds = grpc.ServerCredentials.createSsl( + // null, + // [{ private_key: keyBuf, cert_chain: certBuf }], + // true, + // ); + /// Error: Not implemented: http2.createSecureServer + /// Use insecure credentials for gRPC in Deno (TLS not supported) + const creds = grpc.ServerCredentials.createInsecure(); + server.bindAsync( + `0.0.0.0:${port}`, + creds, + (err, actualPort) => { + if (err) throw err; + console.log(`[grpc] Server listening on port ${actualPort}`); + onReady(actualPort, server); + }, + ); +} + async function handler(req) { const body = await req.json(); body.resourceLogs?.forEach((rLogs) => { @@ -26,100 +145,189 @@ async function handler(req) { return Response.json({ partialSuccess: {} }, { status: 200 }); } -let server; +const protocol = Deno.env.get("OTEL_EXPORTER_OTLP_PROTOCOL")?.toLowerCase(); -function onListen({ port }) { - const command = new Deno.Command(Deno.execPath(), { - args: [ - "run", - "--env-file=env_file", - "-A", - "-q", - Deno.args[0], - ], - env: { - // rest of env is in env_file - OTEL_EXPORTER_OTLP_ENDPOINT: `https://localhost:${port}`, - }, - stdout: "null", - }); - const child = command.spawn(); - child.status - .then((status) => { - if (status.signal) { - throw new Error("child process failed: " + JSON.stringify(status)); - } - return server.shutdown(); - }) - .then(() => { - data.logs.sort((a, b) => - Number( - BigInt(a.observedTimeUnixNano) - BigInt(b.observedTimeUnixNano), - ) - ); - data.spans.sort((a, b) => - Number(BigInt(`0x${a.spanId}`) - BigInt(`0x${b.spanId}`)) - ); - // v8js metrics are non-deterministic - data.metrics = data.metrics.filter((m) => !m.name.startsWith("v8js")); - data.metrics.sort((a, b) => a.name.localeCompare(b.name)); - for (const metric of data.metrics) { - if ("histogram" in metric) { - metric.histogram.dataPoints.sort((a, b) => { - const aKey = a.attributes - .sort((x, y) => x.key.localeCompare(y.key)) - .map(({ key, value }) => `${key}:${JSON.stringify(value)}`) - .join("|"); - const bKey = b.attributes - .sort((x, y) => x.key.localeCompare(y.key)) - .map(({ key, value }) => `${key}:${JSON.stringify(value)}`) - .join("|"); - return aKey.localeCompare(bKey); - }); - - for (const dataPoint of metric.histogram.dataPoints) { - dataPoint.attributes.sort((a, b) => { - return a.key.localeCompare(b.key); - }); +// Only run the necessary collector server +switch (protocol) { + case "grpc": { + startGrpcServer(0, async (port, server) => { + const command = new Deno.Command(Deno.execPath(), { + args: [ + "run", + "--env-file=env_file", + "-A", + "-q", + Deno.args[0], + ], + env: { + // rest of env is in env_file + OTEL_EXPORTER_OTLP_ENDPOINT: `http://localhost:${port}`, + }, + stdout: "null", + }); + const child = command.spawn(); + child.status + .then((status) => { + if (status.signal) { + throw new Error("child process failed: " + JSON.stringify(status)); } - } - if ("sum" in metric) { - metric.sum.dataPoints.sort((a, b) => { - const aKey = a.attributes - .sort((x, y) => x.key.localeCompare(y.key)) - .map(({ key, value }) => `${key}:${JSON.stringify(value)}`) - .join("|"); - const bKey = b.attributes - .sort((x, y) => x.key.localeCompare(y.key)) - .map(({ key, value }) => `${key}:${JSON.stringify(value)}`) - .join("|"); - return aKey.localeCompare(bKey); - }); + server.tryShutdown(() => {}); + }) + .then(() => { + data.logs.sort((a, b) => + Number( + BigInt(a.observedTimeUnixNano) - BigInt(b.observedTimeUnixNano), + ) + ); + data.spans.sort((a, b) => + Number(BigInt(`0x${a.spanId}`) - BigInt(`0x${b.spanId}`)) + ); + // v8js metrics are non-deterministic + data.metrics = data.metrics.filter((m) => !m.name.startsWith("v8js")); + data.metrics.sort((a, b) => a.name.localeCompare(b.name)); + for (const metric of data.metrics) { + if ("histogram" in metric) { + metric.histogram.dataPoints.sort((a, b) => { + const aKey = a.attributes + .sort((x, y) => x.key.localeCompare(y.key)) + .map(({ key, value }) => `${key}:${JSON.stringify(value)}`) + .join("|"); + const bKey = b.attributes + .sort((x, y) => x.key.localeCompare(y.key)) + .map(({ key, value }) => `${key}:${JSON.stringify(value)}`) + .join("|"); + return aKey.localeCompare(bKey); + }); - for (const dataPoint of metric.sum.dataPoints) { - dataPoint.attributes.sort((a, b) => { - return a.key.localeCompare(b.key); - }); + for (const dataPoint of metric.histogram.dataPoints) { + dataPoint.attributes.sort((a, b) => { + return a.key.localeCompare(b.key); + }); + } + } + if ("sum" in metric) { + metric.sum.dataPoints.sort((a, b) => { + const aKey = a.attributes + .sort((x, y) => x.key.localeCompare(y.key)) + .map(({ key, value }) => `${key}:${JSON.stringify(value)}`) + .join("|"); + const bKey = b.attributes + .sort((x, y) => x.key.localeCompare(y.key)) + .map(({ key, value }) => `${key}:${JSON.stringify(value)}`) + .join("|"); + return aKey.localeCompare(bKey); + }); + + for (const dataPoint of metric.sum.dataPoints) { + dataPoint.attributes.sort((a, b) => { + return a.key.localeCompare(b.key); + }); + } + } } - } - } - console.log(JSON.stringify(data, null, 2)); + console.log(JSON.stringify(data, null, 2)); + }); }); -} + break; + } + case "http/protobuf": + case "http/json": { + let server; + function onListen({ port }) { + const command = new Deno.Command(Deno.execPath(), { + args: [ + "run", + "--env-file=env_file", + "-A", + "-q", + Deno.args[0], + ], + env: { + OTEL_EXPORTER_OTLP_ENDPOINT: `https://localhost:${port}`, + }, + stdout: "null", + }); + const child = command.spawn(); + child.status + .then((status) => { + if (status.signal) { + throw new Error("child process failed: " + JSON.stringify(status)); + } + server.shutdown(); + }) + .then(() => { + data.logs.sort((a, b) => + Number( + BigInt(a.observedTimeUnixNano) - BigInt(b.observedTimeUnixNano), + ) + ); + data.spans.sort((a, b) => + Number(BigInt(`0x${a.spanId}`) - BigInt(`0x${b.spanId}`)) + ); + // v8js metrics are non-deterministic + data.metrics = data.metrics.filter((m) => !m.name.startsWith("v8js")); + data.metrics.sort((a, b) => a.name.localeCompare(b.name)); + for (const metric of data.metrics) { + if ("histogram" in metric) { + metric.histogram.dataPoints.sort((a, b) => { + const aKey = a.attributes + .sort((x, y) => x.key.localeCompare(y.key)) + .map(({ key, value }) => `${key}:${JSON.stringify(value)}`) + .join("|"); + const bKey = b.attributes + .sort((x, y) => x.key.localeCompare(y.key)) + .map(({ key, value }) => `${key}:${JSON.stringify(value)}`) + .join("|"); + return aKey.localeCompare(bKey); + }); -if (Deno.env.get("OTEL_DENO_VSOCK")) { - server = Deno.serve({ - cid: -1, - port: 4317, - onListen, - handler, - }); -} else { - server = Deno.serve({ - key: Deno.readTextFileSync("../../../testdata/tls/localhost.key"), - cert: Deno.readTextFileSync("../../../testdata/tls/localhost.crt"), - port: 0, - onListen, - handler, - }); + for (const dataPoint of metric.histogram.dataPoints) { + dataPoint.attributes.sort((a, b) => { + return a.key.localeCompare(b.key); + }); + } + } + if ("sum" in metric) { + metric.sum.dataPoints.sort((a, b) => { + const aKey = a.attributes + .sort((x, y) => x.key.localeCompare(y.key)) + .map(({ key, value }) => `${key}:${JSON.stringify(value)}`) + .join("|"); + const bKey = b.attributes + .sort((x, y) => x.key.localeCompare(y.key)) + .map(({ key, value }) => `${key}:${JSON.stringify(value)}`) + .join("|"); + return aKey.localeCompare(bKey); + }); + + for (const dataPoint of metric.sum.dataPoints) { + dataPoint.attributes.sort((a, b) => { + return a.key.localeCompare(b.key); + }); + } + } + } + console.log(JSON.stringify(data, null, 2)); + }); + } + if (Deno.env.get("OTEL_DENO_VSOCK")) { + server = Deno.serve({ + cid: -1, + port: 4317, + onListen, + handler, + }); + } else { + server = Deno.serve({ + key: Deno.readTextFileSync("../../../testdata/tls/localhost.key"), + cert: Deno.readTextFileSync("../../../testdata/tls/localhost.crt"), + port: 0, + onListen, + handler, + }); + } + break; + } + default: + throw new Error(`Unknown OTEL_EXPORTER_OTLP_PROTOCOL: ${protocol}`); }