From 7de1e18675bbf714a116a92aa451959412966517 Mon Sep 17 00:00:00 2001 From: urismiley Date: Mon, 3 Aug 2026 18:09:02 -0400 Subject: [PATCH 1/3] Add a shared gateway tracing demo with Mongoose. Copilot-Session: 85f151f9-f585-4bdf-9c2b-798f8a2651be --- README.md | 20 + playgrounds/beanie/scripts/lib.sh | 7 +- playgrounds/mongodb-node/scripts/lib.sh | 7 +- playgrounds/mongoose/README.md | 63 +- playgrounds/mongoose/app/package-lock.json | 967 +++++++++++++++++- playgrounds/mongoose/app/package.json | 7 +- playgrounds/mongoose/app/server.js | 67 +- playgrounds/mongoose/app/telemetry.js | 138 +++ playgrounds/mongoose/scripts/lib.sh | 7 +- .../mongoose/scripts/run-telemetry-demo.sh | 30 + .../mongoose/scripts/verify-telemetry.sh | 110 ++ playgrounds/pymongo/scripts/lib.sh | 7 +- shared/telemetry/.env.example | 13 + shared/telemetry/README.md | 108 ++ shared/telemetry/compose.yaml | 53 + shared/telemetry/gateway-setup.json | 24 + shared/telemetry/otel-collector.yaml | 37 + shared/telemetry/scripts/_lib.sh | 68 ++ .../scripts/build-documentdb-image.sh | 92 ++ shared/telemetry/scripts/down.sh | 14 + shared/telemetry/scripts/up.sh | 67 ++ 21 files changed, 1882 insertions(+), 24 deletions(-) create mode 100644 playgrounds/mongoose/app/telemetry.js create mode 100755 playgrounds/mongoose/scripts/run-telemetry-demo.sh create mode 100755 playgrounds/mongoose/scripts/verify-telemetry.sh create mode 100644 shared/telemetry/.env.example create mode 100644 shared/telemetry/README.md create mode 100644 shared/telemetry/compose.yaml create mode 100644 shared/telemetry/gateway-setup.json create mode 100644 shared/telemetry/otel-collector.yaml create mode 100755 shared/telemetry/scripts/_lib.sh create mode 100755 shared/telemetry/scripts/build-documentdb-image.sh create mode 100755 shared/telemetry/scripts/down.sh create mode 100755 shared/telemetry/scripts/up.sh diff --git a/README.md b/README.md index 91f7c2b..55cafa2 100644 --- a/README.md +++ b/README.md @@ -62,12 +62,32 @@ cd playgrounds/pymongo ./scripts/run-app.sh # or run the demo REST API ``` +## Shared telemetry demo + +[`shared/telemetry/`](shared/telemetry/) provides a reusable local stack with a +tracing-enabled DocumentDB image, an OpenTelemetry Collector, and Jaeger. Start +it once, then use any playground against the shared `documentdb-local` +container. + +The Mongoose playground also includes optional application instrumentation and +client-to-gateway trace-context propagation: + +```bash +cd playgrounds/mongoose +./scripts/run-telemetry-demo.sh +``` + +See the [shared telemetry guide](shared/telemetry/README.md) for image build, +configuration, and verification instructions. + ## Repository Layout ``` documentdb-playground/ ├── README.md ├── LICENSE +├── shared/ +│ └── telemetry/ # Collector + Jaeger + tracing-enabled DocumentDB stack └── playgrounds/ ├── mongoose/ # Node.js + Mongoose ODM ├── mongodb-node/ # Node.js + MongoDB native driver diff --git a/playgrounds/beanie/scripts/lib.sh b/playgrounds/beanie/scripts/lib.sh index 84f722f..ecce57f 100755 --- a/playgrounds/beanie/scripts/lib.sh +++ b/playgrounds/beanie/scripts/lib.sh @@ -10,6 +10,7 @@ DOCUMENTDB_CONTAINER="${DOCUMENTDB_CONTAINER:-documentdb-local}" DOCUMENTDB_IMAGE="${DOCUMENTDB_IMAGE:-ghcr.io/documentdb/documentdb/documentdb-local:latest}" DOCUMENTDB_HOST="${DOCUMENTDB_HOST:-localhost}" DOCUMENTDB_PORT="${DOCUMENTDB_PORT:-10260}" +DOCUMENTDB_CONTAINER_PORT=10260 # Note: the emulator rejects some reserved names (e.g. "documentdb"); use a # distinct admin username. DOCUMENTDB_USERNAME="${DOCUMENTDB_USERNAME:-docdbadmin}" @@ -22,6 +23,10 @@ build_uri() { echo "mongodb://${DOCUMENTDB_USERNAME}:${DOCUMENTDB_PASSWORD}@${DOCUMENTDB_HOST}:${DOCUMENTDB_PORT}/?tls=true&tlsAllowInvalidCertificates=true&directConnection=true" } +build_container_uri() { + echo "mongodb://${DOCUMENTDB_USERNAME}:${DOCUMENTDB_PASSWORD}@localhost:${DOCUMENTDB_CONTAINER_PORT}/?tls=true&tlsAllowInvalidCertificates=true&directConnection=true" +} + # Wait until a TCP port accepts connections. # Args: [tries] wait_for_port() { @@ -95,7 +100,7 @@ ensure_documentdb() { wait_for_documentdb() { local uri - uri="$(build_uri)" + uri="$(build_container_uri)" echo "Waiting for DocumentDB to accept connections ..." local i diff --git a/playgrounds/mongodb-node/scripts/lib.sh b/playgrounds/mongodb-node/scripts/lib.sh index cfcdadd..af91ca8 100755 --- a/playgrounds/mongodb-node/scripts/lib.sh +++ b/playgrounds/mongodb-node/scripts/lib.sh @@ -5,6 +5,7 @@ set -euo pipefail DOCUMENTDB_IMAGE="${DOCUMENTDB_IMAGE:-ghcr.io/documentdb/documentdb/documentdb-local:latest}" DOCUMENTDB_CONTAINER="${DOCUMENTDB_CONTAINER:-documentdb-local}" DOCUMENTDB_PORT="${DOCUMENTDB_PORT:-10260}" +DOCUMENTDB_CONTAINER_PORT=10260 DOCUMENTDB_USERNAME="${DOCUMENTDB_USERNAME:-docdbadmin}" DOCUMENTDB_PASSWORD="${DOCUMENTDB_PASSWORD:-Documentdb!Local1}" @@ -12,6 +13,10 @@ build_uri() { echo "mongodb://${DOCUMENTDB_USERNAME}:${DOCUMENTDB_PASSWORD}@localhost:${DOCUMENTDB_PORT}/?tls=true&tlsAllowInvalidCertificates=true&directConnection=true" } +build_container_uri() { + echo "mongodb://${DOCUMENTDB_USERNAME}:${DOCUMENTDB_PASSWORD}@localhost:${DOCUMENTDB_CONTAINER_PORT}/?tls=true&tlsAllowInvalidCertificates=true&directConnection=true" +} + require_docker() { command -v docker >/dev/null || { echo "docker is required (start Docker Desktop / the Docker daemon first)" >&2 @@ -55,7 +60,7 @@ ensure_documentdb() { wait_for_documentdb() { local uri - uri="$(build_uri)" + uri="$(build_container_uri)" echo "Waiting for DocumentDB to accept connections ..." local attempt diff --git a/playgrounds/mongoose/README.md b/playgrounds/mongoose/README.md index 8eae3f5..9edac64 100644 --- a/playgrounds/mongoose/README.md +++ b/playgrounds/mongoose/README.md @@ -9,8 +9,10 @@ instance running in Docker. It includes: (`app/mongoose-crud-test.js`) that exercises connect, schema/index creation, insert, query, update, aggregation, unique-index enforcement, and delete. -Everything runs on your machine: DocumentDB in a Docker container, the app and -tests as local Node.js processes. No Kubernetes, no cloud, no image builds. +The normal app and test paths run entirely on your machine: DocumentDB in a +Docker container, with the app and tests as local Node.js processes. The +optional tracing demo builds a custom local image containing the gateway +tracing changes. > **What is Mongoose?** Mongoose is a **Node.js library** (an ODM, Object Data > Modeling layer), not a CLI tool or a server. Your application imports it to @@ -21,7 +23,7 @@ tests as local Node.js processes. No Kubernetes, no cloud, no image builds. ## Prerequisites - **Docker** (Docker Desktop or a Docker daemon) — runs DocumentDB locally -- **Node.js 18+** and **npm** — runs the app and test suite +- **Node.js 18.19.x or 20.6+** and **npm** — runs the app and test suite That's it. The scripts pull the official `documentdb-local` image and start it for you. @@ -79,6 +81,53 @@ Stop the database when you're done: ./scripts/stop-documentdb.sh ``` +## Trace the application and gateway + +The shared telemetry stack runs a custom DocumentDB image, an OpenTelemetry +Collector, and Jaeger. The Mongoose app emits HTTP and client spans, then puts +the active W3C trace context in each supported database command's `comment` +field. The gateway uses that context as the parent of `gateway.request`. + +```text +HTTP request + -> Express server span + -> Mongoose client span + -> gateway.request + -> gateway.process_request + -> postgres.execute +``` + +First build a local image from a DocumentDB source checkout that contains the +gateway tracing changes: + +```bash +cd ../../shared/telemetry +DOCUMENTDB_SOURCE_DIR=/path/to/documentdb \ + ./scripts/build-documentdb-image.sh +``` + +Start the telemetry stack and Mongoose API: + +```bash +cd ../../playgrounds/mongoose +./scripts/run-telemetry-demo.sh +``` + +In another terminal, generate API traffic and verify that Jaeger received one +trace containing both the application and gateway services: + +```bash +./scripts/verify-telemetry.sh +``` + +The verification helper also requires Python 3. + +The verification script prints a direct trace URL. The Jaeger UI is also +available at . + +The normal `run-app.sh` and `run-test.sh` paths remain unchanged. Application +tracing is enabled only when `OTEL_TRACES_ENABLED=true`. + ## Trying the API With `./scripts/run-app.sh` running, the API is on `http://localhost:3000`: @@ -106,6 +155,8 @@ curl -s http://localhost:3000/stats/genres | jq . | ----------------------------- | ------------------------------------------------------------------------------ | | `scripts/run-test.sh` | Start DocumentDB (if needed) and run the Mongoose CRUD/compatibility suite. | | `scripts/run-app.sh` | Start DocumentDB (if needed) and run the Express + Mongoose API locally. | +| `scripts/run-telemetry-demo.sh` | Start the shared Collector, Jaeger, and DocumentDB stack, then run the traced API. | +| `scripts/verify-telemetry.sh` | Generate API traffic and verify a connected application and gateway trace. | | `scripts/start-documentdb.sh` | Start the local DocumentDB container and wait until it is ready. | | `scripts/stop-documentdb.sh` | Stop and remove the local DocumentDB container. | | `scripts/lib.sh` | Shared helpers: container lifecycle, readiness wait, connection-string builder. | @@ -184,6 +235,9 @@ Read by [`app/db.js`](app/db.js), [`app/server.js`](app/server.js), and | `TLS_INSECURE` | `true` | app + test | When `true`, accepts the gateway's self-signed cert. Set `false` for CA-verified TLS.| | `SERVER_SELECTION_TIMEOUT_MS` | `10000` | app | How long Mongoose waits to select a server before erroring. | | `PORT` | `3000` | app | Port the Express API listens on. | +| `OTEL_TRACES_ENABLED` | `false` | app | Enable HTTP, Mongoose client, and trace-context propagation instrumentation. | +| `OTEL_EXPORTER_OTLP_ENDPOINT` | OpenTelemetry default | app | Collector endpoint used by the traced app. | +| `OTEL_SERVICE_NAME` | SDK default | app | Application service name shown in Jaeger. | ## Running the Test Suite Manually @@ -254,6 +308,7 @@ mongoose/ │ ├── package.json │ ├── db.js # Mongoose connection (DocumentDB options) │ ├── server.js # Express REST API (/books, /health, /stats) +│ ├── telemetry.js # Optional OpenTelemetry setup + context propagation │ ├── models/book.js # Example Mongoose schema/model │ └── mongoose-crud-test.js # Standalone CRUD/compatibility test suite └── scripts/ @@ -261,5 +316,7 @@ mongoose/ ├── start-documentdb.sh # Start the local DocumentDB container ├── stop-documentdb.sh # Stop/remove the local DocumentDB container ├── run-app.sh # Start DocumentDB + run the API locally + ├── run-telemetry-demo.sh # Start shared telemetry + run the traced API + ├── verify-telemetry.sh # Verify the joined app and gateway trace └── run-test.sh # Start DocumentDB + run the test suite ``` diff --git a/playgrounds/mongoose/app/package-lock.json b/playgrounds/mongoose/app/package-lock.json index 077ad8a..a34c7ab 100644 --- a/playgrounds/mongoose/app/package-lock.json +++ b/playgrounds/mongoose/app/package-lock.json @@ -9,11 +9,57 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/exporter-trace-otlp-grpc": "^0.221.0", + "@opentelemetry/instrumentation-express": "^0.69.0", + "@opentelemetry/instrumentation-http": "^0.221.0", + "@opentelemetry/sdk-node": "^0.221.0", "express": "^4.21.2", "mongoose": "^8.9.5" }, "engines": { - "node": ">=18" + "node": "^18.19.0 || >=20.6.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/@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/@mongodb-js/saslprep": { @@ -25,6 +71,618 @@ "sparse-bitfield": "^3.0.3" } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.221.0.tgz", + "integrity": "sha512-OlanaW1vv7ufTqQ3/fPLI4arGt5ZoM+P8abOMki6uEYnpRazepSWDwDnnw+la7kE26SHVC18//SMccrDvLKOXQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/configuration": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/configuration/-/configuration-0.221.0.tgz", + "integrity": "sha512-uE9y56Zdi9Gt/RdxYnVOo3YmFZkKJJMA0gqtBe8wh8gdtF5Asqe+Oh/TWiDtFb1s+31jNY4CWgnfIB1KOITfFA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "yaml": "^2.8.3" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0" + } + }, + "node_modules/@opentelemetry/context-async-hooks": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.10.0.tgz", + "integrity": "sha512-bvyMcgLEkozzSzpEEEo1OMoeQ97bxj6Qs2uN3mPrSdDvObMI1myffD/BPqcLlzZO9//d1SqQA/WPw7Cz2AiqhA==", + "license": "Apache-2.0", + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.10.0.tgz", + "integrity": "sha512-/wNZ8twnEQQA4HoHu22+vcsdru6pWPWxW+7w+FlxT6Id7PE/WIbZmVKkte+PF72e0F2dnImFeHD2syyE1Mw6MQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-grpc": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-grpc/-/exporter-logs-otlp-grpc-0.221.0.tgz", + "integrity": "sha512-txG1G0IrYSsKKMeiWZfj/i5cQmWB+h+hf3HzPpF3RqZVwp+iQQEIsv8Vtmzy6RWVdHdJZfygmVrBI39YTBvWcw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0", + "@opentelemetry/sdk-logs": "0.221.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-http": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.221.0.tgz", + "integrity": "sha512-nKXkr4Tomi6fjYVOf+ytcW3dZAVr4v4Bv5gsT6dr2gvpUPJpKgHB4XbMufMsPotRE3g0XH2GwVVCkN2w6SON+Q==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0", + "@opentelemetry/sdk-logs": "0.221.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.221.0.tgz", + "integrity": "sha512-AH6EY+47gXFaWYgG3hfeOneGiE9xIZGtDBk+9g0sM8NZWzsQhhmqPbQQXJzS7pyCh5jRRr2nYNXVrkCmoojRvQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0", + "@opentelemetry/sdk-logs": "0.221.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-grpc": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.221.0.tgz", + "integrity": "sha512-KOgCtO15FC6C1T/xOqBcr7EyUs7B+7yomGNb5Y97d3s38rPbCCk5sewkmE2b0/itOkQ/PptX8CLlD+kn2mEtTg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/exporter-metrics-otlp-http": "0.221.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.221.0.tgz", + "integrity": "sha512-sRfCKbOzgy8xZQV2as0RzIZlnCmCseCKZGLfRcrpo2CBngJDr+rPtX0zkG0+oUCV5kfQPUoW3W3C96Ag3Y/Clg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-metrics": "2.10.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-proto": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.221.0.tgz", + "integrity": "sha512-YMF4LveY2I3yhw61rn6nmC9FE8U24IZHPeKU1Duc5+sbwjMd8FwZAwba318ImdThCg/HuVQvhm2y6bfgNPnfYg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/exporter-metrics-otlp-http": "0.221.0", + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-prometheus": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.221.0.tgz", + "integrity": "sha512-kW79a20qWESIuAdDrxzg9WKM98twV/NBWBFRAH57ap/+ssZhiCo0hckzKT0zpuwR/gSHrFAQhJL0bYDrnEM34g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-metrics": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-grpc": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.221.0.tgz", + "integrity": "sha512-zXminlZedtq9LvOW64CnNkOqk15zV75k8JgtdTuWFge6+jk2m4GmAUm6L2eIiG1o2a2bZxXw2PDrszm+bps0IA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0", + "@opentelemetry/sdk-trace": "2.10.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-http": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.221.0.tgz", + "integrity": "sha512-AySXiKoC+meiWm6zdVj5T2LnPDZuatveBby1cMOeQteIWsYXAUxs8Sru13G2pVSPrUXz6vF+og7QVBX6GdC/oQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0", + "@opentelemetry/sdk-trace": "2.10.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.221.0.tgz", + "integrity": "sha512-Z9i2T7vgZbWe9rSLYxXVIbeW+XyzUq4rZanW3ZyVNwVDqCsh0EJKUgBWWQ0CZfeuUA+RQPzKgJQHMuWAUnKqXw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0", + "@opentelemetry/sdk-trace": "2.10.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-zipkin": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-2.10.0.tgz", + "integrity": "sha512-7gsvgf0UDoJ4l9ObrwBmz5G/ZogiPk+lq+g5GpLp24YQF/vPM/BSsnOfcLnfinast5ASUgLo78uSC/ObjlnXgg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-trace": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.221.0.tgz", + "integrity": "sha512-cCk80Z/iRDf/5gfsKMB4f74LqVA5yKETB/9ojPzVW/6/f70iu89nJvGxsFCxx4XfSohaOofkU19kiYm84AiAlw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.221.0", + "import-in-the-middle": "^3.0.0", + "require-in-the-middle": "^8.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-express": { + "version": "0.69.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.69.0.tgz", + "integrity": "sha512-91pHMujQgDyhEQrdg8RriMBrRZ/qPaJ0Y2dopQ6lHjW5YjoeytWi8ruM//T6f5o0D95hnqRlv79Pel1lGPqaYg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.9.0", + "@opentelemetry/instrumentation": "^0.221.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-http": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.221.0.tgz", + "integrity": "sha512-oIP91CPIANuYr09tGFElPFKAh6JUar+awJf1kBRYlaeo9b0gDwZHEB2zBfFlvdNFHm0wAVutMZODVi5smKT30g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/instrumentation": "0.221.0", + "@opentelemetry/semantic-conventions": "^1.29.0", + "forwarded-parse": "2.1.2" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.221.0.tgz", + "integrity": "sha512-UFPIq80OH3Ns/oPFHRj14d4DTOxUo+MUFU8hUiCq5jTqFhdeJnfVSANHT+xp92409cA+oxzvlZCe6NM1wvCuBA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/otlp-transformer": "0.221.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-grpc-exporter-base": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.221.0.tgz", + "integrity": "sha512-rQDmNgyiGCTrescjnzH2ntVyUKVIq6I2UjuK8+stT/Xg0ZOT71FVJqwjFdspQl6Yol/Yqsut9bDo+ame8oTmDQ==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.14.3", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.221.0.tgz", + "integrity": "sha512-lg6lkOU08Az23jVcn/0Els9HP+V8PnR4Km6p0KgpTggS0n/WuhnmY64rSh83Of9iR9nD+dpWr6adlcX8KzAwjg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.221.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-logs": "0.221.0", + "@opentelemetry/sdk-metrics": "2.10.0", + "@opentelemetry/sdk-trace": "2.10.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/propagator-b3": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-2.10.0.tgz", + "integrity": "sha512-GnA5B24H+1w8BO21J0q+IWNB0z1v+AGbcquTdIt/dufibhnhgxaA8YKvz0I3akRZhB1jHT+/tlzK+qlAjEDybQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/propagator-jaeger": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-2.10.0.tgz", + "integrity": "sha512-yw/IX8DL470dSMZJoE82ScfYGp7JWZ/G8kFJo35ZILUVTB2jFPTOaioN+8s09pH0RHsWNhweVZb+ZnjJJpCChg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.10.0.tgz", + "integrity": "sha512-q6MMm2zhggzsHVNbabYwut+a6nbuQQe3URUoxaojM/8K1IBfwwPzvxIjNi2/lI1TFe+fMHMW9MWhrtDLEXEnkA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.221.0.tgz", + "integrity": "sha512-FaDcazjyMp7TZZZAsqbo4IkovP0UegoCu0EBkiNt+qCqvUf7FPAsfcrZ3+ZEkKgXZ/jHafop+JoGPDk3A0SmLg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.221.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.10.0.tgz", + "integrity": "sha512-t6r1VSvXNtSDnPXU1FbZeetJb7yyovHmgu0wRSoftxtE0g2rSNhQZQUy69sRUCL+iioJpX8SN/S6wq6ZtvLySQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-node": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-node/-/sdk-node-0.221.0.tgz", + "integrity": "sha512-UbYuvtBrQQB5Prsh9KOKy4kxzexFxfMs5MkteHeWMoswsEB7kiNhyUVkAOFW/qsEzNHtrkgyghrD2ilZJa+5YA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.221.0", + "@opentelemetry/configuration": "0.221.0", + "@opentelemetry/context-async-hooks": "2.10.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/exporter-logs-otlp-grpc": "0.221.0", + "@opentelemetry/exporter-logs-otlp-http": "0.221.0", + "@opentelemetry/exporter-logs-otlp-proto": "0.221.0", + "@opentelemetry/exporter-metrics-otlp-grpc": "0.221.0", + "@opentelemetry/exporter-metrics-otlp-http": "0.221.0", + "@opentelemetry/exporter-metrics-otlp-proto": "0.221.0", + "@opentelemetry/exporter-prometheus": "0.221.0", + "@opentelemetry/exporter-trace-otlp-grpc": "0.221.0", + "@opentelemetry/exporter-trace-otlp-http": "0.221.0", + "@opentelemetry/exporter-trace-otlp-proto": "0.221.0", + "@opentelemetry/exporter-zipkin": "2.10.0", + "@opentelemetry/instrumentation": "0.221.0", + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.221.0", + "@opentelemetry/propagator-b3": "2.10.0", + "@opentelemetry/propagator-jaeger": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-logs": "0.221.0", + "@opentelemetry/sdk-metrics": "2.10.0", + "@opentelemetry/sdk-trace": "2.10.0", + "@opentelemetry/sdk-trace-base": "2.10.0", + "@opentelemetry/sdk-trace-node": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.10.0.tgz", + "integrity": "sha512-MfQGq3GRmTh5fM/y+OjaO0vj6+luCB1XO2gfXCalKCfgKw0eHL++sm75DNweC6ohlp+aFvACqeE0fYayqdRaoQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.10.0.tgz", + "integrity": "sha512-GuYQQT7QD2EeO8lcZLRQzcbOyhqAzL+6WWTKTU9mSUBYBazkEDl+VrQcXQhbB08OWM9anD1aHleVadzulpOaUQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-trace": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-node": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.10.0.tgz", + "integrity": "sha512-GZK/G6oZyBLGlH1pUgeDch7D91KoHd2uotUGIkWCPi9GI5T9X0p4L7nNAMDR1BQjkRYoDqo+ddfVx9t5Uhys+Q==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/context-async-hooks": "2.10.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/sdk-trace-base": "2.10.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.43.0.tgz", + "integrity": "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "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==", + "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==", + "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==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "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==", + "license": "BSD-3-Clause" + }, + "node_modules/@types/node": { + "version": "26.1.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.2.tgz", + "integrity": "sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==", + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, "node_modules/@types/webidl-conversions": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", @@ -53,6 +711,30 @@ "node": ">= 0.6" } }, + "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/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -130,6 +812,44 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -214,6 +934,12 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "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/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -241,6 +967,12 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", @@ -253,6 +985,15 @@ "node": ">= 0.4" } }, + "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/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -341,6 +1082,12 @@ "node": ">= 0.6" } }, + "node_modules/forwarded-parse": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz", + "integrity": "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==", + "license": "MIT" + }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -359,6 +1106,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -464,6 +1220,20 @@ "node": ">=0.10.0" } }, + "node_modules/import-in-the-middle": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.3.tgz", + "integrity": "sha512-AiohS3H80sXO6owEltjGX+glb7qXaDhBoJb9XcQVH4UI207xu/bDLUcadVKp7Qe576reg9yr/PXZjV5qx8gfbA==", + "license": "Apache-2.0", + "dependencies": { + "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", + "module-details-from-path": "^1.0.4" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -479,6 +1249,15 @@ "node": ">= 0.10" } }, + "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/kareem": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", @@ -488,6 +1267,18 @@ "node": ">=12.0.0" } }, + "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", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -563,6 +1354,12 @@ "node": ">= 0.6" } }, + "node_modules/module-details-from-path": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", + "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", + "license": "MIT" + }, "node_modules/mongodb": { "version": "6.20.0", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz", @@ -745,6 +1542,29 @@ "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", "license": "MIT" }, + "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/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -807,6 +1627,51 @@ "node": ">= 0.8" } }, + "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", + "integrity": "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "module-details-from-path": "^1.0.3" + }, + "engines": { + "node": ">=9.3.0 || >=8.10.0 <9.0.0" + } + }, + "node_modules/require-in-the-middle/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/require-in-the-middle/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -980,6 +1845,32 @@ "node": ">= 0.8" } }, + "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/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -1014,6 +1905,12 @@ "node": ">= 0.6" } }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "license": "MIT" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -1062,6 +1959,74 @@ "engines": { "node": ">=18" } + }, + "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/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "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/playgrounds/mongoose/app/package.json b/playgrounds/mongoose/app/package.json index f6d0335..c62ee8d 100644 --- a/playgrounds/mongoose/app/package.json +++ b/playgrounds/mongoose/app/package.json @@ -5,13 +5,18 @@ "license": "MIT", "type": "commonjs", "engines": { - "node": ">=18" + "node": "^18.19.0 || >=20.6.0" }, "scripts": { "start": "node server.js", "test:crud": "node mongoose-crud-test.js" }, "dependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/exporter-trace-otlp-grpc": "^0.221.0", + "@opentelemetry/instrumentation-express": "^0.69.0", + "@opentelemetry/instrumentation-http": "^0.221.0", + "@opentelemetry/sdk-node": "^0.221.0", "express": "^4.21.2", "mongoose": "^8.9.5" } diff --git a/playgrounds/mongoose/app/server.js b/playgrounds/mongoose/app/server.js index cd8a6b0..a442615 100644 --- a/playgrounds/mongoose/app/server.js +++ b/playgrounds/mongoose/app/server.js @@ -1,15 +1,28 @@ 'use strict'; +const { + installMongooseTracePropagation, + shutdownTelemetry, + traceDocumentDbOperation, + tracingEnabled, +} = require('./telemetry'); const express = require('express'); const mongoose = require('mongoose'); const { connect } = require('./db'); const Book = require('./models/book'); +installMongooseTracePropagation(mongoose); + const app = express(); app.use(express.json()); const PORT = Number(process.env.PORT || 3000); +const DB_NAME = process.env.MONGO_DB || 'mongoose_demo'; + +function traceBookOperation(operationName, operation) { + return traceDocumentDbOperation(operationName, DB_NAME, operation); +} // Liveness/readiness probe. Returns 200 only when the Mongoose connection is up. app.get('/health', (req, res) => { @@ -23,7 +36,7 @@ app.get('/health', (req, res) => { // Create a book. app.post('/books', async (req, res) => { try { - const book = await Book.create(req.body); + const book = await traceBookOperation('insert', () => Book.create(req.body)); res.status(201).json(book); } catch (err) { res.status(400).json({ error: err.message }); @@ -34,7 +47,9 @@ app.post('/books', async (req, res) => { app.get('/books', async (req, res) => { try { const filter = req.query.author ? { author: req.query.author } : {}; - const books = await Book.find(filter).sort({ createdAt: -1 }).limit(100).lean(); + const books = await traceBookOperation('find', () => + Book.find(filter).sort({ createdAt: -1 }).limit(100).lean() + ); res.json({ count: books.length, books }); } catch (err) { res.status(500).json({ error: err.message }); @@ -44,7 +59,9 @@ app.get('/books', async (req, res) => { // Fetch a single book by id. app.get('/books/:id', async (req, res) => { try { - const book = await Book.findById(req.params.id).lean(); + const book = await traceBookOperation('findOne', () => + Book.findById(req.params.id).lean() + ); if (!book) return res.status(404).json({ error: 'not found' }); res.json(book); } catch (err) { @@ -55,10 +72,12 @@ app.get('/books/:id', async (req, res) => { // Update a book. app.patch('/books/:id', async (req, res) => { try { - const book = await Book.findByIdAndUpdate(req.params.id, req.body, { - new: true, - runValidators: true, - }).lean(); + const book = await traceBookOperation('findOneAndUpdate', () => + Book.findByIdAndUpdate(req.params.id, req.body, { + new: true, + runValidators: true, + }).lean() + ); if (!book) return res.status(404).json({ error: 'not found' }); res.json(book); } catch (err) { @@ -69,7 +88,9 @@ app.patch('/books/:id', async (req, res) => { // Delete a book. app.delete('/books/:id', async (req, res) => { try { - const result = await Book.findByIdAndDelete(req.params.id).lean(); + const result = await traceBookOperation('findOneAndDelete', () => + Book.findByIdAndDelete(req.params.id).lean() + ); if (!result) return res.status(404).json({ error: 'not found' }); res.status(204).end(); } catch (err) { @@ -80,11 +101,13 @@ app.delete('/books/:id', async (req, res) => { // Simple aggregation: count books per genre. app.get('/stats/genres', async (req, res) => { try { - const stats = await Book.aggregate([ - { $unwind: '$genres' }, - { $group: { _id: '$genres', count: { $sum: 1 } } }, - { $sort: { count: -1 } }, - ]); + const stats = await traceBookOperation('aggregate', () => + Book.aggregate([ + { $unwind: '$genres' }, + { $group: { _id: '$genres', count: { $sum: 1 } } }, + { $sort: { count: -1 } }, + ]) + ); res.json(stats); } catch (err) { res.status(500).json({ error: err.message }); @@ -94,20 +117,34 @@ app.get('/stats/genres', async (req, res) => { async function main() { await connect(); console.log('Connected to DocumentDB via Mongoose'); + if (tracingEnabled) { + console.log('OpenTelemetry tracing enabled'); + } const server = app.listen(PORT, () => console.log(`Mongoose demo API listening on :${PORT}`) ); + let shuttingDown = false; const shutdown = (signal) => { + if (shuttingDown) { + return; + } + shuttingDown = true; console.log(`Received ${signal}, shutting down...`); server.close(() => { - mongoose.connection.close(false).finally(() => process.exit(0)); + Promise.all([mongoose.connection.close(false), shutdownTelemetry()]) + .then(() => process.exit(0)) + .catch((err) => { + console.error('Shutdown error:', err.message); + process.exit(1); + }); }); }; ['SIGTERM', 'SIGINT'].forEach((sig) => process.on(sig, () => shutdown(sig))); } -main().catch((err) => { +main().catch(async (err) => { console.error('Fatal startup error:', err.message); + await shutdownTelemetry(); process.exit(1); }); diff --git a/playgrounds/mongoose/app/telemetry.js b/playgrounds/mongoose/app/telemetry.js new file mode 100644 index 0000000..1851944 --- /dev/null +++ b/playgrounds/mongoose/app/telemetry.js @@ -0,0 +1,138 @@ +'use strict'; + +const { + context, + propagation, + SpanKind, + SpanStatusCode, + trace, +} = require('@opentelemetry/api'); + +const tracingEnabled = /^(1|true|yes)$/i.test(process.env.OTEL_TRACES_ENABLED || ''); + +let sdk; +if (tracingEnabled) { + const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-grpc'); + const { ExpressInstrumentation } = require('@opentelemetry/instrumentation-express'); + const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http'); + const { NodeSDK } = require('@opentelemetry/sdk-node'); + + sdk = new NodeSDK({ + traceExporter: new OTLPTraceExporter(), + instrumentations: [new HttpInstrumentation(), new ExpressInstrumentation()], + }); + sdk.start(); +} + +const tracer = trace.getTracer('documentdb-mongoose-playground'); +let propagationInstalled = false; + +const optionIndexes = new Map([ + ['aggregate', 1], + ['deleteMany', 1], + ['deleteOne', 1], + ['find', 1], + ['findOne', 1], + ['findOneAndDelete', 1], + ['findOneAndReplace', 2], + ['findOneAndUpdate', 2], + ['insertMany', 1], + ['insertOne', 1], + ['replaceOne', 2], + ['updateMany', 2], + ['updateOne', 2], +]); + +function currentTraceComment() { + const carrier = {}; + propagation.inject(context.active(), carrier); + if (typeof carrier.traceparent !== 'string') { + return undefined; + } + + const traceContext = { traceparent: carrier.traceparent }; + if (typeof carrier.tracestate === 'string') { + traceContext.tracestate = carrier.tracestate; + } + return JSON.stringify(traceContext); +} + +function installMongooseTracePropagation(mongoose) { + if (!tracingEnabled || propagationInstalled) { + return; + } + + const Collection = mongoose.mongo && mongoose.mongo.Collection; + if (!Collection) { + throw new Error('Mongoose does not expose the driver Collection type'); + } + + for (const [methodName, optionsIndex] of optionIndexes) { + const original = Collection.prototype[methodName]; + if (typeof original !== 'function') { + continue; + } + + Collection.prototype[methodName] = function tracedCollectionOperation(...args) { + const comment = currentTraceComment(); + if (comment) { + const options = args[optionsIndex]; + if (options == null) { + args[optionsIndex] = { comment }; + } else if (typeof options === 'object' && options.comment == null) { + args[optionsIndex] = { ...options, comment }; + } + } + return original.apply(this, args); + }; + } + + propagationInstalled = true; +} + +async function traceDocumentDbOperation(operationName, namespace, operation) { + if (!tracingEnabled) { + return operation(); + } + + return tracer.startActiveSpan( + `mongoose.${operationName}`, + { + kind: SpanKind.CLIENT, + attributes: { + 'db.system.name': 'documentdb', + 'db.operation.name': operationName, + 'db.namespace': namespace, + }, + }, + async (span) => { + try { + const result = await operation(); + span.setStatus({ code: SpanStatusCode.OK }); + return result; + } catch (err) { + span.recordException(err); + span.setStatus({ + code: SpanStatusCode.ERROR, + message: err && err.message ? err.message : String(err), + }); + throw err; + } finally { + span.end(); + } + } + ); +} + +async function shutdownTelemetry() { + if (sdk) { + await sdk.shutdown(); + } +} + +module.exports = { + installMongooseTracePropagation, + shutdownTelemetry, + traceDocumentDbOperation, + tracingEnabled, +}; diff --git a/playgrounds/mongoose/scripts/lib.sh b/playgrounds/mongoose/scripts/lib.sh index d64c60d..916785d 100755 --- a/playgrounds/mongoose/scripts/lib.sh +++ b/playgrounds/mongoose/scripts/lib.sh @@ -9,6 +9,7 @@ set -euo pipefail DOCUMENTDB_IMAGE="${DOCUMENTDB_IMAGE:-ghcr.io/documentdb/documentdb/documentdb-local:latest}" DOCUMENTDB_CONTAINER="${DOCUMENTDB_CONTAINER:-documentdb-local}" DOCUMENTDB_PORT="${DOCUMENTDB_PORT:-10260}" +DOCUMENTDB_CONTAINER_PORT=10260 DOCUMENTDB_USERNAME="${DOCUMENTDB_USERNAME:-docdbadmin}" DOCUMENTDB_PASSWORD="${DOCUMENTDB_PASSWORD:-Documentdb!Local1}" @@ -19,6 +20,10 @@ build_uri() { echo "mongodb://${DOCUMENTDB_USERNAME}:${DOCUMENTDB_PASSWORD}@localhost:${DOCUMENTDB_PORT}/?tls=true&tlsAllowInvalidCertificates=true&directConnection=true" } +build_container_uri() { + echo "mongodb://${DOCUMENTDB_USERNAME}:${DOCUMENTDB_PASSWORD}@localhost:${DOCUMENTDB_CONTAINER_PORT}/?tls=true&tlsAllowInvalidCertificates=true&directConnection=true" +} + require_docker() { command -v docker >/dev/null || { echo "docker is required (start Docker Desktop / the Docker daemon first)" >&2 @@ -69,7 +74,7 @@ ensure_documentdb() { # Poll the gateway until it responds to a MongoDB ping (or time out). wait_for_documentdb() { local uri - uri="$(build_uri)" + uri="$(build_container_uri)" echo "Waiting for DocumentDB to accept connections ..." local i diff --git a/playgrounds/mongoose/scripts/run-telemetry-demo.sh b/playgrounds/mongoose/scripts/run-telemetry-demo.sh new file mode 100755 index 0000000..c416b0c --- /dev/null +++ b/playgrounds/mongoose/scripts/run-telemetry-demo.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PLAYGROUND_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" +TELEMETRY_DIR="$PLAYGROUND_ROOT/shared/telemetry" +ENV_FILE="${TELEMETRY_ENV_FILE:-$TELEMETRY_DIR/.env}" + +if [ -f "$ENV_FILE" ]; then + set -a + # shellcheck disable=SC1090 + source "$ENV_FILE" + set +a +fi + +"$TELEMETRY_DIR/scripts/up.sh" + +export OTEL_TRACES_ENABLED=true +export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:${OTEL_COLLECTOR_GRPC_PORT:-4317}" +export OTEL_SERVICE_NAME="${OTEL_SERVICE_NAME:-documentdb-mongoose}" +export DOCUMENTDB_IMAGE="${DOCUMENTDB_IMAGE:-documentdb-local:gateway-tracing}" + +echo "" +echo "Application tracing is enabled." +echo "After the API starts, run this in another terminal:" +echo " $SCRIPT_DIR/verify-telemetry.sh" +echo "" + +exec "$SCRIPT_DIR/run-app.sh" diff --git a/playgrounds/mongoose/scripts/verify-telemetry.sh b/playgrounds/mongoose/scripts/verify-telemetry.sh new file mode 100755 index 0000000..6ccc450 --- /dev/null +++ b/playgrounds/mongoose/scripts/verify-telemetry.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PLAYGROUND_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" +TELEMETRY_DIR="$PLAYGROUND_ROOT/shared/telemetry" +ENV_FILE="${TELEMETRY_ENV_FILE:-$TELEMETRY_DIR/.env}" + +if [ -f "$ENV_FILE" ]; then + set -a + # shellcheck disable=SC1090 + source "$ENV_FILE" + set +a +fi + +APP_SERVICE_NAME="${OTEL_SERVICE_NAME:-documentdb-mongoose}" +GATEWAY_SERVICE_NAME="${GATEWAY_SERVICE_NAME:-documentdb_gateway}" +API_URL="${API_URL:-http://localhost:${PORT:-3000}}" +JAEGER_URL="${JAEGER_URL:-http://localhost:${JAEGER_UI_PORT:-16686}}" +CURL_TIMEOUT_SECONDS="${CURL_TIMEOUT_SECONDS:-10}" +TRACE_WAIT_TIMEOUT_SECONDS="${TRACE_WAIT_TIMEOUT_SECONDS:-60}" +unique_id="$(date +%s)" + +command -v curl >/dev/null || { + echo "curl is required" >&2 + exit 1 +} +command -v python3 >/dev/null || { + echo "python3 is required" >&2 + exit 1 +} + +start_time_us="$(python3 -c 'import time; print(time.time_ns() // 1000)')" +curl_args=(--connect-timeout 2 --max-time "$CURL_TIMEOUT_SECONDS" -fsS) + +curl "${curl_args[@]}" "$API_URL/health" >/dev/null +curl "${curl_args[@]}" -X POST "$API_URL/books" \ + -H 'Content-Type: application/json' \ + -d "{\"title\":\"Tracing Demo $unique_id\",\"author\":\"DocumentDB\",\"genres\":[\"telemetry\"],\"pages\":1}" \ + >/dev/null +curl "${curl_args[@]}" "$API_URL/books?author=DocumentDB" >/dev/null +curl "${curl_args[@]}" "$API_URL/stats/genres" >/dev/null + +trace_file="$(mktemp)" +cleanup() { + rm -f -- "$trace_file" +} +trap cleanup EXIT + +deadline=$((SECONDS + TRACE_WAIT_TIMEOUT_SECONDS)) +while [ "$SECONDS" -lt "$deadline" ]; do + if curl "${curl_args[@]}" -G \ + "$JAEGER_URL/api/traces" \ + --data-urlencode "service=$APP_SERVICE_NAME" \ + --data-urlencode "limit=100" \ + --data-urlencode "lookback=1h" \ + >"$trace_file" && + trace_id="$( + python3 - \ + "$trace_file" \ + "$start_time_us" \ + "$APP_SERVICE_NAME" \ + "$GATEWAY_SERVICE_NAME" <<'PY' +import json +import sys + +with open(sys.argv[1], encoding="utf-8") as handle: + payload = json.load(handle) + +start_time_us = int(sys.argv[2]) +app_service_name = sys.argv[3] +gateway_service_name = sys.argv[4] + +for trace in payload.get("data", []): + processes = trace.get("processes", {}) + service_names = { + process.get("serviceName") + for process in processes.values() + if process.get("serviceName") + } + operation_names = { + span.get("operationName") + for span in trace.get("spans", []) + if span.get("operationName") + } + is_current_run = any( + int(span.get("startTime", 0)) >= start_time_us + for span in trace.get("spans", []) + ) + if ( + is_current_run + and app_service_name in service_names + and gateway_service_name in service_names + and "gateway.request" in operation_names + ): + print(trace.get("traceID", "")) + break +PY + )" && + [ -n "$trace_id" ]; then + echo "Verified a connected Mongoose and gateway trace." + echo "$JAEGER_URL/trace/$trace_id" + exit 0 + fi + sleep 2 +done + +echo "No connected Mongoose and gateway trace appeared in Jaeger within ${TRACE_WAIT_TIMEOUT_SECONDS}s." >&2 +exit 1 diff --git a/playgrounds/pymongo/scripts/lib.sh b/playgrounds/pymongo/scripts/lib.sh index c030dda..11892ed 100755 --- a/playgrounds/pymongo/scripts/lib.sh +++ b/playgrounds/pymongo/scripts/lib.sh @@ -10,6 +10,7 @@ DOCUMENTDB_CONTAINER="${DOCUMENTDB_CONTAINER:-documentdb-local}" DOCUMENTDB_IMAGE="${DOCUMENTDB_IMAGE:-ghcr.io/documentdb/documentdb/documentdb-local:latest}" DOCUMENTDB_HOST="${DOCUMENTDB_HOST:-localhost}" DOCUMENTDB_PORT="${DOCUMENTDB_PORT:-10260}" +DOCUMENTDB_CONTAINER_PORT=10260 # Note: the emulator rejects some reserved names (e.g. "documentdb"); use a # distinct admin username. DOCUMENTDB_USERNAME="${DOCUMENTDB_USERNAME:-docdbadmin}" @@ -22,6 +23,10 @@ build_uri() { echo "mongodb://${DOCUMENTDB_USERNAME}:${DOCUMENTDB_PASSWORD}@${DOCUMENTDB_HOST}:${DOCUMENTDB_PORT}/?tls=true&tlsAllowInvalidCertificates=true&directConnection=true" } +build_container_uri() { + echo "mongodb://${DOCUMENTDB_USERNAME}:${DOCUMENTDB_PASSWORD}@localhost:${DOCUMENTDB_CONTAINER_PORT}/?tls=true&tlsAllowInvalidCertificates=true&directConnection=true" +} + # Wait until a TCP port accepts connections. # Args: [tries] wait_for_port() { @@ -95,7 +100,7 @@ ensure_documentdb() { wait_for_documentdb() { local uri - uri="$(build_uri)" + uri="$(build_container_uri)" echo "Waiting for DocumentDB to accept connections ..." local i diff --git a/shared/telemetry/.env.example b/shared/telemetry/.env.example new file mode 100644 index 0000000..e19bdaa --- /dev/null +++ b/shared/telemetry/.env.example @@ -0,0 +1,13 @@ +DOCUMENTDB_IMAGE=documentdb-local:gateway-tracing +DOCUMENTDB_USERNAME=docdbadmin +DOCUMENTDB_PASSWORD=Documentdb!Local1 +DOCUMENTDB_PORT=10260 +DOCUMENTDB_SERVICE_VERSION=local + +OTEL_COLLECTOR_GRPC_PORT=4317 +OTEL_COLLECTOR_HTTP_PORT=4318 +OTEL_COLLECTOR_HEALTH_PORT=13133 +OTEL_TRACES_SAMPLER_ARG=1.0 +DOCUMENTDB_SQL_COMMENTER_ENABLED=false + +JAEGER_UI_PORT=16686 diff --git a/shared/telemetry/README.md b/shared/telemetry/README.md new file mode 100644 index 0000000..6b7589b --- /dev/null +++ b/shared/telemetry/README.md @@ -0,0 +1,108 @@ +# Shared telemetry stack + +This stack runs a tracing-enabled local DocumentDB image, an OpenTelemetry +Collector, and Jaeger. Every playground can use the same `documentdb-local` +container on port `10260`. + +```text +Application or test + -> documentdb-local:10260 + -> OpenTelemetry Collector:4317 + -> Jaeger:4317 + -> Jaeger UI:16686 +``` + +The local DocumentDB image contains PostgreSQL, the DocumentDB extensions, and +the gateway in one container. The image build has two stages, but the demo does +not need separate PostgreSQL and gateway services. + +## Prerequisites + +- Docker with Docker Compose +- A DocumentDB source checkout containing the gateway tracing changes +- Bash 3.2 or newer +- `curl` +- Python 3 for the Mongoose trace verification helper + +## 1. Build the custom image + +Run the build helper with a DocumentDB source directory. The directory can be a +standalone source checkout or a parent repository whose source is under `oss/`. + +```bash +DOCUMENTDB_SOURCE_DIR=/path/to/documentdb \ + ./scripts/build-documentdb-image.sh +``` + +The default local tag is `documentdb-local:gateway-tracing`. Override it with +`DOCUMENTDB_IMAGE` or the script's second positional argument: + +```bash +./scripts/build-documentdb-image.sh \ + /path/to/documentdb \ + documentdb-local:my-tracing-build +``` + +The helper first builds the DocumentDB extension package, then builds the +self-contained image with the gateway binary from the same source revision. + +## 2. Configure the stack + +The checked-in defaults are sufficient for local use. To customize them: + +```bash +cp .env.example .env +``` + +Important settings: + +| Variable | Default | Purpose | +| --- | --- | --- | +| `DOCUMENTDB_IMAGE` | `documentdb-local:gateway-tracing` | Local image containing the tracing changes. | +| `DOCUMENTDB_PORT` | `10260` | Host gateway port. | +| `OTEL_COLLECTOR_GRPC_PORT` | `4317` | Host OTLP/gRPC port for local applications. | +| `OTEL_TRACES_SAMPLER_ARG` | `1.0` | Gateway trace sampling ratio. | +| `DOCUMENTDB_SQL_COMMENTER_ENABLED` | `false` | Add sampled trace context to generated SQL for log correlation. | +| `JAEGER_UI_PORT` | `16686` | Host Jaeger UI port. | + +The gateway configuration is mounted from +[`gateway-setup.json`](gateway-setup.json). Tracing must be enabled there +because JSON configuration takes precedence over environment variables. + +## 3. Start or stop the stack + +```bash +./scripts/up.sh +open http://localhost:16686 +``` + +Stop the containers while preserving DocumentDB data: + +```bash +./scripts/down.sh +``` + +Remove the containers and named volumes: + +```bash +./scripts/down.sh --volumes +``` + +## Use a playground + +Once the stack is running, the existing playground scripts reuse its +`documentdb-local` container automatically. + +For the connected Mongoose application and gateway trace: + +```bash +cd ../../playgrounds/mongoose +./scripts/run-telemetry-demo.sh +``` + +Then run `./scripts/verify-telemetry.sh` in another terminal. The verification +script prints a direct Jaeger trace URL when the application and gateway spans +are joined successfully. + +Other playgrounds can use the same stack by starting it here first and then +running their normal application or test scripts. diff --git a/shared/telemetry/compose.yaml b/shared/telemetry/compose.yaml new file mode 100644 index 0000000..9aab341 --- /dev/null +++ b/shared/telemetry/compose.yaml @@ -0,0 +1,53 @@ +name: documentdb-telemetry + +services: + jaeger: + image: cr.jaegertracing.io/jaegertracing/jaeger:2.20.0 + ports: + - "127.0.0.1:${JAEGER_UI_PORT:-16686}:16686" + + otel-collector: + image: otel/opentelemetry-collector-contrib:0.157.0 + command: ["--config=/etc/otelcol-contrib/config.yaml"] + depends_on: + - jaeger + ports: + - "127.0.0.1:${OTEL_COLLECTOR_GRPC_PORT:-4317}:4317" + - "127.0.0.1:${OTEL_COLLECTOR_HTTP_PORT:-4318}:4318" + - "127.0.0.1:${OTEL_COLLECTOR_HEALTH_PORT:-13133}:13133" + volumes: + - ./otel-collector.yaml:/etc/otelcol-contrib/config.yaml:ro + - documentdb-logs:/var/log/documentdb:ro + + documentdb: + image: ${DOCUMENTDB_IMAGE:-documentdb-local:gateway-tracing} + container_name: documentdb-local + depends_on: + - otel-collector + environment: + USERNAME: ${DOCUMENTDB_USERNAME:-docdbadmin} + PASSWORD: ${DOCUMENTDB_PASSWORD:-Documentdb!Local1} + INIT_DATA: "false" + OTEL_SERVICE_VERSION: ${DOCUMENTDB_SERVICE_VERSION:-local} + OTEL_TRACES_SAMPLER_ARG: ${OTEL_TRACES_SAMPLER_ARG:-1.0} + DOCUMENTDB_SQL_COMMENTER_ENABLED: ${DOCUMENTDB_SQL_COMMENTER_ENABLED:-false} + ports: + - "127.0.0.1:${DOCUMENTDB_PORT:-10260}:10260" + volumes: + - ./gateway-setup.json:/home/documentdb/gateway/pg_documentdb_gw/SetupConfiguration.json:ro + - documentdb-data:/data + - documentdb-logs:/var/log/documentdb + healthcheck: + test: + [ + "CMD-SHELL", + "echo | openssl s_client -connect localhost:10260 -brief 2>&1 | grep -q 'CONNECTION ESTABLISHED'", + ] + interval: 5s + timeout: 5s + retries: 60 + start_period: 30s + +volumes: + documentdb-data: + documentdb-logs: diff --git a/shared/telemetry/gateway-setup.json b/shared/telemetry/gateway-setup.json new file mode 100644 index 0000000..ed8b857 --- /dev/null +++ b/shared/telemetry/gateway-setup.json @@ -0,0 +1,24 @@ +{ + "NodeHostName": "localhost", + "BlockedRolePrefixes": [ + "documentdb", + "citus", + "pg", + "internal_role" + ], + "PostgresPort": 9712, + "GatewayListenPort": 10260, + "HostConfigurationWatchIntervalMs": 1000, + "CertificateOptions": { + "CertType": "PemAutoGenerated" + }, + "UseLocalHost": false, + "TelemetryOptions": { + "ServiceName": "documentdb_gateway", + "Tracing": { + "Enabled": true, + "OtlpEndpoint": "http://otel-collector:4317", + "ExportTimeoutMs": 10000 + } + } +} diff --git a/shared/telemetry/otel-collector.yaml b/shared/telemetry/otel-collector.yaml new file mode 100644 index 0000000..c90093d --- /dev/null +++ b/shared/telemetry/otel-collector.yaml @@ -0,0 +1,37 @@ +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + http: + endpoint: 0.0.0.0:4318 + +processors: + batch: + timeout: 1s + send_batch_size: 1024 + resource: + attributes: + - key: deployment.environment + value: local-demo + action: upsert + +exporters: + otlp_grpc/jaeger: + endpoint: jaeger:4317 + tls: + insecure: true + debug: + verbosity: basic + +extensions: + health_check: + endpoint: 0.0.0.0:13133 + +service: + extensions: [health_check] + pipelines: + traces: + receivers: [otlp] + processors: [resource, batch] + exporters: [otlp_grpc/jaeger, debug] diff --git a/shared/telemetry/scripts/_lib.sh b/shared/telemetry/scripts/_lib.sh new file mode 100755 index 0000000..28f9c26 --- /dev/null +++ b/shared/telemetry/scripts/_lib.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TELEMETRY_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +COMPOSE_FILE="$TELEMETRY_DIR/compose.yaml" +ENV_FILE="${TELEMETRY_ENV_FILE:-$TELEMETRY_DIR/.env}" + +load_telemetry_env() { + if [ -f "$ENV_FILE" ]; then + set -a + # shellcheck disable=SC1090 + source "$ENV_FILE" + set +a + fi + + export DOCUMENTDB_IMAGE="${DOCUMENTDB_IMAGE:-documentdb-local:gateway-tracing}" + export DOCUMENTDB_USERNAME="${DOCUMENTDB_USERNAME:-docdbadmin}" + export DOCUMENTDB_PASSWORD="${DOCUMENTDB_PASSWORD:-Documentdb!Local1}" + export DOCUMENTDB_PORT="${DOCUMENTDB_PORT:-10260}" + export DOCUMENTDB_SERVICE_VERSION="${DOCUMENTDB_SERVICE_VERSION:-local}" + export OTEL_COLLECTOR_GRPC_PORT="${OTEL_COLLECTOR_GRPC_PORT:-4317}" + export OTEL_COLLECTOR_HTTP_PORT="${OTEL_COLLECTOR_HTTP_PORT:-4318}" + export OTEL_COLLECTOR_HEALTH_PORT="${OTEL_COLLECTOR_HEALTH_PORT:-13133}" + export OTEL_TRACES_SAMPLER_ARG="${OTEL_TRACES_SAMPLER_ARG:-1.0}" + export DOCUMENTDB_SQL_COMMENTER_ENABLED="${DOCUMENTDB_SQL_COMMENTER_ENABLED:-false}" + export JAEGER_UI_PORT="${JAEGER_UI_PORT:-16686}" +} + +compose() { + local args=( + --project-directory "$TELEMETRY_DIR" + -f "$COMPOSE_FILE" + ) + if [ -f "$ENV_FILE" ]; then + args+=(--env-file "$ENV_FILE") + fi + docker compose "${args[@]}" "$@" +} + +require_command() { + local command_name="$1" + command -v "$command_name" >/dev/null || { + echo "$command_name is required" >&2 + exit 1 + } +} + +wait_for_url() { + local name="$1" + local url="$2" + local timeout_seconds="${3:-120}" + local deadline=$((SECONDS + timeout_seconds)) + + while true; do + if curl --connect-timeout 2 --max-time 5 -fsS "$url" >/dev/null 2>&1; then + return 0 + fi + if [ "$SECONDS" -ge "$deadline" ]; then + echo "$name did not become ready within ${timeout_seconds}s: $url" >&2 + return 1 + fi + sleep 2 + done +} + +load_telemetry_env diff --git a/shared/telemetry/scripts/build-documentdb-image.sh b/shared/telemetry/scripts/build-documentdb-image.sh new file mode 100755 index 0000000..6a30d13 --- /dev/null +++ b/shared/telemetry/scripts/build-documentdb-image.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SOURCE_INPUT="${1:-${DOCUMENTDB_SOURCE_DIR:-$HOME/repos/documentdb}}" +IMAGE_TAG="${2:-${DOCUMENTDB_IMAGE:-documentdb-local:gateway-tracing}}" +POSTGRES_VERSION="${POSTGRES_VERSION:-17}" +PACKAGE_OS="${PACKAGE_OS:-deb13}" +BASE_IMAGE="${BASE_IMAGE:-debian:trixie-slim}" + +if [ -d "$SOURCE_INPUT/oss/packaging" ]; then + SOURCE_INPUT="$SOURCE_INPUT/oss" +fi + +if [ ! -d "$SOURCE_INPUT" ]; then + echo "DocumentDB source directory does not exist: $SOURCE_INPUT" >&2 + exit 1 +fi + +SOURCE_DIR="$(cd "$SOURCE_INPUT" && pwd -P)" +for required_path in \ + packaging/build_packages.sh \ + packaging/gateway/docker/Dockerfile_documentdb_local \ + pg_documentdb_gw; do + if [ ! -e "$SOURCE_DIR/$required_path" ]; then + echo "DocumentDB source is missing $required_path: $SOURCE_DIR" >&2 + exit 1 + fi +done + +command -v docker >/dev/null || { + echo "docker is required" >&2 + exit 1 +} +docker info >/dev/null 2>&1 || { + echo "Cannot reach the Docker daemon" >&2 + exit 1 +} + +BUILD_DIR="$(mktemp -d "$SOURCE_DIR/.playground-image-build.XXXXXX")" +cleanup() { + rm -rf -- "$BUILD_DIR" +} +trap cleanup EXIT + +OUTPUT_REL="${BUILD_DIR#"$SOURCE_DIR"/}" +SOURCE_REVISION="$(git -C "$SOURCE_DIR" rev-parse --short HEAD 2>/dev/null || echo unknown)" +if [ -n "$(git -C "$SOURCE_DIR" status --porcelain 2>/dev/null || true)" ]; then + SOURCE_REVISION="${SOURCE_REVISION}-dirty" +fi + +echo "Building DocumentDB image" +echo " Source: $SOURCE_DIR ($SOURCE_REVISION)" +echo " PostgreSQL: $POSTGRES_VERSION" +echo " Image: $IMAGE_TAG" + +( + cd "$SOURCE_DIR" + ./packaging/build_packages.sh \ + --os "$PACKAGE_OS" \ + --pg "$POSTGRES_VERSION" \ + --output-dir "$OUTPUT_REL" +) + +packages=() +for package in "$BUILD_DIR"/*.deb; do + [ -f "$package" ] || continue + case "$package" in + *dbgsym*) continue ;; + esac + packages+=("$package") +done +if [ "${#packages[@]}" -ne 1 ]; then + echo "Expected exactly one non-debug Debian package, found ${#packages[@]}" >&2 + printf ' %s\n' "${packages[@]}" >&2 + exit 1 +fi + +PACKAGE_REL="${packages[0]#"$SOURCE_DIR"/}" +( + cd "$SOURCE_DIR" + docker build \ + --build-arg BASE_IMAGE="$BASE_IMAGE" \ + --build-arg POSTGRES_VERSION="$POSTGRES_VERSION" \ + --build-arg DEB_PACKAGE_REL_PATH="$PACKAGE_REL" \ + --label "com.documentdb.playground.source-revision=$SOURCE_REVISION" \ + -t "$IMAGE_TAG" \ + -f packaging/gateway/docker/Dockerfile_documentdb_local . +) + +docker image inspect "$IMAGE_TAG" \ + --format 'Built {{index .RepoTags 0}} ({{.Architecture}}, {{.Size}} bytes)' diff --git a/shared/telemetry/scripts/down.sh b/shared/telemetry/scripts/down.sh new file mode 100755 index 0000000..0a412f7 --- /dev/null +++ b/shared/telemetry/scripts/down.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=_lib.sh +source "$SCRIPT_DIR/_lib.sh" + +args=(down --remove-orphans) +if [ "${1:-}" = "--volumes" ]; then + args+=(--volumes) +fi + +compose "${args[@]}" diff --git a/shared/telemetry/scripts/up.sh b/shared/telemetry/scripts/up.sh new file mode 100755 index 0000000..2e06f52 --- /dev/null +++ b/shared/telemetry/scripts/up.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=_lib.sh +source "$SCRIPT_DIR/_lib.sh" + +require_command docker +require_command curl +docker info >/dev/null 2>&1 || { + echo "Cannot reach the Docker daemon" >&2 + exit 1 +} + +if ! docker image inspect "$DOCUMENTDB_IMAGE" >/dev/null 2>&1; then + cat >&2 </dev/null 2>&1; then + existing_project="$( + docker inspect \ + --format '{{ index .Config.Labels "com.docker.compose.project" }}' \ + documentdb-local 2>/dev/null || true + )" + if [ "$existing_project" != "documentdb-telemetry" ]; then + echo "A container named documentdb-local already exists outside this stack." >&2 + echo "Stop it with its owning playground before starting telemetry." >&2 + exit 1 + fi +fi + +compose up -d + +wait_for_url \ + "OpenTelemetry Collector" \ + "http://127.0.0.1:${OTEL_COLLECTOR_HEALTH_PORT}/" \ + 120 +wait_for_url \ + "Jaeger" \ + "http://127.0.0.1:${JAEGER_UI_PORT}/api/services" \ + 120 + +elapsed=0 +while [ "$(docker inspect --format '{{.State.Health.Status}}' documentdb-local)" != "healthy" ]; do + if [ "$elapsed" -ge 240 ]; then + echo "DocumentDB did not become healthy within 240s" >&2 + compose logs --no-color documentdb + exit 1 + fi + sleep 2 + elapsed=$((elapsed + 2)) +done + +echo "" +echo "DocumentDB telemetry stack is ready." +echo " Gateway: mongodb://localhost:${DOCUMENTDB_PORT}" +echo " OTLP/gRPC: http://localhost:${OTEL_COLLECTOR_GRPC_PORT}" +echo " OTLP/HTTP: http://localhost:${OTEL_COLLECTOR_HTTP_PORT}" +echo " Jaeger UI: http://localhost:${JAEGER_UI_PORT}" From 5b6b9dbb57d80d9fe300d2c497093ce7c20a0a7b Mon Sep 17 00:00:00 2001 From: urismiley Date: Fri, 7 Aug 2026 15:46:19 -0400 Subject: [PATCH 2/3] Use the published gateway tracing image. Copilot-Session: 85f151f9-f585-4bdf-9c2b-798f8a2651be --- README.md | 4 +- playgrounds/mongoose/README.md | 26 ++---- .../mongoose/scripts/run-telemetry-demo.sh | 2 +- shared/telemetry/.env.example | 2 +- shared/telemetry/README.md | 51 ++++------ shared/telemetry/compose.yaml | 2 +- shared/telemetry/scripts/_lib.sh | 2 +- .../scripts/build-documentdb-image.sh | 92 ------------------- shared/telemetry/scripts/up.sh | 11 --- 9 files changed, 35 insertions(+), 157 deletions(-) delete mode 100755 shared/telemetry/scripts/build-documentdb-image.sh diff --git a/README.md b/README.md index 55cafa2..e7bbb0f 100644 --- a/README.md +++ b/README.md @@ -77,8 +77,8 @@ cd playgrounds/mongoose ./scripts/run-telemetry-demo.sh ``` -See the [shared telemetry guide](shared/telemetry/README.md) for image build, -configuration, and verification instructions. +See the [shared telemetry guide](shared/telemetry/README.md) for image, +configuration, and verification details. ## Repository Layout diff --git a/playgrounds/mongoose/README.md b/playgrounds/mongoose/README.md index 9edac64..5d61eee 100644 --- a/playgrounds/mongoose/README.md +++ b/playgrounds/mongoose/README.md @@ -11,8 +11,8 @@ instance running in Docker. It includes: The normal app and test paths run entirely on your machine: DocumentDB in a Docker container, with the app and tests as local Node.js processes. The -optional tracing demo builds a custom local image containing the gateway -tracing changes. +optional tracing demo uses a published image containing the gateway tracing +changes. > **What is Mongoose?** Mongoose is a **Node.js library** (an ODM, Object Data > Modeling layer), not a CLI tool or a server. Your application imports it to @@ -83,10 +83,11 @@ Stop the database when you're done: ## Trace the application and gateway -The shared telemetry stack runs a custom DocumentDB image, an OpenTelemetry -Collector, and Jaeger. The Mongoose app emits HTTP and client spans, then puts -the active W3C trace context in each supported database command's `comment` -field. The gateway uses that context as the parent of `gateway.request`. +The shared telemetry stack runs a tracing-enabled DocumentDB image, an +OpenTelemetry Collector, and Jaeger. The Mongoose app emits HTTP and client +spans, then puts the active W3C trace context in each supported database +command's `comment` field. The gateway uses that context as the parent of +`gateway.request`. ```text HTTP request @@ -97,16 +98,9 @@ HTTP request -> postgres.execute ``` -First build a local image from a DocumentDB source checkout that contains the -gateway tracing changes: - -```bash -cd ../../shared/telemetry -DOCUMENTDB_SOURCE_DIR=/path/to/documentdb \ - ./scripts/build-documentdb-image.sh -``` - -Start the telemetry stack and Mongoose API: +Start the telemetry stack and Mongoose API. The stack pulls +`ghcr.io/documentdb/documentdb/documentdb-local:trace-4fbbfcb8` +automatically: ```bash cd ../../playgrounds/mongoose diff --git a/playgrounds/mongoose/scripts/run-telemetry-demo.sh b/playgrounds/mongoose/scripts/run-telemetry-demo.sh index c416b0c..481b158 100755 --- a/playgrounds/mongoose/scripts/run-telemetry-demo.sh +++ b/playgrounds/mongoose/scripts/run-telemetry-demo.sh @@ -19,7 +19,7 @@ fi export OTEL_TRACES_ENABLED=true export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:${OTEL_COLLECTOR_GRPC_PORT:-4317}" export OTEL_SERVICE_NAME="${OTEL_SERVICE_NAME:-documentdb-mongoose}" -export DOCUMENTDB_IMAGE="${DOCUMENTDB_IMAGE:-documentdb-local:gateway-tracing}" +export DOCUMENTDB_IMAGE="${DOCUMENTDB_IMAGE:-ghcr.io/documentdb/documentdb/documentdb-local:trace-4fbbfcb8}" echo "" echo "Application tracing is enabled." diff --git a/shared/telemetry/.env.example b/shared/telemetry/.env.example index e19bdaa..1ef3df8 100644 --- a/shared/telemetry/.env.example +++ b/shared/telemetry/.env.example @@ -1,4 +1,4 @@ -DOCUMENTDB_IMAGE=documentdb-local:gateway-tracing +DOCUMENTDB_IMAGE=ghcr.io/documentdb/documentdb/documentdb-local:trace-4fbbfcb8 DOCUMENTDB_USERNAME=docdbadmin DOCUMENTDB_PASSWORD=Documentdb!Local1 DOCUMENTDB_PORT=10260 diff --git a/shared/telemetry/README.md b/shared/telemetry/README.md index 6b7589b..cd2b0cc 100644 --- a/shared/telemetry/README.md +++ b/shared/telemetry/README.md @@ -1,8 +1,8 @@ # Shared telemetry stack -This stack runs a tracing-enabled local DocumentDB image, an OpenTelemetry -Collector, and Jaeger. Every playground can use the same `documentdb-local` -container on port `10260`. +This stack runs a tracing-enabled DocumentDB image, an OpenTelemetry Collector, +and Jaeger. Every playground can use the same `documentdb-local` container on +port `10260`. ```text Application or test @@ -12,41 +12,28 @@ Application or test -> Jaeger UI:16686 ``` -The local DocumentDB image contains PostgreSQL, the DocumentDB extensions, and -the gateway in one container. The image build has two stages, but the demo does -not need separate PostgreSQL and gateway services. +The DocumentDB image contains PostgreSQL 17, the DocumentDB extensions, and the +gateway in one container. The demo does not need separate PostgreSQL and +gateway services. + +By default, the stack pulls this image: + +```text +ghcr.io/documentdb/documentdb/documentdb-local:trace-4fbbfcb8 +``` + +It was built from DocumentDB commit +[`4fbbfcb8`](https://github.com/documentdb/documentdb/commit/4fbbfcb879cfdedaab5e5f4b37698fbc28f0a7e9), +which contains the gateway tracing support used by this demo. ## Prerequisites - Docker with Docker Compose -- A DocumentDB source checkout containing the gateway tracing changes - Bash 3.2 or newer - `curl` - Python 3 for the Mongoose trace verification helper -## 1. Build the custom image - -Run the build helper with a DocumentDB source directory. The directory can be a -standalone source checkout or a parent repository whose source is under `oss/`. - -```bash -DOCUMENTDB_SOURCE_DIR=/path/to/documentdb \ - ./scripts/build-documentdb-image.sh -``` - -The default local tag is `documentdb-local:gateway-tracing`. Override it with -`DOCUMENTDB_IMAGE` or the script's second positional argument: - -```bash -./scripts/build-documentdb-image.sh \ - /path/to/documentdb \ - documentdb-local:my-tracing-build -``` - -The helper first builds the DocumentDB extension package, then builds the -self-contained image with the gateway binary from the same source revision. - -## 2. Configure the stack +## 1. Configure the stack The checked-in defaults are sufficient for local use. To customize them: @@ -58,7 +45,7 @@ Important settings: | Variable | Default | Purpose | | --- | --- | --- | -| `DOCUMENTDB_IMAGE` | `documentdb-local:gateway-tracing` | Local image containing the tracing changes. | +| `DOCUMENTDB_IMAGE` | `ghcr.io/documentdb/documentdb/documentdb-local:trace-4fbbfcb8` | Published image containing the tracing changes. | | `DOCUMENTDB_PORT` | `10260` | Host gateway port. | | `OTEL_COLLECTOR_GRPC_PORT` | `4317` | Host OTLP/gRPC port for local applications. | | `OTEL_TRACES_SAMPLER_ARG` | `1.0` | Gateway trace sampling ratio. | @@ -69,7 +56,7 @@ The gateway configuration is mounted from [`gateway-setup.json`](gateway-setup.json). Tracing must be enabled there because JSON configuration takes precedence over environment variables. -## 3. Start or stop the stack +## 2. Start or stop the stack ```bash ./scripts/up.sh diff --git a/shared/telemetry/compose.yaml b/shared/telemetry/compose.yaml index 9aab341..bed3841 100644 --- a/shared/telemetry/compose.yaml +++ b/shared/telemetry/compose.yaml @@ -20,7 +20,7 @@ services: - documentdb-logs:/var/log/documentdb:ro documentdb: - image: ${DOCUMENTDB_IMAGE:-documentdb-local:gateway-tracing} + image: ${DOCUMENTDB_IMAGE:-ghcr.io/documentdb/documentdb/documentdb-local:trace-4fbbfcb8} container_name: documentdb-local depends_on: - otel-collector diff --git a/shared/telemetry/scripts/_lib.sh b/shared/telemetry/scripts/_lib.sh index 28f9c26..1706820 100755 --- a/shared/telemetry/scripts/_lib.sh +++ b/shared/telemetry/scripts/_lib.sh @@ -15,7 +15,7 @@ load_telemetry_env() { set +a fi - export DOCUMENTDB_IMAGE="${DOCUMENTDB_IMAGE:-documentdb-local:gateway-tracing}" + export DOCUMENTDB_IMAGE="${DOCUMENTDB_IMAGE:-ghcr.io/documentdb/documentdb/documentdb-local:trace-4fbbfcb8}" export DOCUMENTDB_USERNAME="${DOCUMENTDB_USERNAME:-docdbadmin}" export DOCUMENTDB_PASSWORD="${DOCUMENTDB_PASSWORD:-Documentdb!Local1}" export DOCUMENTDB_PORT="${DOCUMENTDB_PORT:-10260}" diff --git a/shared/telemetry/scripts/build-documentdb-image.sh b/shared/telemetry/scripts/build-documentdb-image.sh deleted file mode 100755 index 6a30d13..0000000 --- a/shared/telemetry/scripts/build-documentdb-image.sh +++ /dev/null @@ -1,92 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -SOURCE_INPUT="${1:-${DOCUMENTDB_SOURCE_DIR:-$HOME/repos/documentdb}}" -IMAGE_TAG="${2:-${DOCUMENTDB_IMAGE:-documentdb-local:gateway-tracing}}" -POSTGRES_VERSION="${POSTGRES_VERSION:-17}" -PACKAGE_OS="${PACKAGE_OS:-deb13}" -BASE_IMAGE="${BASE_IMAGE:-debian:trixie-slim}" - -if [ -d "$SOURCE_INPUT/oss/packaging" ]; then - SOURCE_INPUT="$SOURCE_INPUT/oss" -fi - -if [ ! -d "$SOURCE_INPUT" ]; then - echo "DocumentDB source directory does not exist: $SOURCE_INPUT" >&2 - exit 1 -fi - -SOURCE_DIR="$(cd "$SOURCE_INPUT" && pwd -P)" -for required_path in \ - packaging/build_packages.sh \ - packaging/gateway/docker/Dockerfile_documentdb_local \ - pg_documentdb_gw; do - if [ ! -e "$SOURCE_DIR/$required_path" ]; then - echo "DocumentDB source is missing $required_path: $SOURCE_DIR" >&2 - exit 1 - fi -done - -command -v docker >/dev/null || { - echo "docker is required" >&2 - exit 1 -} -docker info >/dev/null 2>&1 || { - echo "Cannot reach the Docker daemon" >&2 - exit 1 -} - -BUILD_DIR="$(mktemp -d "$SOURCE_DIR/.playground-image-build.XXXXXX")" -cleanup() { - rm -rf -- "$BUILD_DIR" -} -trap cleanup EXIT - -OUTPUT_REL="${BUILD_DIR#"$SOURCE_DIR"/}" -SOURCE_REVISION="$(git -C "$SOURCE_DIR" rev-parse --short HEAD 2>/dev/null || echo unknown)" -if [ -n "$(git -C "$SOURCE_DIR" status --porcelain 2>/dev/null || true)" ]; then - SOURCE_REVISION="${SOURCE_REVISION}-dirty" -fi - -echo "Building DocumentDB image" -echo " Source: $SOURCE_DIR ($SOURCE_REVISION)" -echo " PostgreSQL: $POSTGRES_VERSION" -echo " Image: $IMAGE_TAG" - -( - cd "$SOURCE_DIR" - ./packaging/build_packages.sh \ - --os "$PACKAGE_OS" \ - --pg "$POSTGRES_VERSION" \ - --output-dir "$OUTPUT_REL" -) - -packages=() -for package in "$BUILD_DIR"/*.deb; do - [ -f "$package" ] || continue - case "$package" in - *dbgsym*) continue ;; - esac - packages+=("$package") -done -if [ "${#packages[@]}" -ne 1 ]; then - echo "Expected exactly one non-debug Debian package, found ${#packages[@]}" >&2 - printf ' %s\n' "${packages[@]}" >&2 - exit 1 -fi - -PACKAGE_REL="${packages[0]#"$SOURCE_DIR"/}" -( - cd "$SOURCE_DIR" - docker build \ - --build-arg BASE_IMAGE="$BASE_IMAGE" \ - --build-arg POSTGRES_VERSION="$POSTGRES_VERSION" \ - --build-arg DEB_PACKAGE_REL_PATH="$PACKAGE_REL" \ - --label "com.documentdb.playground.source-revision=$SOURCE_REVISION" \ - -t "$IMAGE_TAG" \ - -f packaging/gateway/docker/Dockerfile_documentdb_local . -) - -docker image inspect "$IMAGE_TAG" \ - --format 'Built {{index .RepoTags 0}} ({{.Architecture}}, {{.Size}} bytes)' diff --git a/shared/telemetry/scripts/up.sh b/shared/telemetry/scripts/up.sh index 2e06f52..fb88a26 100755 --- a/shared/telemetry/scripts/up.sh +++ b/shared/telemetry/scripts/up.sh @@ -13,17 +13,6 @@ docker info >/dev/null 2>&1 || { exit 1 } -if ! docker image inspect "$DOCUMENTDB_IMAGE" >/dev/null 2>&1; then - cat >&2 </dev/null 2>&1; then existing_project="$( docker inspect \ From ee4b88291ef66da6bced31592c327c425b723985 Mon Sep 17 00:00:00 2001 From: urismiley Date: Fri, 7 Aug 2026 15:49:59 -0400 Subject: [PATCH 3/3] Harden Mongoose query inputs. Copilot-Session: 85f151f9-f585-4bdf-9c2b-798f8a2651be --- playgrounds/mongoose/app/server.js | 44 ++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/playgrounds/mongoose/app/server.js b/playgrounds/mongoose/app/server.js index a442615..2eae3ad 100644 --- a/playgrounds/mongoose/app/server.js +++ b/playgrounds/mongoose/app/server.js @@ -19,11 +19,41 @@ app.use(express.json()); const PORT = Number(process.env.PORT || 3000); const DB_NAME = process.env.MONGO_DB || 'mongoose_demo'; +const BOOK_UPDATE_FIELDS = [ + 'title', + 'author', + 'genres', + 'pages', + 'published', + 'inStock', + 'rating', +]; function traceBookOperation(operationName, operation) { return traceDocumentDbOperation(operationName, DB_NAME, operation); } +function parseAuthor(value) { + if (value === undefined) return undefined; + if (typeof value !== 'string') return null; + + const author = value.trim(); + if (author.length === 0 || author.length > 100) return null; + return author; +} + +function pickBookUpdates(body) { + if (!body || typeof body !== 'object' || Array.isArray(body)) return null; + + const changes = {}; + for (const field of BOOK_UPDATE_FIELDS) { + if (Object.prototype.hasOwnProperty.call(body, field)) { + changes[field] = body[field]; + } + } + return changes; +} + // Liveness/readiness probe. Returns 200 only when the Mongoose connection is up. app.get('/health', (req, res) => { const state = mongoose.connection.readyState; // 1 = connected @@ -46,7 +76,13 @@ app.post('/books', async (req, res) => { // List books, optionally filtered by author. app.get('/books', async (req, res) => { try { - const filter = req.query.author ? { author: req.query.author } : {}; + const author = parseAuthor(req.query.author); + if (author === null) { + return res.status(400).json({ + error: 'author must be a non-empty string up to 100 characters', + }); + } + const filter = author === undefined ? {} : { author }; const books = await traceBookOperation('find', () => Book.find(filter).sort({ createdAt: -1 }).limit(100).lean() ); @@ -72,8 +108,12 @@ app.get('/books/:id', async (req, res) => { // Update a book. app.patch('/books/:id', async (req, res) => { try { + const changes = pickBookUpdates(req.body); + if (!changes || Object.keys(changes).length === 0) { + return res.status(400).json({ error: 'no supported book fields provided' }); + } const book = await traceBookOperation('findOneAndUpdate', () => - Book.findByIdAndUpdate(req.params.id, req.body, { + Book.findByIdAndUpdate(req.params.id, { $set: changes }, { new: true, runValidators: true, }).lean()