diff --git a/examples/otel-metrics-demo/.gitignore b/examples/otel-metrics-demo/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/examples/otel-metrics-demo/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/examples/otel-metrics-demo/README.md b/examples/otel-metrics-demo/README.md new file mode 100644 index 0000000..34cbbba --- /dev/null +++ b/examples/otel-metrics-demo/README.md @@ -0,0 +1,42 @@ +# OTLP Metrics Demo + +A tiny standalone script that exports OpenTelemetry **metrics** to a Temps +server over OTLP/HTTP — the fastest way to confirm the metrics ingest path works +end to end. + +## What + +Uses the standard OpenTelemetry SDK (`@opentelemetry/sdk-metrics` + +`@opentelemetry/exporter-metrics-otlp-proto`) to push protobuf metrics straight +to Temps' `/api/otel/v1/metrics` endpoint — no collector sidecar required. It +emits three instruments, each with labels: + +| Instrument | Type | Name | +|---|---|---| +| Counter | sum | `demo.requests.total` | +| Histogram | histogram | `demo.request.duration` | +| Observable gauge | gauge | `demo.active.requests` | + +For a full app that wires analytics, error tracking, tracing **and** metrics, +see [`observability-starter`](../observability-starter). + +## Run + +```bash +bun install +TEMPS_TOKEN=tk_your_key TEMPS_PROJECT_ID=123 node index.mjs +``` + +Then open your project's **Monitoring → Metrics** in Temps; the three +`demo.*` metrics should appear within a few seconds. + +## Configuration + +| Env var | Default | Purpose | +|---|---|---| +| `TEMPS_TOKEN` | — (required) | A Temps API key (`tk_…`) with access to the project | +| `TEMPS_PROJECT_ID` | — (required) | Project to attribute metrics to (sent as the `X-Temps-Project-Id` header) | +| `TEMPS_OTLP_URL` | `http://localhost:8080/api/otel/v1/metrics` | The Temps OTLP/HTTP metrics endpoint | + +Create an API key in Temps under **Settings → API Keys** (or via the CLI: +`temps api-key --name metrics-demo --role admin`). diff --git a/examples/otel-metrics-demo/bun.lock b/examples/otel-metrics-demo/bun.lock new file mode 100644 index 0000000..e8680b4 --- /dev/null +++ b/examples/otel-metrics-demo/bun.lock @@ -0,0 +1,73 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "otel-metrics-demo", + "dependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/exporter-metrics-otlp-proto": "^0.57.2", + "@opentelemetry/resources": "^1.30.1", + "@opentelemetry/sdk-metrics": "^1.30.1", + "@opentelemetry/semantic-conventions": "^1.30.0", + }, + }, + }, + "packages": { + "@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="], + + "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.57.2", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A=="], + + "@opentelemetry/core": ["@opentelemetry/core@1.30.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ=="], + + "@opentelemetry/exporter-metrics-otlp-http": ["@opentelemetry/exporter-metrics-otlp-http@0.57.2", "", { "dependencies": { "@opentelemetry/core": "1.30.1", "@opentelemetry/otlp-exporter-base": "0.57.2", "@opentelemetry/otlp-transformer": "0.57.2", "@opentelemetry/resources": "1.30.1", "@opentelemetry/sdk-metrics": "1.30.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-ttb9+4iKw04IMubjm3t0EZsYRNWr3kg44uUuzfo9CaccYlOh8cDooe4QObDUkvx9d5qQUrbEckhrWKfJnKhemA=="], + + "@opentelemetry/exporter-metrics-otlp-proto": ["@opentelemetry/exporter-metrics-otlp-proto@0.57.2", "", { "dependencies": { "@opentelemetry/core": "1.30.1", "@opentelemetry/exporter-metrics-otlp-http": "0.57.2", "@opentelemetry/otlp-exporter-base": "0.57.2", "@opentelemetry/otlp-transformer": "0.57.2", "@opentelemetry/resources": "1.30.1", "@opentelemetry/sdk-metrics": "1.30.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-HX068Q2eNs38uf7RIkNN9Hl4Ynl+3lP0++KELkXMCpsCbFO03+0XNNZ1SkwxPlP9jrhQahsMPMkzNXpq3fKsnw=="], + + "@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.57.2", "", { "dependencies": { "@opentelemetry/core": "1.30.1", "@opentelemetry/otlp-transformer": "0.57.2" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-XdxEzL23Urhidyebg5E6jZoaiW5ygP/mRjxLHixogbqwDy2Faduzb5N0o/Oi+XTIJu+iyxXdVORjXax+Qgfxag=="], + + "@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.57.2", "", { "dependencies": { "@opentelemetry/api-logs": "0.57.2", "@opentelemetry/core": "1.30.1", "@opentelemetry/resources": "1.30.1", "@opentelemetry/sdk-logs": "0.57.2", "@opentelemetry/sdk-metrics": "1.30.1", "@opentelemetry/sdk-trace-base": "1.30.1", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-48IIRj49gbQVK52jYsw70+Jv+JbahT8BqT2Th7C4H7RCM9d0gZ5sgNPoMpWldmfjvIsSgiGJtjfk9MeZvjhoig=="], + + "@opentelemetry/resources": ["@opentelemetry/resources@1.30.1", "", { "dependencies": { "@opentelemetry/core": "1.30.1", "@opentelemetry/semantic-conventions": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA=="], + + "@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.57.2", "", { "dependencies": { "@opentelemetry/api-logs": "0.57.2", "@opentelemetry/core": "1.30.1", "@opentelemetry/resources": "1.30.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-TXFHJ5c+BKggWbdEQ/inpgIzEmS2BGQowLE9UhsMd7YYlUfBQJ4uax0VF/B5NYigdM/75OoJGhAV3upEhK+3gg=="], + + "@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@1.30.1", "", { "dependencies": { "@opentelemetry/core": "1.30.1", "@opentelemetry/resources": "1.30.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-q9zcZ0Okl8jRgmy7eNW3Ku1XSgg3sDLa5evHZpCwjspw7E8Is4K/haRPDJrBcX3YSn/Y7gUvFnByNYEKQNbNog=="], + + "@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@1.30.1", "", { "dependencies": { "@opentelemetry/core": "1.30.1", "@opentelemetry/resources": "1.30.1", "@opentelemetry/semantic-conventions": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg=="], + + "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.41.1", "", {}, "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA=="], + + "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], + + "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], + + "@protobufjs/codegen": ["@protobufjs/codegen@2.0.5", "", {}, "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g=="], + + "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.1", "", {}, "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg=="], + + "@protobufjs/fetch": ["@protobufjs/fetch@1.1.1", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1" } }, "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw=="], + + "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], + + "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], + + "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], + + "@protobufjs/utf8": ["@protobufjs/utf8@1.1.1", "", {}, "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg=="], + + "@types/node": ["@types/node@26.0.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw=="], + + "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], + + "protobufjs": ["protobufjs@7.6.4", "", { "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" } }, "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw=="], + + "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], + + "@opentelemetry/core/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="], + + "@opentelemetry/resources/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="], + + "@opentelemetry/sdk-trace-base/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="], + } +} diff --git a/examples/otel-metrics-demo/index.mjs b/examples/otel-metrics-demo/index.mjs new file mode 100644 index 0000000..b2bb2f1 --- /dev/null +++ b/examples/otel-metrics-demo/index.mjs @@ -0,0 +1,81 @@ +// Minimal real-OpenTelemetry-SDK app that exports OTLP/HTTP protobuf metrics to +// a Temps server. Emits a Counter, a Histogram and an ObservableGauge — each +// with labels — then force-flushes and exits. Use it to verify the Temps +// ClickHouse metrics ingest path end-to-end. +// +// TEMPS_TOKEN=tk_... TEMPS_PROJECT_ID=5 node index.mjs +// +import { MeterProvider, PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics'; +import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-proto'; +import { Resource } from '@opentelemetry/resources'; +import { + ATTR_SERVICE_NAME, + ATTR_SERVICE_VERSION, +} from '@opentelemetry/semantic-conventions'; + +const TOKEN = process.env.TEMPS_TOKEN; +const PROJECT_ID = process.env.TEMPS_PROJECT_ID; +const URL = + process.env.TEMPS_OTLP_URL || 'http://localhost:8080/api/otel/v1/metrics'; + +if (!TOKEN || !PROJECT_ID) { + console.error('Set TEMPS_TOKEN and TEMPS_PROJECT_ID'); + process.exit(1); +} + +const exporter = new OTLPMetricExporter({ + url: URL, + headers: { + Authorization: `Bearer ${TOKEN}`, + 'X-Temps-Project-Id': String(PROJECT_ID), + }, +}); + +const provider = new MeterProvider({ + resource: new Resource({ + [ATTR_SERVICE_NAME]: 'otel-metrics-demo', + [ATTR_SERVICE_VERSION]: '1.0.0', + }), + readers: [ + new PeriodicExportingMetricReader({ + exporter, + exportIntervalMillis: 2000, + }), + ], +}); + +const meter = provider.getMeter('otel-metrics-demo'); + +// Counter (monotonic, cumulative by default). +const requests = meter.createCounter('demo.requests.total', { + description: 'Total demo requests', + unit: '1', +}); +// Histogram (explicit buckets). +const latency = meter.createHistogram('demo.request.duration', { + description: 'Demo request latency', + unit: 'ms', +}); +// Observable gauge. +let inFlight = 0; +meter + .createObservableGauge('demo.active.requests', { + description: 'In-flight demo requests', + unit: '1', + }) + .addCallback((res) => res.observe(inFlight, { region: 'eu' })); + +// Generate some traffic. +for (let i = 0; i < 100; i++) { + const method = i % 2 === 0 ? 'GET' : 'POST'; + requests.add(1, { 'http.method': method, route: '/checkout' }); + latency.record(Math.round(Math.random() * 480) + 5, { 'http.method': method }); + inFlight = Math.floor(Math.random() * 12); +} + +console.log(`exporting metrics to ${URL} (project ${PROJECT_ID})...`); +await provider.forceFlush(); +// Give the periodic reader one cycle too, then shut down cleanly. +await new Promise((r) => setTimeout(r, 2500)); +await provider.shutdown(); +console.log('done — metrics flushed.'); diff --git a/examples/otel-metrics-demo/package.json b/examples/otel-metrics-demo/package.json new file mode 100644 index 0000000..9450b77 --- /dev/null +++ b/examples/otel-metrics-demo/package.json @@ -0,0 +1,16 @@ +{ + "name": "otel-metrics-demo", + "private": true, + "type": "module", + "description": "Minimal real-OpenTelemetry-SDK app that exports OTLP/HTTP protobuf metrics to a Temps server, used to verify the ClickHouse metrics ingest path end-to-end.", + "scripts": { + "start": "node index.mjs" + }, + "dependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/sdk-metrics": "^1.30.1", + "@opentelemetry/exporter-metrics-otlp-proto": "^0.57.2", + "@opentelemetry/resources": "^1.30.1", + "@opentelemetry/semantic-conventions": "^1.30.0" + } +}