From 0220c8d832d4d72cbf7ebaa19afe654a396e5e88 Mon Sep 17 00:00:00 2001 From: Nicacio Oliveira Date: Thu, 25 Jun 2026 13:58:21 -0300 Subject: [PATCH 1/5] feat(observability): align metrics with OTel semantic conventions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use official @opentelemetry/semantic-conventions constants (bumped 1.25.1 -> 1.37.0) instead of hardcoded strings, and rename/retype metrics to semconv: - http_request_duration (ms) -> http.server.request.duration (seconds), with semconv attributes http.request.method / http.route / http.response.status_code (status as int, not string). - block_op_duration (ms) -> deco.block.operation.duration (seconds). - cache_hit -> deco.cache.hits; cache attributes namespaced under deco.cache.*. - Resource attributes migrated off the deprecated SemanticResourceAttributes to ATTR_* constants; deployment.environment -> deployment.environment.name. - Seconds histogram buckets widened to cover sub-second HTTP latencies through multi-second operations. deco-proprietary names (no semconv equivalent) live in observability/otel/ conventions.ts. Clean rename — no dual-emit (ClickStack is a fresh backend). Co-Authored-By: Claude Opus 4.8 --- deps.ts | 25 +++++++++++++++++++++++-- observability/http.ts | 23 +++++++++++++++-------- observability/observe.ts | 30 +++++++++++++++++++----------- observability/otel/config.ts | 25 ++++++++++++------------- observability/otel/conventions.ts | 16 ++++++++++++++++ observability/otel/metrics.ts | 20 ++++++++++++++++++-- runtime/caches/common.ts | 16 +++++++++++----- 7 files changed, 114 insertions(+), 41 deletions(-) create mode 100644 observability/otel/conventions.ts diff --git a/deps.ts b/deps.ts index 4ec50a83e..61e58754f 100644 --- a/deps.ts +++ b/deps.ts @@ -73,9 +73,30 @@ export type { SamplingResult, } from "npm:@opentelemetry/sdk-trace-base@1.25.1"; export { NodeTracerProvider } from "npm:@opentelemetry/sdk-trace-node@1.25.1"; +// Stable semantic conventions (OTel semconv) — use these instead of hardcoded +// attribute/metric name strings. export { - SemanticResourceAttributes, -} from "npm:@opentelemetry/semantic-conventions@1.25.1"; + ATTR_ERROR_TYPE, + ATTR_HTTP_REQUEST_METHOD, + ATTR_HTTP_RESPONSE_STATUS_CODE, + ATTR_HTTP_ROUTE, + ATTR_SERVER_ADDRESS, + ATTR_SERVICE_NAME, + ATTR_SERVICE_VERSION, + ATTR_URL_PATH, + ATTR_URL_QUERY, + ATTR_URL_SCHEME, + ATTR_USER_AGENT_ORIGINAL, + METRIC_HTTP_SERVER_REQUEST_DURATION, +} from "npm:@opentelemetry/semantic-conventions@1.37.0"; +// Incubating (not yet stable) semantic conventions. +export { + ATTR_CLOUD_PROVIDER, + ATTR_CLOUD_REGION, + ATTR_DEPLOYMENT_ENVIRONMENT_NAME, + ATTR_HTTP_REQUEST_BODY_SIZE, + ATTR_SERVICE_INSTANCE_ID, +} from "npm:@opentelemetry/semantic-conventions@1.37.0/incubating"; export { ExplicitBucketHistogramAggregation, diff --git a/observability/http.ts b/observability/http.ts index bcb03d63d..58ca3539b 100644 --- a/observability/http.ts +++ b/observability/http.ts @@ -1,9 +1,16 @@ -import { ValueType } from "../deps.ts"; +import { + ATTR_HTTP_REQUEST_METHOD, + ATTR_HTTP_RESPONSE_STATUS_CODE, + ATTR_HTTP_ROUTE, + METRIC_HTTP_SERVER_REQUEST_DURATION, + ValueType, +} from "../deps.ts"; import { meter } from "./otel/metrics.ts"; -const httpDuration = meter.createHistogram("http_request_duration", { - description: "http request duration", - unit: "ms", +// OTel semconv: name `http.server.request.duration`, unit seconds. +const httpDuration = meter.createHistogram(METRIC_HTTP_SERVER_REQUEST_DURATION, { + description: "Duration of HTTP server requests.", + unit: "s", valueType: ValueType.DOUBLE, }); /** @@ -12,10 +19,10 @@ const httpDuration = meter.createHistogram("http_request_duration", { export const startObserve = () => { const start = performance.now(); return (method: string, path: string, status: number) => { - httpDuration.record(Math.round(performance.now() - start), { - "http.method": method, - "http.route": path, - "http.response.status": `${status}`, + httpDuration.record((performance.now() - start) / 1000, { + [ATTR_HTTP_REQUEST_METHOD]: method, + [ATTR_HTTP_ROUTE]: path, + [ATTR_HTTP_RESPONSE_STATUS_CODE]: status, }); }; }; diff --git a/observability/observe.ts b/observability/observe.ts index a4c1518ac..1b10b806c 100644 --- a/observability/observe.ts +++ b/observability/observe.ts @@ -1,12 +1,20 @@ import { isWrappedError } from "../blocks/loader.ts"; import { ValueType } from "../deps.ts"; import { meter, OTEL_ENABLE_EXTRA_METRICS } from "./otel/metrics.ts"; +import { + ATTR_DECO_OPERATION_ERROR, + ATTR_DECO_OPERATION_NAME, + METRIC_DECO_BLOCK_OPERATION_DURATION, +} from "./otel/conventions.ts"; -const operationDuration = meter.createHistogram("block_op_duration", { - description: "operation duration", - unit: "ms", - valueType: ValueType.DOUBLE, -}); +const operationDuration = meter.createHistogram( + METRIC_DECO_BLOCK_OPERATION_DURATION, + { + description: "Duration of deco block operations.", + unit: "s", + valueType: ValueType.DOUBLE, + }, +); /** * Observe function durations based on the provided labels @@ -16,21 +24,21 @@ export const observe = async ( f: () => Promise, ): Promise => { const start = performance.now(); - let isError = "false"; + let isError = false; try { const result = await f(); if (isWrappedError(result)) { - isError = "true"; + isError = true; } return result; } catch (error) { - isError = "true"; + isError = true; throw error; } finally { if (OTEL_ENABLE_EXTRA_METRICS) { - operationDuration.record(performance.now() - start, { - "operation.name": op, - "operation.is_error": isError, + operationDuration.record((performance.now() - start) / 1000, { + [ATTR_DECO_OPERATION_NAME]: op, + [ATTR_DECO_OPERATION_ERROR]: isError, }); } } diff --git a/observability/otel/config.ts b/observability/otel/config.ts index 47d576c9c..a7b6fb5c0 100644 --- a/observability/otel/config.ts +++ b/observability/otel/config.ts @@ -3,6 +3,12 @@ import { Logger } from "@std/log/logger"; import { Context, context } from "../../deco.ts"; import denoJSON from "../../deno.json" with { type: "json" }; import { + ATTR_CLOUD_PROVIDER, + ATTR_CLOUD_REGION, + ATTR_DEPLOYMENT_ENVIRONMENT_NAME, + ATTR_SERVICE_INSTANCE_ID, + ATTR_SERVICE_NAME, + ATTR_SERVICE_VERSION, BatchSpanProcessor, FetchInstrumentation, NodeTracerProvider, @@ -11,7 +17,6 @@ import { ParentBasedSampler, registerInstrumentations, Resource, - SemanticResourceAttributes, } from "../../deps.ts"; import { DenoRuntimeInstrumentation } from "./instrumentation/deno-runtime.ts"; import { DebugSampler } from "./samplers/debug.ts"; @@ -34,20 +39,14 @@ const apps_ver = tryGetVersionOf("apps/") ?? export const resource = Resource.default().merge( new Resource({ - [SemanticResourceAttributes.SERVICE_NAME]: Deno.env.get(ENV_SITE_NAME) ?? - "deco", - [SemanticResourceAttributes.SERVICE_VERSION]: - Context.active().deploymentId ?? - Deno.hostname(), - [SemanticResourceAttributes.SERVICE_INSTANCE_ID]: crypto.randomUUID(), - [SemanticResourceAttributes.CLOUD_PROVIDER]: context.platform, + [ATTR_SERVICE_NAME]: Deno.env.get(ENV_SITE_NAME) ?? "deco", + [ATTR_SERVICE_VERSION]: Context.active().deploymentId ?? Deno.hostname(), + [ATTR_SERVICE_INSTANCE_ID]: crypto.randomUUID(), + [ATTR_CLOUD_PROVIDER]: context.platform, "deco.runtime.version": denoJSON.version, "deco.apps.version": apps_ver, - [SemanticResourceAttributes.CLOUD_REGION]: Deno.env.get("DENO_REGION") ?? - "unknown", - [SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: Deno.env.get( - "DECO_ENV_NAME", - ) + [ATTR_CLOUD_REGION]: Deno.env.get("DENO_REGION") ?? "unknown", + [ATTR_DEPLOYMENT_ENVIRONMENT_NAME]: Deno.env.get("DECO_ENV_NAME") ? `env-${Deno.env.get("DECO_ENV_NAME")}` : "production", }), diff --git a/observability/otel/conventions.ts b/observability/otel/conventions.ts new file mode 100644 index 000000000..bbf0508d7 --- /dev/null +++ b/observability/otel/conventions.ts @@ -0,0 +1,16 @@ +// deco-proprietary telemetry conventions, for signals that have no OTel +// semantic-conventions equivalent. Standard signals (HTTP, URL, service, +// cloud, gen_ai, ...) MUST use the official @opentelemetry/semantic-conventions +// constants re-exported from deps.ts — do not hardcode those names here. + +// Metrics +export const METRIC_DECO_BLOCK_OPERATION_DURATION = + "deco.block.operation.duration"; +export const METRIC_DECO_CACHE_HITS = "deco.cache.hits"; + +// Attributes +export const ATTR_DECO_OPERATION_NAME = "deco.operation.name"; +export const ATTR_DECO_OPERATION_ERROR = "deco.operation.error"; +export const ATTR_DECO_CACHE_RESULT = "deco.cache.result"; +export const ATTR_DECO_CACHE_ENGINE = "deco.cache.engine"; +export const ATTR_DECO_CACHE_STATUS = "deco.cache.status"; diff --git a/observability/otel/metrics.ts b/observability/otel/metrics.ts index b5ea77d7d..d267e7a62 100644 --- a/observability/otel/metrics.ts +++ b/observability/otel/metrics.ts @@ -28,9 +28,25 @@ const headersStringToObject = (headersString: string | undefined | null) => { return Object.fromEntries(splitByComma); }; -// Add views with different boundaries for each unit. +// Add views with different boundaries for each unit. Now that durations are +// recorded in seconds (OTel semconv), the `s` buckets must cover both +// sub-second HTTP latencies and multi-second gen_ai/block operations. const msBoundaries = [10, 100, 500, 1000, 5000, 10000, 15000]; -const sBoundaries = [1, 5, 10, 50]; +const sBoundaries = [ + 0.005, + 0.01, + 0.025, + 0.05, + 0.1, + 0.25, + 0.5, + 1, + 2.5, + 5, + 10, + 30, + 60, +]; type IMeter = ReturnType; const meterProvider: MeterProvider = new MeterProvider({ diff --git a/runtime/caches/common.ts b/runtime/caches/common.ts index 915037f8d..26a9acc4e 100644 --- a/runtime/caches/common.ts +++ b/runtime/caches/common.ts @@ -1,6 +1,12 @@ import { type Exception, ValueType } from "../../deps.ts"; import { tracer } from "../../observability/otel/config.ts"; import { meter } from "../../observability/otel/metrics.ts"; +import { + ATTR_DECO_CACHE_ENGINE, + ATTR_DECO_CACHE_RESULT, + ATTR_DECO_CACHE_STATUS, + METRIC_DECO_CACHE_HITS, +} from "../../observability/otel/conventions.ts"; import { inFuture } from "./utils.ts"; export interface CacheMetrics { @@ -8,7 +14,7 @@ export interface CacheMetrics { total: number; hits: number; } -const cacheHit = meter.createCounter("cache_hit", { +const cacheHit = meter.createCounter(METRIC_DECO_CACHE_HITS, { unit: "1", valueType: ValueType.DOUBLE, }); @@ -38,17 +44,17 @@ export const withInstrumentation = ( put: cacheImpl.put.bind(cacheImpl), match: async (req, opts) => { const span = tracer.startSpan("cache-match", { - attributes: { engine }, + attributes: { [ATTR_DECO_CACHE_ENGINE]: engine }, }); try { const isMatch = await cacheImpl.match(req, opts); //there is an edge case where there is no expires header, but technically our loader always sets it const result = getCacheStatus(isMatch); - span.setAttribute("cache_status", result); + span.setAttribute(ATTR_DECO_CACHE_STATUS, result); cacheHit.add(1, { - result, - engine, + [ATTR_DECO_CACHE_RESULT]: result, + [ATTR_DECO_CACHE_ENGINE]: engine, }); return isMatch; } catch (err) { From 84d9f41029ff0fb745972ba0a80fe8e779afe833 Mon Sep 17 00:00:00 2001 From: Nicacio Oliveira Date: Thu, 25 Jun 2026 16:14:44 -0300 Subject: [PATCH 2/5] fix(observability): rename deco.cache.hits -> deco.cache.lookups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The counter records every cache lookup dimensioned by deco.cache.result (hit/stale/miss), so "hits" was a misnomer and collided with @decocms/start's hits/misses counters. Rename to deco.cache.lookups — unified cache metric across both frameworks (single counter + deco.cache.result dimension). Co-Authored-By: Claude Opus 4.8 --- observability/otel/conventions.ts | 4 +++- runtime/caches/common.ts | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/observability/otel/conventions.ts b/observability/otel/conventions.ts index bbf0508d7..0dc5c7fca 100644 --- a/observability/otel/conventions.ts +++ b/observability/otel/conventions.ts @@ -6,7 +6,9 @@ // Metrics export const METRIC_DECO_BLOCK_OPERATION_DURATION = "deco.block.operation.duration"; -export const METRIC_DECO_CACHE_HITS = "deco.cache.hits"; +// Single cache counter dimensioned by `deco.cache.result` — unified with +// @decocms/start (avoids a `deco.cache.hits` name/semantics collision). +export const METRIC_DECO_CACHE_LOOKUPS = "deco.cache.lookups"; // Attributes export const ATTR_DECO_OPERATION_NAME = "deco.operation.name"; diff --git a/runtime/caches/common.ts b/runtime/caches/common.ts index 26a9acc4e..780b4b30d 100644 --- a/runtime/caches/common.ts +++ b/runtime/caches/common.ts @@ -5,7 +5,7 @@ import { ATTR_DECO_CACHE_ENGINE, ATTR_DECO_CACHE_RESULT, ATTR_DECO_CACHE_STATUS, - METRIC_DECO_CACHE_HITS, + METRIC_DECO_CACHE_LOOKUPS, } from "../../observability/otel/conventions.ts"; import { inFuture } from "./utils.ts"; @@ -14,7 +14,7 @@ export interface CacheMetrics { total: number; hits: number; } -const cacheHit = meter.createCounter(METRIC_DECO_CACHE_HITS, { +const cacheHit = meter.createCounter(METRIC_DECO_CACHE_LOOKUPS, { unit: "1", valueType: ValueType.DOUBLE, }); From d252e148a2733393636da62e607faa1c24fac8e2 Mon Sep 17 00:00:00 2001 From: Nicacio Oliveira Date: Thu, 25 Jun 2026 16:18:35 -0300 Subject: [PATCH 3/5] fix(observability): cache -> deco.cache.hits/misses (match @decocms/start) Use the metric names @decocms/start already established (two counters) instead of inventing deco.cache.lookups. Both dimensioned by deco.cache.result. Co-Authored-By: Claude Opus 4.8 --- observability/otel/conventions.ts | 7 ++++--- runtime/caches/common.ts | 14 +++++++++++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/observability/otel/conventions.ts b/observability/otel/conventions.ts index 0dc5c7fca..48516d063 100644 --- a/observability/otel/conventions.ts +++ b/observability/otel/conventions.ts @@ -6,9 +6,10 @@ // Metrics export const METRIC_DECO_BLOCK_OPERATION_DURATION = "deco.block.operation.duration"; -// Single cache counter dimensioned by `deco.cache.result` — unified with -// @decocms/start (avoids a `deco.cache.hits` name/semantics collision). -export const METRIC_DECO_CACHE_LOOKUPS = "deco.cache.lookups"; +// Cache counters, dimensioned by `deco.cache.result` — names match +// @decocms/start (tanstack) so both frameworks aggregate on the same metrics. +export const METRIC_DECO_CACHE_HITS = "deco.cache.hits"; +export const METRIC_DECO_CACHE_MISSES = "deco.cache.misses"; // Attributes export const ATTR_DECO_OPERATION_NAME = "deco.operation.name"; diff --git a/runtime/caches/common.ts b/runtime/caches/common.ts index 780b4b30d..840072164 100644 --- a/runtime/caches/common.ts +++ b/runtime/caches/common.ts @@ -5,7 +5,8 @@ import { ATTR_DECO_CACHE_ENGINE, ATTR_DECO_CACHE_RESULT, ATTR_DECO_CACHE_STATUS, - METRIC_DECO_CACHE_LOOKUPS, + METRIC_DECO_CACHE_HITS, + METRIC_DECO_CACHE_MISSES, } from "../../observability/otel/conventions.ts"; import { inFuture } from "./utils.ts"; @@ -14,7 +15,13 @@ export interface CacheMetrics { total: number; hits: number; } -const cacheHit = meter.createCounter(METRIC_DECO_CACHE_LOOKUPS, { +// Two counters (names match @decocms/start); `deco.cache.result` carries the +// outcome (hit/stale/miss) for both. +const cacheHits = meter.createCounter(METRIC_DECO_CACHE_HITS, { + unit: "1", + valueType: ValueType.DOUBLE, +}); +const cacheMisses = meter.createCounter(METRIC_DECO_CACHE_MISSES, { unit: "1", valueType: ValueType.DOUBLE, }); @@ -52,7 +59,8 @@ export const withInstrumentation = ( const result = getCacheStatus(isMatch); span.setAttribute(ATTR_DECO_CACHE_STATUS, result); - cacheHit.add(1, { + const counter = result === "miss" ? cacheMisses : cacheHits; + counter.add(1, { [ATTR_DECO_CACHE_RESULT]: result, [ATTR_DECO_CACHE_ENGINE]: engine, }); From a4e8020bb2aafb1c41460b64ea93c87a7f45b7dd Mon Sep 17 00:00:00 2001 From: Nicacio Oliveira Date: Thu, 25 Jun 2026 16:34:32 -0300 Subject: [PATCH 4/5] fix(observability): cache -> single deco.cache.requests + deco.cache.status Follow OTel semconv modeling (no canonical cache metric exists; mirror the nfs.server.repcache.requests + .status pattern + the prefer-attributes-over- metrics guidance). One counter dimensioned by deco.cache.status (hit/stale/ miss). Same key on span + metric. Co-Authored-By: Claude Opus 4.8 --- observability/otel/conventions.ts | 11 ++++++----- runtime/caches/common.ts | 20 ++++++-------------- 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/observability/otel/conventions.ts b/observability/otel/conventions.ts index 48516d063..5499ff91a 100644 --- a/observability/otel/conventions.ts +++ b/observability/otel/conventions.ts @@ -6,14 +6,15 @@ // Metrics export const METRIC_DECO_BLOCK_OPERATION_DURATION = "deco.block.operation.duration"; -// Cache counters, dimensioned by `deco.cache.result` — names match -// @decocms/start (tanstack) so both frameworks aggregate on the same metrics. -export const METRIC_DECO_CACHE_HITS = "deco.cache.hits"; -export const METRIC_DECO_CACHE_MISSES = "deco.cache.misses"; +// Single cache counter dimensioned by `deco.cache.status` — follows the OTel +// semconv pattern (cf. nfs.server.repcache.requests + .status) and the general +// guidance to prefer attributes over separate metrics. Unified with +// @decocms/start so both frameworks aggregate on the same series. +export const METRIC_DECO_CACHE_REQUESTS = "deco.cache.requests"; // Attributes export const ATTR_DECO_OPERATION_NAME = "deco.operation.name"; export const ATTR_DECO_OPERATION_ERROR = "deco.operation.error"; -export const ATTR_DECO_CACHE_RESULT = "deco.cache.result"; export const ATTR_DECO_CACHE_ENGINE = "deco.cache.engine"; +// Cache outcome: hit | stale | miss (| bypass). Same key on span + metric. export const ATTR_DECO_CACHE_STATUS = "deco.cache.status"; diff --git a/runtime/caches/common.ts b/runtime/caches/common.ts index 840072164..d453e5e15 100644 --- a/runtime/caches/common.ts +++ b/runtime/caches/common.ts @@ -3,10 +3,8 @@ import { tracer } from "../../observability/otel/config.ts"; import { meter } from "../../observability/otel/metrics.ts"; import { ATTR_DECO_CACHE_ENGINE, - ATTR_DECO_CACHE_RESULT, ATTR_DECO_CACHE_STATUS, - METRIC_DECO_CACHE_HITS, - METRIC_DECO_CACHE_MISSES, + METRIC_DECO_CACHE_REQUESTS, } from "../../observability/otel/conventions.ts"; import { inFuture } from "./utils.ts"; @@ -15,14 +13,9 @@ export interface CacheMetrics { total: number; hits: number; } -// Two counters (names match @decocms/start); `deco.cache.result` carries the -// outcome (hit/stale/miss) for both. -const cacheHits = meter.createCounter(METRIC_DECO_CACHE_HITS, { - unit: "1", - valueType: ValueType.DOUBLE, -}); -const cacheMisses = meter.createCounter(METRIC_DECO_CACHE_MISSES, { - unit: "1", +// Single cache counter; `deco.cache.status` carries the outcome. +const cacheRequests = meter.createCounter(METRIC_DECO_CACHE_REQUESTS, { + unit: "{request}", valueType: ValueType.DOUBLE, }); @@ -59,9 +52,8 @@ export const withInstrumentation = ( const result = getCacheStatus(isMatch); span.setAttribute(ATTR_DECO_CACHE_STATUS, result); - const counter = result === "miss" ? cacheMisses : cacheHits; - counter.add(1, { - [ATTR_DECO_CACHE_RESULT]: result, + cacheRequests.add(1, { + [ATTR_DECO_CACHE_STATUS]: result, [ATTR_DECO_CACHE_ENGINE]: engine, }); return isMatch; From 8f9a96021d82a83fac708ce6fabe4806670d5bcf Mon Sep 17 00:00:00 2001 From: Nicacio Oliveira Date: Fri, 26 Jun 2026 11:06:51 -0300 Subject: [PATCH 5/5] fix(observability): vendor incubating semconv consts + correct service.version - Stop re-exporting from @opentelemetry/semantic-conventions/incubating (OTel advises against depending on the unstable entry point); vendor the 4 needed experimental attribute names as plain constants in conventions.ts. - service.version: use the deployment revision (deploymentId) falling back to the framework version, not Deno.hostname() (which is instance identity). Addresses PR review (cubic P2 + coderabbit). Cache is already a single deco.cache.requests counter + deco.cache.status label (earlier commit). Co-Authored-By: Claude Opus 4.8 --- deps.ts | 12 ++++-------- observability/otel/config.ts | 14 +++++++++----- observability/otel/conventions.ts | 9 +++++++++ 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/deps.ts b/deps.ts index 61e58754f..a6f8b3020 100644 --- a/deps.ts +++ b/deps.ts @@ -89,14 +89,10 @@ export { ATTR_USER_AGENT_ORIGINAL, METRIC_HTTP_SERVER_REQUEST_DURATION, } from "npm:@opentelemetry/semantic-conventions@1.37.0"; -// Incubating (not yet stable) semantic conventions. -export { - ATTR_CLOUD_PROVIDER, - ATTR_CLOUD_REGION, - ATTR_DEPLOYMENT_ENVIRONMENT_NAME, - ATTR_HTTP_REQUEST_BODY_SIZE, - ATTR_SERVICE_INSTANCE_ID, -} from "npm:@opentelemetry/semantic-conventions@1.37.0/incubating"; +// Incubating (experimental) semconv names are NOT re-exported here — OTel +// advises libraries against depending on the unstable `/incubating` entry +// point. The few we need are vendored as plain constants in +// observability/otel/conventions.ts. export { ExplicitBucketHistogramAggregation, diff --git a/observability/otel/config.ts b/observability/otel/config.ts index a7b6fb5c0..55b52c710 100644 --- a/observability/otel/config.ts +++ b/observability/otel/config.ts @@ -3,10 +3,6 @@ import { Logger } from "@std/log/logger"; import { Context, context } from "../../deco.ts"; import denoJSON from "../../deno.json" with { type: "json" }; import { - ATTR_CLOUD_PROVIDER, - ATTR_CLOUD_REGION, - ATTR_DEPLOYMENT_ENVIRONMENT_NAME, - ATTR_SERVICE_INSTANCE_ID, ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION, BatchSpanProcessor, @@ -18,6 +14,12 @@ import { registerInstrumentations, Resource, } from "../../deps.ts"; +import { + ATTR_CLOUD_PROVIDER, + ATTR_CLOUD_REGION, + ATTR_DEPLOYMENT_ENVIRONMENT_NAME, + ATTR_SERVICE_INSTANCE_ID, +} from "./conventions.ts"; import { DenoRuntimeInstrumentation } from "./instrumentation/deno-runtime.ts"; import { DebugSampler } from "./samplers/debug.ts"; import { type SamplingOptions, URLBasedSampler } from "./samplers/urlBased.ts"; @@ -40,7 +42,9 @@ const apps_ver = tryGetVersionOf("apps/") ?? export const resource = Resource.default().merge( new Resource({ [ATTR_SERVICE_NAME]: Deno.env.get(ENV_SITE_NAME) ?? "deco", - [ATTR_SERVICE_VERSION]: Context.active().deploymentId ?? Deno.hostname(), + // Version of the deployed artifact (the deployment revision), falling back + // to the framework version — NOT the hostname (that is instance identity). + [ATTR_SERVICE_VERSION]: Context.active().deploymentId ?? denoJSON.version, [ATTR_SERVICE_INSTANCE_ID]: crypto.randomUUID(), [ATTR_CLOUD_PROVIDER]: context.platform, "deco.runtime.version": denoJSON.version, diff --git a/observability/otel/conventions.ts b/observability/otel/conventions.ts index 5499ff91a..6b4e7d539 100644 --- a/observability/otel/conventions.ts +++ b/observability/otel/conventions.ts @@ -18,3 +18,12 @@ export const ATTR_DECO_OPERATION_ERROR = "deco.operation.error"; export const ATTR_DECO_CACHE_ENGINE = "deco.cache.engine"; // Cache outcome: hit | stale | miss (| bypass). Same key on span + metric. export const ATTR_DECO_CACHE_STATUS = "deco.cache.status"; + +// Vendored copies of EXPERIMENTAL (incubating) OTel semconv attribute names. +// OTel recommends libraries NOT import from `@opentelemetry/.../incubating` +// (the entry point is unstable across versions); copy the values instead. +// Sourced from @opentelemetry/semantic-conventions 1.37.0/incubating. +export const ATTR_CLOUD_PROVIDER = "cloud.provider"; +export const ATTR_CLOUD_REGION = "cloud.region"; +export const ATTR_DEPLOYMENT_ENVIRONMENT_NAME = "deployment.environment.name"; +export const ATTR_SERVICE_INSTANCE_ID = "service.instance.id";