From 987cd13fe331d9b4d455b97f9d2d34a453d85f65 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Jul 2026 21:12:14 +0000 Subject: [PATCH 1/4] feat: accept OTLP on default OpenTelemetry ports (4318 HTTP, 4317 gRPC) The extension previously accepted OTLP data from the runtime only over HTTP on the proxy listener port (9009, DASH0_LISTENER_PORT), which also serves the Lambda Runtime API proxy. It now additionally listens on the default OpenTelemetry ports: - 4318 (DASH0_OTLP_HTTP_PORT): dedicated OTLP/HTTP listener serving only the /v1/traces, /v1/logs and /v1/metrics ingest routes - 4317 (DASH0_OTLP_GRPC_PORT): new OTLP/gRPC receiver (tonic) for the trace, logs and metrics services, feeding the same processing pipeline The receivers were refactored so the transport-independent payload handling is shared between the HTTP handlers and the gRPC services. OTLP/HTTP on port 9009 keeps working for backwards compatibility. The runtime distros (Node, Python, Java) and the manual-instrumentation docs now point to port 4318. The manual-instrumentation integration test lambda is deployed in two variants exercising both OTLP/HTTP (4318) and OTLP/gRPC (4317). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01H3abARSdGW8Edy6LT8CBdP --- Cargo.lock | 61 +++- Cargo.toml | 3 +- README.md | 6 +- .../iac/lambdas/manual/package-lock.json | 337 +++++++++++++++++- .../iac/lambdas/manual/package.json | 2 + .../iac/lambdas/manual/tracing.js | 47 ++- .../iac/lib/integration-tests-stack.ts | 44 ++- .../tests/src/test-manual-node.test.ts | 24 +- .../io/dash0/javaagent/Dash0Configurator.java | 2 +- opt/node/distro/src/constants.ts | 2 +- .../src/dash0_opentelemetry/__init__.py | 2 +- src/config/endpoints.rs | 22 ++ src/main.rs | 89 +++-- src/otlp/grpc_receiver.rs | 132 +++++++ src/otlp/logs_receiver.rs | 37 +- src/otlp/metrics_receiver.rs | 37 +- src/otlp/mod.rs | 1 + src/otlp/receiver.rs | 39 +- src/route.rs | 37 ++ 19 files changed, 803 insertions(+), 121 deletions(-) create mode 100644 src/otlp/grpc_receiver.rs diff --git a/Cargo.lock b/Cargo.lock index 6765d99b..c2d4f010 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -62,7 +62,7 @@ dependencies = [ "hyper", "hyper-rustls", "hyper-util", - "matchit", + "matchit 0.7.3", "once_cell", "opentelemetry-proto", "parking_lot", @@ -75,6 +75,7 @@ dependencies = [ "sha2", "tokio", "tokio-test", + "tonic", "tracing", "tracing-subscriber", ] @@ -101,6 +102,49 @@ dependencies = [ "fs_extra", ] +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "itoa", + "matchit 0.8.4", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "sync_wrapper", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", +] + [[package]] name = "base64" version = "0.22.1" @@ -643,12 +687,24 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "memchr" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "mio" version = "1.2.0" @@ -1319,8 +1375,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fec7c61a0695dc1887c1b53952990f3ad2e3a31453e1f49f10e75424943a93ec" dependencies = [ "async-trait", + "axum", "base64", "bytes", + "h2", "http", "http-body", "http-body-util", @@ -1329,6 +1387,7 @@ dependencies = [ "hyper-util", "percent-encoding", "pin-project", + "socket2", "sync_wrapper", "tokio", "tokio-stream", diff --git a/Cargo.toml b/Cargo.toml index 45b5abd9..c79576b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,8 @@ bytes = "1" hyper-rustls = { version = "0.27", features = ["http1", "native-tokio", "tls12", "ring"] } rustls = { version = "0.23", default-features = false, features = ["ring"] } matchit = "0.7" -opentelemetry-proto = { version = "0.31", features = ["trace", "logs", "metrics", "with-serde"] } +opentelemetry-proto = { version = "0.31", features = ["trace", "logs", "metrics", "with-serde", "gen-tonic"] } +tonic = "0.14" chrono = { version = "0.4", features = ["serde"] } prost = "0.14.1" serde = { version = "1", features = ["derive"] } diff --git a/README.md b/README.md index 56b54764..beb03c4d 100644 --- a/README.md +++ b/README.md @@ -125,10 +125,12 @@ The following environment variables allow fine-grained control over secret maski If you prefer to set up OpenTelemetry instrumentation yourself instead of relying on the extension's auto-instrumentation, you can use the manual layer and point your OTLP exporters to the extension's local endpoint. The extension will receive the telemetry, enrich it, and forward it to Dash0. 1. Add the manual layer to your Lambda function: `arn:aws:lambda::115813213817:layer:dash0-extension-manual:`. -2. Configure your OTLP trace exporter to send to `http://127.0.0.1:9009/v1/traces`. -3. If exporting metrics, configure your OTLP metric exporter to send to `http://127.0.0.1:9009/v1/metrics`. +2. Configure your OTLP trace exporter to send to `http://127.0.0.1:4318/v1/traces` (OTLP/HTTP) or `http://127.0.0.1:4317` (OTLP/gRPC). +3. If exporting metrics, configure your OTLP metric exporter to send to `http://127.0.0.1:4318/v1/metrics` (OTLP/HTTP) or `http://127.0.0.1:4317` (OTLP/gRPC). 4. Make sure to flush all telemetry before the Lambda invocation completes (e.g., in a response hook or before returning the response). +The extension accepts OTLP on the default OpenTelemetry ports: `4318` for OTLP/HTTP and `4317` for OTLP/gRPC (overridable via the `DASH0_OTLP_HTTP_PORT` and `DASH0_OTLP_GRPC_PORT` environment variables). For backwards compatibility, OTLP/HTTP is also still accepted on the extension's proxy port (`9009`). + ## Enrichment Attributes diff --git a/integration-tests/iac/lambdas/manual/package-lock.json b/integration-tests/iac/lambdas/manual/package-lock.json index 04087949..a7095bd5 100644 --- a/integration-tests/iac/lambdas/manual/package-lock.json +++ b/integration-tests/iac/lambdas/manual/package-lock.json @@ -10,7 +10,9 @@ "license": "ISC", "dependencies": { "@opentelemetry/api": "^1.9.0", + "@opentelemetry/exporter-metrics-otlp-grpc": "^0.211.0", "@opentelemetry/exporter-metrics-otlp-http": "^0.211.0", + "@opentelemetry/exporter-trace-otlp-grpc": "^0.211.0", "@opentelemetry/exporter-trace-otlp-http": "^0.211.0", "@opentelemetry/instrumentation": "^0.211.0", "@opentelemetry/instrumentation-aws-lambda": "^0.63.0", @@ -21,6 +23,70 @@ "@opentelemetry/semantic-conventions": "^1.39.0" } }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.4.tgz", + "integrity": "sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.1.tgz", + "integrity": "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader/node_modules/protobufjs": { + "version": "7.6.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz", + "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, "node_modules/@opentelemetry/api": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", @@ -69,6 +135,28 @@ "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, + "node_modules/@opentelemetry/exporter-metrics-otlp-grpc": { + "version": "0.211.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.211.0.tgz", + "integrity": "sha512-D/U3G8L4PzZp8ot5hX9wpgbTymgtLZCiwR7heMe4LsbGV4OdctS1nfyvaQHLT6CiGZ6FjKc1Vk9s6kbo9SWLXQ==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.7.1", + "@opentelemetry/core": "2.5.0", + "@opentelemetry/exporter-metrics-otlp-http": "0.211.0", + "@opentelemetry/otlp-exporter-base": "0.211.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.211.0", + "@opentelemetry/otlp-transformer": "0.211.0", + "@opentelemetry/resources": "2.5.0", + "@opentelemetry/sdk-metrics": "2.5.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, "node_modules/@opentelemetry/exporter-metrics-otlp-http": { "version": "0.211.0", "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.211.0.tgz", @@ -88,6 +176,27 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/exporter-trace-otlp-grpc": { + "version": "0.211.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.211.0.tgz", + "integrity": "sha512-eFwx4Gvu6LaEiE1rOd4ypgAiWEdZu7Qzm2QNN2nJqPW1XDeAVH1eNwVcVQl+QK9HR/JCDZ78PZgD7xD/DBDqbw==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.7.1", + "@opentelemetry/core": "2.5.0", + "@opentelemetry/otlp-exporter-base": "0.211.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.211.0", + "@opentelemetry/otlp-transformer": "0.211.0", + "@opentelemetry/resources": "2.5.0", + "@opentelemetry/sdk-trace-base": "2.5.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, "node_modules/@opentelemetry/exporter-trace-otlp-http": { "version": "0.211.0", "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.211.0.tgz", @@ -157,6 +266,24 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/otlp-grpc-exporter-base": { + "version": "0.211.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.211.0.tgz", + "integrity": "sha512-mR5X+N4SuphJeb7/K7y0JNMC8N1mB6gEtjyTLv+TSAhl0ZxNQzpSKP8S5Opk90fhAqVYD4R0SQSAirEBlH1KSA==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.7.1", + "@opentelemetry/core": "2.5.0", + "@opentelemetry/otlp-exporter-base": "0.211.0", + "@opentelemetry/otlp-transformer": "0.211.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, "node_modules/@opentelemetry/otlp-transformer": { "version": "0.211.0", "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.211.0.tgz", @@ -283,25 +410,24 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", "license": "BSD-3-Clause", "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" + "@protobufjs/aspromise": "^1.1.1" } }, "node_modules/@protobufjs/float": { @@ -329,9 +455,9 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", "license": "BSD-3-Clause" }, "node_modules/@types/aws-lambda": { @@ -370,12 +496,68 @@ "acorn": "^8" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/cjs-module-lexer": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -393,6 +575,30 @@ } } }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/import-in-the-middle": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-2.0.5.tgz", @@ -405,6 +611,21 @@ "module-details-from-path": "^1.0.4" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, "node_modules/long": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", @@ -447,6 +668,15 @@ "node": ">=12.0.0" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-in-the-middle": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz", @@ -460,11 +690,90 @@ "node": ">=9.3.0 || >=8.10.0 <9.0.0" } }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } } } } diff --git a/integration-tests/iac/lambdas/manual/package.json b/integration-tests/iac/lambdas/manual/package.json index e9aae413..5f14e96c 100644 --- a/integration-tests/iac/lambdas/manual/package.json +++ b/integration-tests/iac/lambdas/manual/package.json @@ -13,6 +13,7 @@ "description": "", "dependencies": { "@opentelemetry/api": "^1.9.0", + "@opentelemetry/exporter-trace-otlp-grpc": "^0.211.0", "@opentelemetry/exporter-trace-otlp-http": "^0.211.0", "@opentelemetry/instrumentation": "^0.211.0", "@opentelemetry/instrumentation-aws-lambda": "^0.63.0", @@ -21,6 +22,7 @@ "@opentelemetry/sdk-trace-node": "^2.5.0", "@opentelemetry/semantic-conventions": "^1.39.0", "@opentelemetry/sdk-metrics": "^2.5.0", + "@opentelemetry/exporter-metrics-otlp-grpc": "^0.211.0", "@opentelemetry/exporter-metrics-otlp-http": "^0.211.0" } } diff --git a/integration-tests/iac/lambdas/manual/tracing.js b/integration-tests/iac/lambdas/manual/tracing.js index 49b2d595..f6ef2e7c 100644 --- a/integration-tests/iac/lambdas/manual/tracing.js +++ b/integration-tests/iac/lambdas/manual/tracing.js @@ -4,22 +4,49 @@ console.log('[tracing] Loading tracing module'); const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node'); const { SimpleSpanProcessor } = require('@opentelemetry/sdk-trace-base'); -const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http'); const { resourceFromAttributes } = require('@opentelemetry/resources'); const { ATTR_SERVICE_NAME } = require('@opentelemetry/semantic-conventions'); const { AwsLambdaInstrumentation } = require('@opentelemetry/instrumentation-aws-lambda'); const { registerInstrumentations } = require('@opentelemetry/instrumentation'); const { MeterProvider, PeriodicExportingMetricReader } = require('@opentelemetry/sdk-metrics'); -const { OTLPMetricExporter } = require('@opentelemetry/exporter-metrics-otlp-http'); + +// The extension accepts OTLP on the default OpenTelemetry ports: +// 4318 for OTLP/HTTP and 4317 for OTLP/gRPC. +const protocol = process.env.OTLP_PROTOCOL || 'http'; +console.log(`[tracing] Using OTLP protocol: ${protocol}`); + +let exporter; +let metricExporter; +if (protocol === 'grpc') { + const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-grpc'); + const { OTLPMetricExporter } = require('@opentelemetry/exporter-metrics-otlp-grpc'); + exporter = new OTLPTraceExporter({ + url: `http://127.0.0.1:4317`, + }); + metricExporter = new OTLPMetricExporter({ + url: `http://127.0.0.1:4317`, + headers: { + Authorization: `Bearer ${process.env.DASH0_TOKEN}`, + }, + }); +} else { + const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http'); + const { OTLPMetricExporter } = require('@opentelemetry/exporter-metrics-otlp-http'); + exporter = new OTLPTraceExporter({ + url: `http://127.0.0.1:4318/v1/traces`, + }); + metricExporter = new OTLPMetricExporter({ + url: `http://127.0.0.1:4318/v1/metrics`, + headers: { + Authorization: `Bearer ${process.env.DASH0_TOKEN}`, + }, + }); +} const resource = resourceFromAttributes({ [ATTR_SERVICE_NAME]: process.env.AWS_LAMBDA_FUNCTION_NAME || 'lambda-metrics', }); -const exporter = new OTLPTraceExporter({ - url: `http://127.0.0.1:9009/v1/traces`, -}); - const provider = new NodeTracerProvider({ resource, spanProcessors: [new SimpleSpanProcessor(exporter)], @@ -27,14 +54,6 @@ const provider = new NodeTracerProvider({ provider.register(); -// Metrics setup -const metricExporter = new OTLPMetricExporter({ - url: `http://127.0.0.1:9009/v1/metrics`, - headers: { - Authorization: `Bearer ${process.env.DASH0_TOKEN}`, - }, -}); - const meterProvider = new MeterProvider({ resource, readers: [ diff --git a/integration-tests/iac/lib/integration-tests-stack.ts b/integration-tests/iac/lib/integration-tests-stack.ts index 11a94dda..446bc8be 100644 --- a/integration-tests/iac/lib/integration-tests-stack.ts +++ b/integration-tests/iac/lib/integration-tests-stack.ts @@ -328,25 +328,31 @@ class ManualStack extends cdk.NestedStack { }); for (const runtime of runtimes) { const runtimeName = runtime.name.replace(/\./g, '-'); - new lambda.Function(this, `manual-instrumentation-${runtimeName}`, { - functionName: `${props.prefix}manual-instrumentation-${runtimeName}`, - runtime, - memorySize: 512, - handler: 'index.hello', - architecture: lambda.Architecture.X86_64, - timeout: cdk.Duration.seconds(10), - code, - layers: [props.layer], - role: props.role, - environment: { - AWS_LAMBDA_EXEC_WRAPPER: "/opt/wrapper", - DASH0_TOKEN: process.env.DASH0_DEV_API_TOKEN!, - DASH0_ENDPOINT: "https://ingress.eu-west-1.aws.dash0-dev.com:4318", - DASH0_EXTENSION_LOG_LEVEL: "info", - }, - logGroup: props.logGroup, - loggingFormat: lambda.LoggingFormat.TEXT, - }); + // One function per OTLP protocol: 'http' exports to the default + // OTLP/HTTP port (4318), 'grpc' to the default OTLP/gRPC port (4317). + for (const otlpProtocol of ['http', 'grpc']) { + const nameSuffix = otlpProtocol === 'http' ? '' : `-${otlpProtocol}`; + new lambda.Function(this, `manual-instrumentation${nameSuffix}-${runtimeName}`, { + functionName: `${props.prefix}manual-instrumentation${nameSuffix}-${runtimeName}`, + runtime, + memorySize: 512, + handler: 'index.hello', + architecture: lambda.Architecture.X86_64, + timeout: cdk.Duration.seconds(10), + code, + layers: [props.layer], + role: props.role, + environment: { + AWS_LAMBDA_EXEC_WRAPPER: "/opt/wrapper", + DASH0_TOKEN: process.env.DASH0_DEV_API_TOKEN!, + DASH0_ENDPOINT: "https://ingress.eu-west-1.aws.dash0-dev.com:4318", + DASH0_EXTENSION_LOG_LEVEL: "info", + OTLP_PROTOCOL: otlpProtocol, + }, + logGroup: props.logGroup, + loggingFormat: lambda.LoggingFormat.TEXT, + }); + } } } } diff --git a/integration-tests/tests/src/test-manual-node.test.ts b/integration-tests/tests/src/test-manual-node.test.ts index 0b7dd7a1..74833e0e 100644 --- a/integration-tests/tests/src/test-manual-node.test.ts +++ b/integration-tests/tests/src/test-manual-node.test.ts @@ -47,15 +47,21 @@ const verifyManualInstrumentation = async (functionName: string) => { describe.concurrent('Manual instrumentation Lambda', () => { const runtimes = NODE_RUNTIMES; + // 'http' exports OTLP to the default OTLP/HTTP port (4318), + // 'grpc' to the default OTLP/gRPC port (4317). + const otlpProtocols = ['http', 'grpc']; for (const runtime of runtimes) { - const functionName = `${RESOURCE_PREFIX}manual-instrumentation-${runtime}` - it( - `invokes ${functionName} and receives trace`, - async () => { - console.log(`Starting test for ${functionName}`, new Date().toISOString()); - await verifyManualInstrumentation(functionName); - }, - TEST_TIMEOUT_MS - ); + for (const otlpProtocol of otlpProtocols) { + const nameSuffix = otlpProtocol === 'http' ? '' : `-${otlpProtocol}`; + const functionName = `${RESOURCE_PREFIX}manual-instrumentation${nameSuffix}-${runtime}` + it( + `invokes ${functionName} and receives trace (OTLP via ${otlpProtocol})`, + async () => { + console.log(`Starting test for ${functionName}`, new Date().toISOString()); + await verifyManualInstrumentation(functionName); + }, + TEST_TIMEOUT_MS + ); + } } }); diff --git a/opt/java/opentelemetry-java-distro/custom/src/main/java/io/dash0/javaagent/Dash0Configurator.java b/opt/java/opentelemetry-java-distro/custom/src/main/java/io/dash0/javaagent/Dash0Configurator.java index 5d93ded1..9fe4df00 100644 --- a/opt/java/opentelemetry-java-distro/custom/src/main/java/io/dash0/javaagent/Dash0Configurator.java +++ b/opt/java/opentelemetry-java-distro/custom/src/main/java/io/dash0/javaagent/Dash0Configurator.java @@ -47,7 +47,7 @@ public class Dash0Configurator implements AutoConfigurationCustomizerProvider { public static final Logger LOGGER = Logger.getLogger(Dash0Configurator.class.getName()); public static final String DASH0_EXTENSION_ENDPOINT_URL = - "http://127.0.0.1:9009"; + "http://127.0.0.1:4318"; @Override public void customize(AutoConfigurationCustomizer autoConfiguration) { diff --git a/opt/node/distro/src/constants.ts b/opt/node/distro/src/constants.ts index 11d1c634..d1ff01fe 100644 --- a/opt/node/distro/src/constants.ts +++ b/opt/node/distro/src/constants.ts @@ -1,4 +1,4 @@ export const DASH0_LOGGING_NAMESPACE = '@dash0/opentelemetry'; export const DEFAULT_DASH0_EXTENSION_ENDPOINT = - 'http://127.0.0.1:9009/v1/traces'; + 'http://127.0.0.1:4318/v1/traces'; diff --git a/opt/python/distro/src/dash0_opentelemetry/__init__.py b/opt/python/distro/src/dash0_opentelemetry/__init__.py index a3e39205..ed42e19a 100644 --- a/opt/python/distro/src/dash0_opentelemetry/__init__.py +++ b/opt/python/distro/src/dash0_opentelemetry/__init__.py @@ -102,7 +102,7 @@ def init() -> Dict[str, Any]: from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.sdk.trace import SpanLimits, TracerProvider - DASH0_EXTENSION_ENDPOINT = "http://127.0.0.1:9009/v1/traces" + DASH0_EXTENSION_ENDPOINT = "http://127.0.0.1:4318/v1/traces" traces_endpoint = os.getenv("DASH0_EXTENSION_ENDPOINT", DASH0_EXTENSION_ENDPOINT) dash0_token = os.getenv("DASH0_TOKEN") diff --git a/src/config/endpoints.rs b/src/config/endpoints.rs index 8eef4425..6a0654c1 100644 --- a/src/config/endpoints.rs +++ b/src/config/endpoints.rs @@ -16,6 +16,28 @@ pub fn extension_port() -> u16 { .unwrap_or(crate::DEFAULT_PROXY_PORT) } +/// Get the OTLP/HTTP receiver port from environment variable or use the +/// default OpenTelemetry HTTP port (4318) +pub fn otlp_http_port() -> u16 { + use std::env::var; + + var("DASH0_OTLP_HTTP_PORT") + .ok() + .and_then(|v| v.parse::().ok()) + .unwrap_or(crate::DEFAULT_OTLP_HTTP_PORT) +} + +/// Get the OTLP/gRPC receiver port from environment variable or use the +/// default OpenTelemetry gRPC port (4317) +pub fn otlp_grpc_port() -> u16 { + use std::env::var; + + var("DASH0_OTLP_GRPC_PORT") + .ok() + .and_then(|v| v.parse::().ok()) + .unwrap_or(crate::DEFAULT_OTLP_GRPC_PORT) +} + /// Latch in the API endpoints defined in ENV variables /// #[allow(dead_code)] diff --git a/src/main.rs b/src/main.rs index 8be3c786..beae8c2f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,6 +28,12 @@ pub const EXTENSION_NAME: &str = "dash0"; /// Default port to listen on, overriden by DASH0_LISTENER_PORT environment variable pub const DEFAULT_PROXY_PORT: u16 = 9009; +/// Default OpenTelemetry OTLP/HTTP port, overriden by DASH0_OTLP_HTTP_PORT environment variable +pub const DEFAULT_OTLP_HTTP_PORT: u16 = 4318; + +/// Default OpenTelemetry OTLP/gRPC port, overriden by DASH0_OTLP_GRPC_PORT environment variable +pub const DEFAULT_OTLP_GRPC_PORT: u16 = 4317; + pub static LAMBDA_RUNTIME_API_VERSION: &str = "2018-06-01"; /// Returns the log prefix for standard log messages @@ -90,20 +96,70 @@ async fn main() { ); } }; + + // Runtime API proxy, plus OTLP/HTTP for backwards compatibility. + let proxy_listener = bind_listener(addr).await; + let server_join_handle = spawn_http1_server(proxy_listener, route::dispatch); + + // Dedicated OTLP receivers on the default OpenTelemetry ports. + let otlp_http_addr = SocketAddr::from(([0, 0, 0, 0], config::endpoints::otlp_http_port())); + let otlp_http_listener = bind_listener(otlp_http_addr).await; + spawn_http1_server(otlp_http_listener, route::dispatch_otlp); + + let otlp_grpc_addr = SocketAddr::from(([0, 0, 0, 0], config::endpoints::otlp_grpc_port())); + tokio::spawn(otlp::grpc_receiver::serve(otlp_grpc_addr)); + + // Initialize the extension and continually get next extension event. + tokio::task::spawn(async { + extension::register::register().await; + extension::register::register_telemetry().await; + // Lambda Application runtime will start once our extension is registered + stats::app_start(); + + loop { + // Lambda Extension API requires we wait for next extension event + extension::events::get_next().await; + } + }); + + if let Err(e) = server_join_handle.await { + tracing::error!( + "[{}] Failed to join server task: {}", + crate::log_prefix(), + e + ); + } +} + +async fn bind_listener(addr: SocketAddr) -> TcpListener { tracing::info!("[{}] listening on {}", crate::log_prefix(), addr); - let listener = match TcpListener::bind(addr).await { + match TcpListener::bind(addr).await { Ok(listener) => listener, Err(e) => { - tracing::error!("[{}] Failed to bind listener: {}", crate::log_prefix(), e); + tracing::error!( + "[{}] Failed to bind listener on {}: {}", + crate::log_prefix(), + addr, + e + ); panic!( "[{}] Cannot start without bound listener", crate::log_prefix() ); } - }; + } +} - let server_join_handle = tokio::spawn(async move { +fn spawn_http1_server(listener: TcpListener, handler: F) -> tokio::task::JoinHandle<()> +where + F: Fn(hyper::Request) -> Fut + Copy + Send + 'static, + Fut: std::future::Future< + Output = Result, std::convert::Infallible>, + > + Send + + 'static, +{ + tokio::spawn(async move { loop { let (stream, _peer) = match listener.accept().await { Ok(pair) => pair, @@ -115,33 +171,12 @@ async fn main() { let io = TokioIo::new(stream); tokio::spawn(async move { if let Err(e) = http1::Builder::new() - .serve_connection(io, service_fn(route::dispatch)) + .serve_connection(io, service_fn(handler)) .await { tracing::debug!("[{}] connection error: {}", crate::log_prefix(), e); } }); } - }); - - // Initialize the extension and continually get next extension event. - tokio::task::spawn(async { - extension::register::register().await; - extension::register::register_telemetry().await; - // Lambda Application runtime will start once our extension is registered - stats::app_start(); - - loop { - // Lambda Extension API requires we wait for next extension event - extension::events::get_next().await; - } - }); - - if let Err(e) = server_join_handle.await { - tracing::error!( - "[{}] Failed to join server task: {}", - crate::log_prefix(), - e - ); - } + }) } diff --git a/src/otlp/grpc_receiver.rs b/src/otlp/grpc_receiver.rs new file mode 100644 index 00000000..6ecb0c05 --- /dev/null +++ b/src/otlp/grpc_receiver.rs @@ -0,0 +1,132 @@ +//! OTLP/gRPC receiver. +//! +//! Accepts telemetry from the runtime on the default OpenTelemetry gRPC port +//! (4317) and feeds it into the same processing pipeline as the OTLP/HTTP +//! receivers. Payloads are re-encoded as protobuf and stored as if they had +//! been received on the corresponding OTLP/HTTP path, since the exporter +//! forwards them to the backend via OTLP/HTTP. + +use std::net::SocketAddr; + +use bytes::Bytes; +use hyper::header::{HeaderMap, HeaderValue, CONTENT_TYPE}; +use hyper::Method; +use opentelemetry_proto::tonic::collector::logs::v1::logs_service_server::{ + LogsService, LogsServiceServer, +}; +use opentelemetry_proto::tonic::collector::logs::v1::{ + ExportLogsServiceRequest, ExportLogsServiceResponse, +}; +use opentelemetry_proto::tonic::collector::metrics::v1::metrics_service_server::{ + MetricsService, MetricsServiceServer, +}; +use opentelemetry_proto::tonic::collector::metrics::v1::{ + ExportMetricsServiceRequest, ExportMetricsServiceResponse, +}; +use opentelemetry_proto::tonic::collector::trace::v1::trace_service_server::{ + TraceService, TraceServiceServer, +}; +use opentelemetry_proto::tonic::collector::trace::v1::{ + ExportTraceServiceRequest, ExportTraceServiceResponse, +}; +use prost::Message; +use tonic::{Request, Response, Status}; + +use crate::otlp::logs_receiver::handle_logs_payload; +use crate::otlp::metrics_receiver::handle_metrics_payload; +use crate::otlp::receiver::handle_traces_payload; + +/// Headers for a stored payload received via gRPC: the body is always +/// protobuf-encoded, matching what the OTLP/HTTP exporter sends upstream. +fn protobuf_headers() -> HeaderMap { + let mut headers = HeaderMap::new(); + headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/x-protobuf"), + ); + headers +} + +#[derive(Default)] +pub struct OtlpGrpcTraceService; + +#[tonic::async_trait] +impl TraceService for OtlpGrpcTraceService { + async fn export( + &self, + request: Request, + ) -> Result, Status> { + let body = request.into_inner().encode_to_vec(); + handle_traces_payload( + Method::POST, + "/v1/traces".to_string(), + protobuf_headers(), + Bytes::from(body), + ); + Ok(Response::new(ExportTraceServiceResponse::default())) + } +} + +#[derive(Default)] +pub struct OtlpGrpcLogsService; + +#[tonic::async_trait] +impl LogsService for OtlpGrpcLogsService { + async fn export( + &self, + request: Request, + ) -> Result, Status> { + let body = request.into_inner().encode_to_vec(); + handle_logs_payload( + Method::POST, + "/v1/logs".to_string(), + protobuf_headers(), + Bytes::from(body), + ); + Ok(Response::new(ExportLogsServiceResponse::default())) + } +} + +#[derive(Default)] +pub struct OtlpGrpcMetricsService; + +#[tonic::async_trait] +impl MetricsService for OtlpGrpcMetricsService { + async fn export( + &self, + request: Request, + ) -> Result, Status> { + let body = request.into_inner().encode_to_vec(); + handle_metrics_payload( + Method::POST, + "/v1/metrics".to_string(), + protobuf_headers(), + Bytes::from(body), + ); + Ok(Response::new(ExportMetricsServiceResponse::default())) + } +} + +/// Serves the OTLP/gRPC receiver on the given address. Only returns on +/// server failure. +pub async fn serve(addr: SocketAddr) { + tracing::info!( + "[{}] OTLP/gRPC receiver listening on {}", + crate::log_prefix(), + addr + ); + + if let Err(e) = tonic::transport::Server::builder() + .add_service(TraceServiceServer::new(OtlpGrpcTraceService)) + .add_service(LogsServiceServer::new(OtlpGrpcLogsService)) + .add_service(MetricsServiceServer::new(OtlpGrpcMetricsService)) + .serve(addr) + .await + { + tracing::error!("[{}] OTLP/gRPC receiver failed: {}", crate::log_prefix(), e); + panic!( + "[{}] Cannot continue without OTLP/gRPC receiver", + crate::log_prefix() + ); + } +} diff --git a/src/otlp/logs_receiver.rs b/src/otlp/logs_receiver.rs index 05ecf19f..d0f633ae 100644 --- a/src/otlp/logs_receiver.rs +++ b/src/otlp/logs_receiver.rs @@ -1,8 +1,10 @@ use std::time::Instant; +use bytes::Bytes; use http_body_util::BodyExt; use hyper::body::Incoming; -use hyper::{Request, Response}; +use hyper::header::HeaderMap; +use hyper::{Method, Request, Response}; use opentelemetry_proto::tonic::collector::logs::v1::ExportLogsServiceRequest; use prost::Message; @@ -10,10 +12,30 @@ use crate::route::{empty_body, ResBody}; use crate::state::invocation_data::{store_log, StoredLog}; pub async fn logs(req: Request) -> Result, hyper::Error> { - let start = Instant::now(); let (parts, body) = req.into_parts(); let body_bytes = body.collect().await?.to_bytes(); + let path_and_query = parts + .uri + .path_and_query() + .map(|pq| pq.as_str().to_string()) + .unwrap_or_else(|| "/".to_string()); + + handle_logs_payload(parts.method, path_and_query, parts.headers, body_bytes); + + Ok(Response::builder().status(200).body(empty_body()).unwrap()) +} + +/// Shared handling of an OTLP logs payload, independent of the transport +/// (OTLP/HTTP or OTLP/gRPC) it was received over. +pub fn handle_logs_payload( + method: Method, + path_and_query: String, + headers: HeaderMap, + body_bytes: Bytes, +) { + let start = Instant::now(); + let mut encoded_body: Vec = body_bytes.to_vec(); let mut converted_from_json = false; @@ -59,7 +81,7 @@ pub async fn logs(req: Request) -> Result, hyper::Er } } - let mut headers = parts.headers; + let mut headers = headers; if converted_from_json { headers.insert( hyper::header::CONTENT_TYPE, @@ -68,12 +90,8 @@ pub async fn logs(req: Request) -> Result, hyper::Er } store_log(StoredLog { - method: parts.method, - path_and_query: parts - .uri - .path_and_query() - .map(|pq| pq.as_str().to_string()) - .unwrap_or_else(|| "/".to_string()), + method, + path_and_query, headers, body: encoded_body, }); @@ -83,5 +101,4 @@ pub async fn logs(req: Request) -> Result, hyper::Er crate::log_prefix(), start.elapsed().as_millis(), ); - Ok(Response::builder().status(200).body(empty_body()).unwrap()) } diff --git a/src/otlp/metrics_receiver.rs b/src/otlp/metrics_receiver.rs index c83dc5cd..f6ffce68 100644 --- a/src/otlp/metrics_receiver.rs +++ b/src/otlp/metrics_receiver.rs @@ -1,8 +1,10 @@ use std::time::Instant; +use bytes::Bytes; use http_body_util::BodyExt; use hyper::body::Incoming; -use hyper::{Request, Response}; +use hyper::header::HeaderMap; +use hyper::{Method, Request, Response}; use opentelemetry_proto::tonic::collector::metrics::v1::ExportMetricsServiceRequest; use prost::Message; @@ -10,10 +12,30 @@ use crate::route::{empty_body, ResBody}; use crate::state::invocation_data::{store_metric, StoredMetric}; pub async fn metrics(req: Request) -> Result, hyper::Error> { - let start = Instant::now(); let (parts, body) = req.into_parts(); let body_bytes = body.collect().await?.to_bytes(); + let path_and_query = parts + .uri + .path_and_query() + .map(|pq| pq.as_str().to_string()) + .unwrap_or_else(|| "/".to_string()); + + handle_metrics_payload(parts.method, path_and_query, parts.headers, body_bytes); + + Ok(Response::builder().status(200).body(empty_body()).unwrap()) +} + +/// Shared handling of an OTLP metrics payload, independent of the transport +/// (OTLP/HTTP or OTLP/gRPC) it was received over. +pub fn handle_metrics_payload( + method: Method, + path_and_query: String, + headers: HeaderMap, + body_bytes: Bytes, +) { + let start = Instant::now(); + let mut encoded_body: Vec = body_bytes.to_vec(); let mut converted_from_json = false; @@ -59,7 +81,7 @@ pub async fn metrics(req: Request) -> Result, hyper: } } - let mut headers = parts.headers; + let mut headers = headers; if converted_from_json { headers.insert( hyper::header::CONTENT_TYPE, @@ -68,12 +90,8 @@ pub async fn metrics(req: Request) -> Result, hyper: } store_metric(StoredMetric { - method: parts.method, - path_and_query: parts - .uri - .path_and_query() - .map(|pq| pq.as_str().to_string()) - .unwrap_or_else(|| "/".to_string()), + method, + path_and_query, headers, body: encoded_body, }); @@ -83,5 +101,4 @@ pub async fn metrics(req: Request) -> Result, hyper: crate::log_prefix(), start.elapsed().as_millis(), ); - Ok(Response::builder().status(200).body(empty_body()).unwrap()) } diff --git a/src/otlp/mod.rs b/src/otlp/mod.rs index a31e4d89..a7213e19 100644 --- a/src/otlp/mod.rs +++ b/src/otlp/mod.rs @@ -2,6 +2,7 @@ pub const OTEL_SCHEMA_URL: &str = "https://opentelemetry.io/schemas/1.11.0"; pub mod attributes; pub mod exporter; +pub mod grpc_receiver; pub mod log_mutations; pub mod logs_receiver; pub mod masking; diff --git a/src/otlp/receiver.rs b/src/otlp/receiver.rs index 4612c9c4..7f67d486 100644 --- a/src/otlp/receiver.rs +++ b/src/otlp/receiver.rs @@ -1,8 +1,10 @@ use std::time::Instant; +use bytes::Bytes; use http_body_util::BodyExt; use hyper::body::Incoming; -use hyper::{Request, Response}; +use hyper::header::HeaderMap; +use hyper::{Method, Request, Response}; use opentelemetry_proto::tonic::collector::trace::v1::ExportTraceServiceRequest; use prost::Message; @@ -12,10 +14,30 @@ use crate::state::invocation_data::StoredTrace; use crate::state::invocation_entry; pub async fn traces(req: Request) -> Result, hyper::Error> { - let start = Instant::now(); let (parts, body) = req.into_parts(); let body_bytes = body.collect().await?.to_bytes(); + let path_and_query = parts + .uri + .path_and_query() + .map(|pq| pq.as_str().to_string()) + .unwrap_or_else(|| "/".to_string()); + + handle_traces_payload(parts.method, path_and_query, parts.headers, body_bytes); + + Ok(Response::builder().status(200).body(empty_body()).unwrap()) +} + +/// Shared handling of an OTLP traces payload, independent of the transport +/// (OTLP/HTTP or OTLP/gRPC) it was received over. +pub fn handle_traces_payload( + method: Method, + path_and_query: String, + headers: HeaderMap, + body_bytes: Bytes, +) { + let start = Instant::now(); + // Try to decode and add event payload to server span from AWS Lambda instrumentation let mut encoded_body: Vec = body_bytes.to_vec(); let mut invocation_ids: Vec = Vec::new(); @@ -30,7 +52,7 @@ pub async fn traces(req: Request) -> Result, hyper:: match ExportTraceServiceRequest::decode(body_bytes.as_ref()) { Ok(mut decoded) => { if drop_duplicate_java_instrumenations(&decoded) { - return Ok(Response::builder().status(200).body(empty_body()).unwrap()); + return; } process_trace_request(&mut decoded, &mut invocation_ids, &mut encoded_body); @@ -84,7 +106,7 @@ pub async fn traces(req: Request) -> Result, hyper:: } // If we converted from JSON to protobuf, update the Content-Type header - let mut headers = parts.headers; + let mut headers = headers; if converted_from_json { headers.insert( hyper::header::CONTENT_TYPE, @@ -109,12 +131,8 @@ pub async fn traces(req: Request) -> Result, hyper:: invocation_entry::store_trace_by_id( &invocation_id, StoredTrace { - method: parts.method, - path_and_query: parts - .uri - .path_and_query() - .map(|pq| pq.as_str().to_string()) - .unwrap_or_else(|| "/".to_string()), + method, + path_and_query, headers, body: encoded_body, invocation_ids, @@ -127,5 +145,4 @@ pub async fn traces(req: Request) -> Result, hyper:: crate::log_prefix(), start.elapsed().as_millis(), ); - Ok(Response::builder().status(200).body(empty_body()).unwrap()) } diff --git a/src/route.rs b/src/route.rs index ef3e05e9..c0c9a019 100644 --- a/src/route.rs +++ b/src/route.rs @@ -114,8 +114,20 @@ static ROUTES: Lazy = Lazy::new(|| { Routes { get, post } }); +/// Routes for the dedicated OTLP/HTTP listener on the default OpenTelemetry +/// HTTP port. Only OTLP ingest paths are exposed there — the Lambda Runtime +/// API proxy routes stay on the proxy listener. +static OTLP_ROUTES: Lazy> = Lazy::new(|| { + let mut post = MatchitRouter::new(); + post.insert("/v1/traces", h_traces as Handler).unwrap(); + post.insert("/v1/logs", h_logs as Handler).unwrap(); + post.insert("/v1/metrics", h_metrics as Handler).unwrap(); + post +}); + pub fn init() { Lazy::force(&ROUTES); + Lazy::force(&OTLP_ROUTES); Lazy::force(&HTTPS_CLIENT); invocation_entry::force_init(); } @@ -143,6 +155,31 @@ pub async fn dispatch(req: Request) -> Result, Infal } } +/// Dispatch for the dedicated OTLP/HTTP listener: only OTLP ingest routes, +/// anything else is a 404 (no runtime proxy passthrough). +pub async fn dispatch_otlp(req: Request) -> Result, Infallible> { + let handler = if *req.method() == Method::POST { + OTLP_ROUTES.at(req.uri().path()).ok().map(|m| *m.value) + } else { + None + }; + + let Some(handler) = handler else { + return Ok(Response::builder().status(404).body(empty_body()).unwrap()); + }; + + match handler(req).await { + Ok(resp) => Ok(resp), + Err(e) => { + tracing::error!("[{}] Handler error: {}", crate::log_prefix(), e); + Ok(Response::builder() + .status(500) + .body(full_body(Bytes::from_static(b"500 - Internal Error"))) + .unwrap()) + } + } +} + pub(crate) static HTTPS_CLIENT: Lazy, ReqBody>> = Lazy::new(|| { let https = HttpsConnectorBuilder::new() From 19c758ed1724dccfef08313632901c8b8cc5adc1 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Jul 2026 18:08:16 +0000 Subject: [PATCH 2/4] revert: remove OTLP/gRPC receiver, keep OTLP/HTTP on default port 4318 The gRPC receiver added ~1.2 MiB zipped per layer (tonic, axum, h2) while providing no benefit over OTLP/HTTP on loopback: OTLP only uses unary calls with identical protobuf payloads, and all shipped distros export via OTLP/HTTP anyway. The extension now accepts OTLP over HTTP on the default OpenTelemetry port 4318 (DASH0_OTLP_HTTP_PORT) plus the legacy proxy port 9009, and no longer listens on 4317. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01H3abARSdGW8Edy6LT8CBdP --- Cargo.lock | 61 +--- Cargo.toml | 3 +- README.md | 6 +- .../iac/lambdas/manual/package-lock.json | 337 +----------------- .../iac/lambdas/manual/package.json | 2 - .../iac/lambdas/manual/tracing.js | 48 +-- .../iac/lib/integration-tests-stack.ts | 44 +-- .../tests/src/test-manual-node.test.ts | 24 +- src/config/endpoints.rs | 11 - src/main.rs | 8 +- src/otlp/grpc_receiver.rs | 132 ------- src/otlp/logs_receiver.rs | 37 +- src/otlp/metrics_receiver.rs | 37 +- src/otlp/mod.rs | 1 - src/otlp/receiver.rs | 39 +- 15 files changed, 94 insertions(+), 696 deletions(-) delete mode 100644 src/otlp/grpc_receiver.rs diff --git a/Cargo.lock b/Cargo.lock index c2d4f010..6765d99b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -62,7 +62,7 @@ dependencies = [ "hyper", "hyper-rustls", "hyper-util", - "matchit 0.7.3", + "matchit", "once_cell", "opentelemetry-proto", "parking_lot", @@ -75,7 +75,6 @@ dependencies = [ "sha2", "tokio", "tokio-test", - "tonic", "tracing", "tracing-subscriber", ] @@ -102,49 +101,6 @@ dependencies = [ "fs_extra", ] -[[package]] -name = "axum" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" -dependencies = [ - "axum-core", - "bytes", - "futures-util", - "http", - "http-body", - "http-body-util", - "itoa", - "matchit 0.8.4", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "serde_core", - "sync_wrapper", - "tower", - "tower-layer", - "tower-service", -] - -[[package]] -name = "axum-core" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "http-body-util", - "mime", - "pin-project-lite", - "sync_wrapper", - "tower-layer", - "tower-service", -] - [[package]] name = "base64" version = "0.22.1" @@ -687,24 +643,12 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" -[[package]] -name = "matchit" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" - [[package]] name = "memchr" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - [[package]] name = "mio" version = "1.2.0" @@ -1375,10 +1319,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fec7c61a0695dc1887c1b53952990f3ad2e3a31453e1f49f10e75424943a93ec" dependencies = [ "async-trait", - "axum", "base64", "bytes", - "h2", "http", "http-body", "http-body-util", @@ -1387,7 +1329,6 @@ dependencies = [ "hyper-util", "percent-encoding", "pin-project", - "socket2", "sync_wrapper", "tokio", "tokio-stream", diff --git a/Cargo.toml b/Cargo.toml index c79576b9..45b5abd9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,8 +16,7 @@ bytes = "1" hyper-rustls = { version = "0.27", features = ["http1", "native-tokio", "tls12", "ring"] } rustls = { version = "0.23", default-features = false, features = ["ring"] } matchit = "0.7" -opentelemetry-proto = { version = "0.31", features = ["trace", "logs", "metrics", "with-serde", "gen-tonic"] } -tonic = "0.14" +opentelemetry-proto = { version = "0.31", features = ["trace", "logs", "metrics", "with-serde"] } chrono = { version = "0.4", features = ["serde"] } prost = "0.14.1" serde = { version = "1", features = ["derive"] } diff --git a/README.md b/README.md index beb03c4d..05b969f5 100644 --- a/README.md +++ b/README.md @@ -125,11 +125,11 @@ The following environment variables allow fine-grained control over secret maski If you prefer to set up OpenTelemetry instrumentation yourself instead of relying on the extension's auto-instrumentation, you can use the manual layer and point your OTLP exporters to the extension's local endpoint. The extension will receive the telemetry, enrich it, and forward it to Dash0. 1. Add the manual layer to your Lambda function: `arn:aws:lambda::115813213817:layer:dash0-extension-manual:`. -2. Configure your OTLP trace exporter to send to `http://127.0.0.1:4318/v1/traces` (OTLP/HTTP) or `http://127.0.0.1:4317` (OTLP/gRPC). -3. If exporting metrics, configure your OTLP metric exporter to send to `http://127.0.0.1:4318/v1/metrics` (OTLP/HTTP) or `http://127.0.0.1:4317` (OTLP/gRPC). +2. Configure your OTLP trace exporter to send to `http://127.0.0.1:4318/v1/traces` (OTLP/HTTP). +3. If exporting metrics, configure your OTLP metric exporter to send to `http://127.0.0.1:4318/v1/metrics` (OTLP/HTTP). 4. Make sure to flush all telemetry before the Lambda invocation completes (e.g., in a response hook or before returning the response). -The extension accepts OTLP on the default OpenTelemetry ports: `4318` for OTLP/HTTP and `4317` for OTLP/gRPC (overridable via the `DASH0_OTLP_HTTP_PORT` and `DASH0_OTLP_GRPC_PORT` environment variables). For backwards compatibility, OTLP/HTTP is also still accepted on the extension's proxy port (`9009`). +The extension accepts OTLP over HTTP on the default OpenTelemetry HTTP port `4318` (overridable via the `DASH0_OTLP_HTTP_PORT` environment variable). OTLP/gRPC is not supported. For backwards compatibility, OTLP/HTTP is also still accepted on the extension's proxy port (`9009`). ## Enrichment Attributes diff --git a/integration-tests/iac/lambdas/manual/package-lock.json b/integration-tests/iac/lambdas/manual/package-lock.json index a7095bd5..04087949 100644 --- a/integration-tests/iac/lambdas/manual/package-lock.json +++ b/integration-tests/iac/lambdas/manual/package-lock.json @@ -10,9 +10,7 @@ "license": "ISC", "dependencies": { "@opentelemetry/api": "^1.9.0", - "@opentelemetry/exporter-metrics-otlp-grpc": "^0.211.0", "@opentelemetry/exporter-metrics-otlp-http": "^0.211.0", - "@opentelemetry/exporter-trace-otlp-grpc": "^0.211.0", "@opentelemetry/exporter-trace-otlp-http": "^0.211.0", "@opentelemetry/instrumentation": "^0.211.0", "@opentelemetry/instrumentation-aws-lambda": "^0.63.0", @@ -23,70 +21,6 @@ "@opentelemetry/semantic-conventions": "^1.39.0" } }, - "node_modules/@grpc/grpc-js": { - "version": "1.14.4", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.4.tgz", - "integrity": "sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==", - "license": "Apache-2.0", - "dependencies": { - "@grpc/proto-loader": "^0.8.0", - "@js-sdsl/ordered-map": "^4.4.2" - }, - "engines": { - "node": ">=12.10.0" - } - }, - "node_modules/@grpc/proto-loader": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.1.tgz", - "integrity": "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==", - "license": "Apache-2.0", - "dependencies": { - "lodash.camelcase": "^4.3.0", - "long": "^5.0.0", - "protobufjs": "^7.5.5", - "yargs": "^17.7.2" - }, - "bin": { - "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@grpc/proto-loader/node_modules/protobufjs": { - "version": "7.6.5", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz", - "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==", - "hasInstallScript": true, - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.5", - "@protobufjs/eventemitter": "^1.1.1", - "@protobufjs/fetch": "^1.1.1", - "@protobufjs/float": "^1.0.2", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.1", - "@types/node": ">=13.7.0", - "long": "^5.3.2" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/@js-sdsl/ordered-map": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", - "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/js-sdsl" - } - }, "node_modules/@opentelemetry/api": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", @@ -135,28 +69,6 @@ "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, - "node_modules/@opentelemetry/exporter-metrics-otlp-grpc": { - "version": "0.211.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.211.0.tgz", - "integrity": "sha512-D/U3G8L4PzZp8ot5hX9wpgbTymgtLZCiwR7heMe4LsbGV4OdctS1nfyvaQHLT6CiGZ6FjKc1Vk9s6kbo9SWLXQ==", - "license": "Apache-2.0", - "dependencies": { - "@grpc/grpc-js": "^1.7.1", - "@opentelemetry/core": "2.5.0", - "@opentelemetry/exporter-metrics-otlp-http": "0.211.0", - "@opentelemetry/otlp-exporter-base": "0.211.0", - "@opentelemetry/otlp-grpc-exporter-base": "0.211.0", - "@opentelemetry/otlp-transformer": "0.211.0", - "@opentelemetry/resources": "2.5.0", - "@opentelemetry/sdk-metrics": "2.5.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, "node_modules/@opentelemetry/exporter-metrics-otlp-http": { "version": "0.211.0", "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.211.0.tgz", @@ -176,27 +88,6 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/exporter-trace-otlp-grpc": { - "version": "0.211.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.211.0.tgz", - "integrity": "sha512-eFwx4Gvu6LaEiE1rOd4ypgAiWEdZu7Qzm2QNN2nJqPW1XDeAVH1eNwVcVQl+QK9HR/JCDZ78PZgD7xD/DBDqbw==", - "license": "Apache-2.0", - "dependencies": { - "@grpc/grpc-js": "^1.7.1", - "@opentelemetry/core": "2.5.0", - "@opentelemetry/otlp-exporter-base": "0.211.0", - "@opentelemetry/otlp-grpc-exporter-base": "0.211.0", - "@opentelemetry/otlp-transformer": "0.211.0", - "@opentelemetry/resources": "2.5.0", - "@opentelemetry/sdk-trace-base": "2.5.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, "node_modules/@opentelemetry/exporter-trace-otlp-http": { "version": "0.211.0", "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.211.0.tgz", @@ -266,24 +157,6 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/otlp-grpc-exporter-base": { - "version": "0.211.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.211.0.tgz", - "integrity": "sha512-mR5X+N4SuphJeb7/K7y0JNMC8N1mB6gEtjyTLv+TSAhl0ZxNQzpSKP8S5Opk90fhAqVYD4R0SQSAirEBlH1KSA==", - "license": "Apache-2.0", - "dependencies": { - "@grpc/grpc-js": "^1.7.1", - "@opentelemetry/core": "2.5.0", - "@opentelemetry/otlp-exporter-base": "0.211.0", - "@opentelemetry/otlp-transformer": "0.211.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, "node_modules/@opentelemetry/otlp-transformer": { "version": "0.211.0", "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.211.0.tgz", @@ -410,24 +283,25 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/codegen": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", - "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/eventemitter": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", - "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/fetch": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", - "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", "license": "BSD-3-Clause", "dependencies": { - "@protobufjs/aspromise": "^1.1.1" + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" } }, "node_modules/@protobufjs/float": { @@ -455,9 +329,9 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/utf8": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", - "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", "license": "BSD-3-Clause" }, "node_modules/@types/aws-lambda": { @@ -496,68 +370,12 @@ "acorn": "^8" } }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/cjs-module-lexer": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -575,30 +393,6 @@ } } }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, "node_modules/import-in-the-middle": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-2.0.5.tgz", @@ -611,21 +405,6 @@ "module-details-from-path": "^1.0.4" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", - "license": "MIT" - }, "node_modules/long": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", @@ -668,15 +447,6 @@ "node": ">=12.0.0" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/require-in-the-middle": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz", @@ -690,90 +460,11 @@ "node": ">=9.3.0 || >=8.10.0 <9.0.0" } }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "license": "MIT" - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs": { - "version": "17.7.3", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", - "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "license": "ISC", - "engines": { - "node": ">=12" - } } } } diff --git a/integration-tests/iac/lambdas/manual/package.json b/integration-tests/iac/lambdas/manual/package.json index 5f14e96c..e9aae413 100644 --- a/integration-tests/iac/lambdas/manual/package.json +++ b/integration-tests/iac/lambdas/manual/package.json @@ -13,7 +13,6 @@ "description": "", "dependencies": { "@opentelemetry/api": "^1.9.0", - "@opentelemetry/exporter-trace-otlp-grpc": "^0.211.0", "@opentelemetry/exporter-trace-otlp-http": "^0.211.0", "@opentelemetry/instrumentation": "^0.211.0", "@opentelemetry/instrumentation-aws-lambda": "^0.63.0", @@ -22,7 +21,6 @@ "@opentelemetry/sdk-trace-node": "^2.5.0", "@opentelemetry/semantic-conventions": "^1.39.0", "@opentelemetry/sdk-metrics": "^2.5.0", - "@opentelemetry/exporter-metrics-otlp-grpc": "^0.211.0", "@opentelemetry/exporter-metrics-otlp-http": "^0.211.0" } } diff --git a/integration-tests/iac/lambdas/manual/tracing.js b/integration-tests/iac/lambdas/manual/tracing.js index f6ef2e7c..d89cfd7d 100644 --- a/integration-tests/iac/lambdas/manual/tracing.js +++ b/integration-tests/iac/lambdas/manual/tracing.js @@ -4,49 +4,23 @@ console.log('[tracing] Loading tracing module'); const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node'); const { SimpleSpanProcessor } = require('@opentelemetry/sdk-trace-base'); +const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http'); const { resourceFromAttributes } = require('@opentelemetry/resources'); const { ATTR_SERVICE_NAME } = require('@opentelemetry/semantic-conventions'); const { AwsLambdaInstrumentation } = require('@opentelemetry/instrumentation-aws-lambda'); const { registerInstrumentations } = require('@opentelemetry/instrumentation'); const { MeterProvider, PeriodicExportingMetricReader } = require('@opentelemetry/sdk-metrics'); - -// The extension accepts OTLP on the default OpenTelemetry ports: -// 4318 for OTLP/HTTP and 4317 for OTLP/gRPC. -const protocol = process.env.OTLP_PROTOCOL || 'http'; -console.log(`[tracing] Using OTLP protocol: ${protocol}`); - -let exporter; -let metricExporter; -if (protocol === 'grpc') { - const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-grpc'); - const { OTLPMetricExporter } = require('@opentelemetry/exporter-metrics-otlp-grpc'); - exporter = new OTLPTraceExporter({ - url: `http://127.0.0.1:4317`, - }); - metricExporter = new OTLPMetricExporter({ - url: `http://127.0.0.1:4317`, - headers: { - Authorization: `Bearer ${process.env.DASH0_TOKEN}`, - }, - }); -} else { - const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http'); - const { OTLPMetricExporter } = require('@opentelemetry/exporter-metrics-otlp-http'); - exporter = new OTLPTraceExporter({ - url: `http://127.0.0.1:4318/v1/traces`, - }); - metricExporter = new OTLPMetricExporter({ - url: `http://127.0.0.1:4318/v1/metrics`, - headers: { - Authorization: `Bearer ${process.env.DASH0_TOKEN}`, - }, - }); -} +const { OTLPMetricExporter } = require('@opentelemetry/exporter-metrics-otlp-http'); const resource = resourceFromAttributes({ [ATTR_SERVICE_NAME]: process.env.AWS_LAMBDA_FUNCTION_NAME || 'lambda-metrics', }); +// The extension accepts OTLP/HTTP on the default OpenTelemetry HTTP port (4318). +const exporter = new OTLPTraceExporter({ + url: `http://127.0.0.1:4318/v1/traces`, +}); + const provider = new NodeTracerProvider({ resource, spanProcessors: [new SimpleSpanProcessor(exporter)], @@ -54,6 +28,14 @@ const provider = new NodeTracerProvider({ provider.register(); +// Metrics setup +const metricExporter = new OTLPMetricExporter({ + url: `http://127.0.0.1:4318/v1/metrics`, + headers: { + Authorization: `Bearer ${process.env.DASH0_TOKEN}`, + }, +}); + const meterProvider = new MeterProvider({ resource, readers: [ diff --git a/integration-tests/iac/lib/integration-tests-stack.ts b/integration-tests/iac/lib/integration-tests-stack.ts index 446bc8be..11a94dda 100644 --- a/integration-tests/iac/lib/integration-tests-stack.ts +++ b/integration-tests/iac/lib/integration-tests-stack.ts @@ -328,31 +328,25 @@ class ManualStack extends cdk.NestedStack { }); for (const runtime of runtimes) { const runtimeName = runtime.name.replace(/\./g, '-'); - // One function per OTLP protocol: 'http' exports to the default - // OTLP/HTTP port (4318), 'grpc' to the default OTLP/gRPC port (4317). - for (const otlpProtocol of ['http', 'grpc']) { - const nameSuffix = otlpProtocol === 'http' ? '' : `-${otlpProtocol}`; - new lambda.Function(this, `manual-instrumentation${nameSuffix}-${runtimeName}`, { - functionName: `${props.prefix}manual-instrumentation${nameSuffix}-${runtimeName}`, - runtime, - memorySize: 512, - handler: 'index.hello', - architecture: lambda.Architecture.X86_64, - timeout: cdk.Duration.seconds(10), - code, - layers: [props.layer], - role: props.role, - environment: { - AWS_LAMBDA_EXEC_WRAPPER: "/opt/wrapper", - DASH0_TOKEN: process.env.DASH0_DEV_API_TOKEN!, - DASH0_ENDPOINT: "https://ingress.eu-west-1.aws.dash0-dev.com:4318", - DASH0_EXTENSION_LOG_LEVEL: "info", - OTLP_PROTOCOL: otlpProtocol, - }, - logGroup: props.logGroup, - loggingFormat: lambda.LoggingFormat.TEXT, - }); - } + new lambda.Function(this, `manual-instrumentation-${runtimeName}`, { + functionName: `${props.prefix}manual-instrumentation-${runtimeName}`, + runtime, + memorySize: 512, + handler: 'index.hello', + architecture: lambda.Architecture.X86_64, + timeout: cdk.Duration.seconds(10), + code, + layers: [props.layer], + role: props.role, + environment: { + AWS_LAMBDA_EXEC_WRAPPER: "/opt/wrapper", + DASH0_TOKEN: process.env.DASH0_DEV_API_TOKEN!, + DASH0_ENDPOINT: "https://ingress.eu-west-1.aws.dash0-dev.com:4318", + DASH0_EXTENSION_LOG_LEVEL: "info", + }, + logGroup: props.logGroup, + loggingFormat: lambda.LoggingFormat.TEXT, + }); } } } diff --git a/integration-tests/tests/src/test-manual-node.test.ts b/integration-tests/tests/src/test-manual-node.test.ts index 74833e0e..0b7dd7a1 100644 --- a/integration-tests/tests/src/test-manual-node.test.ts +++ b/integration-tests/tests/src/test-manual-node.test.ts @@ -47,21 +47,15 @@ const verifyManualInstrumentation = async (functionName: string) => { describe.concurrent('Manual instrumentation Lambda', () => { const runtimes = NODE_RUNTIMES; - // 'http' exports OTLP to the default OTLP/HTTP port (4318), - // 'grpc' to the default OTLP/gRPC port (4317). - const otlpProtocols = ['http', 'grpc']; for (const runtime of runtimes) { - for (const otlpProtocol of otlpProtocols) { - const nameSuffix = otlpProtocol === 'http' ? '' : `-${otlpProtocol}`; - const functionName = `${RESOURCE_PREFIX}manual-instrumentation${nameSuffix}-${runtime}` - it( - `invokes ${functionName} and receives trace (OTLP via ${otlpProtocol})`, - async () => { - console.log(`Starting test for ${functionName}`, new Date().toISOString()); - await verifyManualInstrumentation(functionName); - }, - TEST_TIMEOUT_MS - ); - } + const functionName = `${RESOURCE_PREFIX}manual-instrumentation-${runtime}` + it( + `invokes ${functionName} and receives trace`, + async () => { + console.log(`Starting test for ${functionName}`, new Date().toISOString()); + await verifyManualInstrumentation(functionName); + }, + TEST_TIMEOUT_MS + ); } }); diff --git a/src/config/endpoints.rs b/src/config/endpoints.rs index 6a0654c1..e2f7088a 100644 --- a/src/config/endpoints.rs +++ b/src/config/endpoints.rs @@ -27,17 +27,6 @@ pub fn otlp_http_port() -> u16 { .unwrap_or(crate::DEFAULT_OTLP_HTTP_PORT) } -/// Get the OTLP/gRPC receiver port from environment variable or use the -/// default OpenTelemetry gRPC port (4317) -pub fn otlp_grpc_port() -> u16 { - use std::env::var; - - var("DASH0_OTLP_GRPC_PORT") - .ok() - .and_then(|v| v.parse::().ok()) - .unwrap_or(crate::DEFAULT_OTLP_GRPC_PORT) -} - /// Latch in the API endpoints defined in ENV variables /// #[allow(dead_code)] diff --git a/src/main.rs b/src/main.rs index beae8c2f..d8eea6fb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -31,9 +31,6 @@ pub const DEFAULT_PROXY_PORT: u16 = 9009; /// Default OpenTelemetry OTLP/HTTP port, overriden by DASH0_OTLP_HTTP_PORT environment variable pub const DEFAULT_OTLP_HTTP_PORT: u16 = 4318; -/// Default OpenTelemetry OTLP/gRPC port, overriden by DASH0_OTLP_GRPC_PORT environment variable -pub const DEFAULT_OTLP_GRPC_PORT: u16 = 4317; - pub static LAMBDA_RUNTIME_API_VERSION: &str = "2018-06-01"; /// Returns the log prefix for standard log messages @@ -101,14 +98,11 @@ async fn main() { let proxy_listener = bind_listener(addr).await; let server_join_handle = spawn_http1_server(proxy_listener, route::dispatch); - // Dedicated OTLP receivers on the default OpenTelemetry ports. + // Dedicated OTLP/HTTP receiver on the default OpenTelemetry HTTP port. let otlp_http_addr = SocketAddr::from(([0, 0, 0, 0], config::endpoints::otlp_http_port())); let otlp_http_listener = bind_listener(otlp_http_addr).await; spawn_http1_server(otlp_http_listener, route::dispatch_otlp); - let otlp_grpc_addr = SocketAddr::from(([0, 0, 0, 0], config::endpoints::otlp_grpc_port())); - tokio::spawn(otlp::grpc_receiver::serve(otlp_grpc_addr)); - // Initialize the extension and continually get next extension event. tokio::task::spawn(async { extension::register::register().await; diff --git a/src/otlp/grpc_receiver.rs b/src/otlp/grpc_receiver.rs deleted file mode 100644 index 6ecb0c05..00000000 --- a/src/otlp/grpc_receiver.rs +++ /dev/null @@ -1,132 +0,0 @@ -//! OTLP/gRPC receiver. -//! -//! Accepts telemetry from the runtime on the default OpenTelemetry gRPC port -//! (4317) and feeds it into the same processing pipeline as the OTLP/HTTP -//! receivers. Payloads are re-encoded as protobuf and stored as if they had -//! been received on the corresponding OTLP/HTTP path, since the exporter -//! forwards them to the backend via OTLP/HTTP. - -use std::net::SocketAddr; - -use bytes::Bytes; -use hyper::header::{HeaderMap, HeaderValue, CONTENT_TYPE}; -use hyper::Method; -use opentelemetry_proto::tonic::collector::logs::v1::logs_service_server::{ - LogsService, LogsServiceServer, -}; -use opentelemetry_proto::tonic::collector::logs::v1::{ - ExportLogsServiceRequest, ExportLogsServiceResponse, -}; -use opentelemetry_proto::tonic::collector::metrics::v1::metrics_service_server::{ - MetricsService, MetricsServiceServer, -}; -use opentelemetry_proto::tonic::collector::metrics::v1::{ - ExportMetricsServiceRequest, ExportMetricsServiceResponse, -}; -use opentelemetry_proto::tonic::collector::trace::v1::trace_service_server::{ - TraceService, TraceServiceServer, -}; -use opentelemetry_proto::tonic::collector::trace::v1::{ - ExportTraceServiceRequest, ExportTraceServiceResponse, -}; -use prost::Message; -use tonic::{Request, Response, Status}; - -use crate::otlp::logs_receiver::handle_logs_payload; -use crate::otlp::metrics_receiver::handle_metrics_payload; -use crate::otlp::receiver::handle_traces_payload; - -/// Headers for a stored payload received via gRPC: the body is always -/// protobuf-encoded, matching what the OTLP/HTTP exporter sends upstream. -fn protobuf_headers() -> HeaderMap { - let mut headers = HeaderMap::new(); - headers.insert( - CONTENT_TYPE, - HeaderValue::from_static("application/x-protobuf"), - ); - headers -} - -#[derive(Default)] -pub struct OtlpGrpcTraceService; - -#[tonic::async_trait] -impl TraceService for OtlpGrpcTraceService { - async fn export( - &self, - request: Request, - ) -> Result, Status> { - let body = request.into_inner().encode_to_vec(); - handle_traces_payload( - Method::POST, - "/v1/traces".to_string(), - protobuf_headers(), - Bytes::from(body), - ); - Ok(Response::new(ExportTraceServiceResponse::default())) - } -} - -#[derive(Default)] -pub struct OtlpGrpcLogsService; - -#[tonic::async_trait] -impl LogsService for OtlpGrpcLogsService { - async fn export( - &self, - request: Request, - ) -> Result, Status> { - let body = request.into_inner().encode_to_vec(); - handle_logs_payload( - Method::POST, - "/v1/logs".to_string(), - protobuf_headers(), - Bytes::from(body), - ); - Ok(Response::new(ExportLogsServiceResponse::default())) - } -} - -#[derive(Default)] -pub struct OtlpGrpcMetricsService; - -#[tonic::async_trait] -impl MetricsService for OtlpGrpcMetricsService { - async fn export( - &self, - request: Request, - ) -> Result, Status> { - let body = request.into_inner().encode_to_vec(); - handle_metrics_payload( - Method::POST, - "/v1/metrics".to_string(), - protobuf_headers(), - Bytes::from(body), - ); - Ok(Response::new(ExportMetricsServiceResponse::default())) - } -} - -/// Serves the OTLP/gRPC receiver on the given address. Only returns on -/// server failure. -pub async fn serve(addr: SocketAddr) { - tracing::info!( - "[{}] OTLP/gRPC receiver listening on {}", - crate::log_prefix(), - addr - ); - - if let Err(e) = tonic::transport::Server::builder() - .add_service(TraceServiceServer::new(OtlpGrpcTraceService)) - .add_service(LogsServiceServer::new(OtlpGrpcLogsService)) - .add_service(MetricsServiceServer::new(OtlpGrpcMetricsService)) - .serve(addr) - .await - { - tracing::error!("[{}] OTLP/gRPC receiver failed: {}", crate::log_prefix(), e); - panic!( - "[{}] Cannot continue without OTLP/gRPC receiver", - crate::log_prefix() - ); - } -} diff --git a/src/otlp/logs_receiver.rs b/src/otlp/logs_receiver.rs index d0f633ae..05ecf19f 100644 --- a/src/otlp/logs_receiver.rs +++ b/src/otlp/logs_receiver.rs @@ -1,10 +1,8 @@ use std::time::Instant; -use bytes::Bytes; use http_body_util::BodyExt; use hyper::body::Incoming; -use hyper::header::HeaderMap; -use hyper::{Method, Request, Response}; +use hyper::{Request, Response}; use opentelemetry_proto::tonic::collector::logs::v1::ExportLogsServiceRequest; use prost::Message; @@ -12,30 +10,10 @@ use crate::route::{empty_body, ResBody}; use crate::state::invocation_data::{store_log, StoredLog}; pub async fn logs(req: Request) -> Result, hyper::Error> { + let start = Instant::now(); let (parts, body) = req.into_parts(); let body_bytes = body.collect().await?.to_bytes(); - let path_and_query = parts - .uri - .path_and_query() - .map(|pq| pq.as_str().to_string()) - .unwrap_or_else(|| "/".to_string()); - - handle_logs_payload(parts.method, path_and_query, parts.headers, body_bytes); - - Ok(Response::builder().status(200).body(empty_body()).unwrap()) -} - -/// Shared handling of an OTLP logs payload, independent of the transport -/// (OTLP/HTTP or OTLP/gRPC) it was received over. -pub fn handle_logs_payload( - method: Method, - path_and_query: String, - headers: HeaderMap, - body_bytes: Bytes, -) { - let start = Instant::now(); - let mut encoded_body: Vec = body_bytes.to_vec(); let mut converted_from_json = false; @@ -81,7 +59,7 @@ pub fn handle_logs_payload( } } - let mut headers = headers; + let mut headers = parts.headers; if converted_from_json { headers.insert( hyper::header::CONTENT_TYPE, @@ -90,8 +68,12 @@ pub fn handle_logs_payload( } store_log(StoredLog { - method, - path_and_query, + method: parts.method, + path_and_query: parts + .uri + .path_and_query() + .map(|pq| pq.as_str().to_string()) + .unwrap_or_else(|| "/".to_string()), headers, body: encoded_body, }); @@ -101,4 +83,5 @@ pub fn handle_logs_payload( crate::log_prefix(), start.elapsed().as_millis(), ); + Ok(Response::builder().status(200).body(empty_body()).unwrap()) } diff --git a/src/otlp/metrics_receiver.rs b/src/otlp/metrics_receiver.rs index f6ffce68..c83dc5cd 100644 --- a/src/otlp/metrics_receiver.rs +++ b/src/otlp/metrics_receiver.rs @@ -1,10 +1,8 @@ use std::time::Instant; -use bytes::Bytes; use http_body_util::BodyExt; use hyper::body::Incoming; -use hyper::header::HeaderMap; -use hyper::{Method, Request, Response}; +use hyper::{Request, Response}; use opentelemetry_proto::tonic::collector::metrics::v1::ExportMetricsServiceRequest; use prost::Message; @@ -12,30 +10,10 @@ use crate::route::{empty_body, ResBody}; use crate::state::invocation_data::{store_metric, StoredMetric}; pub async fn metrics(req: Request) -> Result, hyper::Error> { + let start = Instant::now(); let (parts, body) = req.into_parts(); let body_bytes = body.collect().await?.to_bytes(); - let path_and_query = parts - .uri - .path_and_query() - .map(|pq| pq.as_str().to_string()) - .unwrap_or_else(|| "/".to_string()); - - handle_metrics_payload(parts.method, path_and_query, parts.headers, body_bytes); - - Ok(Response::builder().status(200).body(empty_body()).unwrap()) -} - -/// Shared handling of an OTLP metrics payload, independent of the transport -/// (OTLP/HTTP or OTLP/gRPC) it was received over. -pub fn handle_metrics_payload( - method: Method, - path_and_query: String, - headers: HeaderMap, - body_bytes: Bytes, -) { - let start = Instant::now(); - let mut encoded_body: Vec = body_bytes.to_vec(); let mut converted_from_json = false; @@ -81,7 +59,7 @@ pub fn handle_metrics_payload( } } - let mut headers = headers; + let mut headers = parts.headers; if converted_from_json { headers.insert( hyper::header::CONTENT_TYPE, @@ -90,8 +68,12 @@ pub fn handle_metrics_payload( } store_metric(StoredMetric { - method, - path_and_query, + method: parts.method, + path_and_query: parts + .uri + .path_and_query() + .map(|pq| pq.as_str().to_string()) + .unwrap_or_else(|| "/".to_string()), headers, body: encoded_body, }); @@ -101,4 +83,5 @@ pub fn handle_metrics_payload( crate::log_prefix(), start.elapsed().as_millis(), ); + Ok(Response::builder().status(200).body(empty_body()).unwrap()) } diff --git a/src/otlp/mod.rs b/src/otlp/mod.rs index a7213e19..a31e4d89 100644 --- a/src/otlp/mod.rs +++ b/src/otlp/mod.rs @@ -2,7 +2,6 @@ pub const OTEL_SCHEMA_URL: &str = "https://opentelemetry.io/schemas/1.11.0"; pub mod attributes; pub mod exporter; -pub mod grpc_receiver; pub mod log_mutations; pub mod logs_receiver; pub mod masking; diff --git a/src/otlp/receiver.rs b/src/otlp/receiver.rs index 7f67d486..4612c9c4 100644 --- a/src/otlp/receiver.rs +++ b/src/otlp/receiver.rs @@ -1,10 +1,8 @@ use std::time::Instant; -use bytes::Bytes; use http_body_util::BodyExt; use hyper::body::Incoming; -use hyper::header::HeaderMap; -use hyper::{Method, Request, Response}; +use hyper::{Request, Response}; use opentelemetry_proto::tonic::collector::trace::v1::ExportTraceServiceRequest; use prost::Message; @@ -14,30 +12,10 @@ use crate::state::invocation_data::StoredTrace; use crate::state::invocation_entry; pub async fn traces(req: Request) -> Result, hyper::Error> { + let start = Instant::now(); let (parts, body) = req.into_parts(); let body_bytes = body.collect().await?.to_bytes(); - let path_and_query = parts - .uri - .path_and_query() - .map(|pq| pq.as_str().to_string()) - .unwrap_or_else(|| "/".to_string()); - - handle_traces_payload(parts.method, path_and_query, parts.headers, body_bytes); - - Ok(Response::builder().status(200).body(empty_body()).unwrap()) -} - -/// Shared handling of an OTLP traces payload, independent of the transport -/// (OTLP/HTTP or OTLP/gRPC) it was received over. -pub fn handle_traces_payload( - method: Method, - path_and_query: String, - headers: HeaderMap, - body_bytes: Bytes, -) { - let start = Instant::now(); - // Try to decode and add event payload to server span from AWS Lambda instrumentation let mut encoded_body: Vec = body_bytes.to_vec(); let mut invocation_ids: Vec = Vec::new(); @@ -52,7 +30,7 @@ pub fn handle_traces_payload( match ExportTraceServiceRequest::decode(body_bytes.as_ref()) { Ok(mut decoded) => { if drop_duplicate_java_instrumenations(&decoded) { - return; + return Ok(Response::builder().status(200).body(empty_body()).unwrap()); } process_trace_request(&mut decoded, &mut invocation_ids, &mut encoded_body); @@ -106,7 +84,7 @@ pub fn handle_traces_payload( } // If we converted from JSON to protobuf, update the Content-Type header - let mut headers = headers; + let mut headers = parts.headers; if converted_from_json { headers.insert( hyper::header::CONTENT_TYPE, @@ -131,8 +109,12 @@ pub fn handle_traces_payload( invocation_entry::store_trace_by_id( &invocation_id, StoredTrace { - method, - path_and_query, + method: parts.method, + path_and_query: parts + .uri + .path_and_query() + .map(|pq| pq.as_str().to_string()) + .unwrap_or_else(|| "/".to_string()), headers, body: encoded_body, invocation_ids, @@ -145,4 +127,5 @@ pub fn handle_traces_payload( crate::log_prefix(), start.elapsed().as_millis(), ); + Ok(Response::builder().status(200).body(empty_body()).unwrap()) } From 845f0b4344c5df4a01e47c20350bd34c0a52b35c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 11 Jul 2026 08:49:21 +0000 Subject: [PATCH 3/4] fix: export python spans with 10ms batch delay to avoid loss on timeout The python distro used BatchSpanProcessor defaults, i.e. a 5s schedule delay. Spans still queued in the SDK when Lambda kills a timed-out runtime are lost, which made the python timeout integration tests flaky (the HTTP client span intermittently never reached the extension). The Java distro already batches tightly (otel.bsp.schedule.delay=10ms) for the same reason; align the python distro. The standard OTEL_BSP_SCHEDULE_DELAY environment variable still takes precedence. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01H3abARSdGW8Edy6LT8CBdP --- opt/python/distro/src/dash0_opentelemetry/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/opt/python/distro/src/dash0_opentelemetry/__init__.py b/opt/python/distro/src/dash0_opentelemetry/__init__.py index ed42e19a..090ae266 100644 --- a/opt/python/distro/src/dash0_opentelemetry/__init__.py +++ b/opt/python/distro/src/dash0_opentelemetry/__init__.py @@ -124,6 +124,11 @@ def init() -> Dict[str, Any]: endpoint=traces_endpoint, headers={"Authorization": f"Bearer {dash0_token}"}, ), + # The default 5s schedule delay races against Lambda killing the + # runtime on timeout, losing spans that are still queued. Export + # aggressively: the endpoint is the extension on loopback, and the + # Java distro batches equally tightly (otel.bsp.schedule.delay=10ms). + schedule_delay_millis=int(os.getenv("OTEL_BSP_SCHEDULE_DELAY", "10")), ) ) From a697270b2aee7fe29a6234e40b6928a095f4f9e2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 11 Jul 2026 10:07:02 +0000 Subject: [PATCH 4/4] fix: signal runtimeDone only after synthesizing error traces On platform.runtimeDone with an error status (e.g. timeout), the telemetry handler signaled the runtime-done notifier before building the synthetic trace. With DASH0_SEND_ON_INVOCATION_END=true the notifier wakes the invocation-end flush task, which concurrently drains the stored traces. When that task won the race, build_synthetic_trace could no longer recover the invocation's real trace id from the stored spans and fell back to a derived one, breaking correlation between the synthetic handler span and already-received spans (observed as the flaky python timeout integration tests: the HTTP client span never matched the synthetic trace id). Defer the signal until after error traces are built and sent, making the ordering deterministic. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01H3abARSdGW8Edy6LT8CBdP --- src/extension/telemetry_receiver.rs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/extension/telemetry_receiver.rs b/src/extension/telemetry_receiver.rs index ccb59bf6..05151ff6 100644 --- a/src/extension/telemetry_receiver.rs +++ b/src/extension/telemetry_receiver.rs @@ -30,6 +30,8 @@ pub async fn telemetry(req: Request) -> Result, hype body_text ); + let mut saw_runtime_done = false; + if let Ok(mut logs) = serde_json::from_str::>(&body_text) { @@ -37,17 +39,13 @@ pub async fn telemetry(req: Request) -> Result, hype for log in &logs { if log.r#type == "platform.runtimeDone" { + saw_runtime_done = true; if let Some(id) = &log.invocation_id { let is_error = error_invocation_ids.iter().any(|(eid, _)| eid == id); if !is_error { create_supplementary_spans(id, true); } } - if let Some(notifier) = crate::state::invocation_data::take_runtime_done_notifier() - { - tracing::info!("[{}] Signaled platform.runtimeDone", crate::log_prefix()); - let _ = notifier.send(()); - } } if log.r#type == "platform.report" { @@ -106,5 +104,16 @@ pub async fn telemetry(req: Request) -> Result, hype flush_telemetry_logs(None).await; } + // Signal platform.runtimeDone only after error traces have been built and + // sent. Signaling earlier lets the send-on-invocation-end flush task drain + // the stored traces concurrently, so build_synthetic_trace can no longer + // recover the invocation's real trace id and correlation is lost. + if saw_runtime_done { + if let Some(notifier) = crate::state::invocation_data::take_runtime_done_notifier() { + tracing::info!("[{}] Signaled platform.runtimeDone", crate::log_prefix()); + let _ = notifier.send(()); + } + } + Ok(Response::builder().status(200).body(empty_body()).unwrap()) }