Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,26 @@ 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 (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,
Expand Down
23 changes: 15 additions & 8 deletions observability/http.ts
Original file line number Diff line number Diff line change
@@ -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,
});
/**
Expand All @@ -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,
});
};
};
30 changes: 19 additions & 11 deletions observability/observe.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -16,21 +24,21 @@ export const observe = async <T>(
f: () => Promise<T>,
): Promise<T> => {
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,
});
}
}
Expand Down
29 changes: 16 additions & 13 deletions observability/otel/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { Logger } from "@std/log/logger";
import { Context, context } from "../../deco.ts";
import denoJSON from "../../deno.json" with { type: "json" };
import {
ATTR_SERVICE_NAME,
ATTR_SERVICE_VERSION,
BatchSpanProcessor,
FetchInstrumentation,
NodeTracerProvider,
Expand All @@ -11,8 +13,13 @@ import {
ParentBasedSampler,
registerInstrumentations,
Resource,
SemanticResourceAttributes,
} 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";
Expand All @@ -34,20 +41,16 @@ 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",
// 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,
"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",
}),
Expand Down
29 changes: 29 additions & 0 deletions observability/otel/conventions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// 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";
// 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_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";
20 changes: 18 additions & 2 deletions observability/otel/metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<MeterProvider["getMeter"]>;
const meterProvider: MeterProvider = new MeterProvider({
Expand Down
20 changes: 13 additions & 7 deletions runtime/caches/common.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
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_STATUS,
METRIC_DECO_CACHE_REQUESTS,
} from "../../observability/otel/conventions.ts";
import { inFuture } from "./utils.ts";

export interface CacheMetrics {
engine: string;
total: number;
hits: number;
}
const cacheHit = meter.createCounter("cache_hit", {
unit: "1",
// Single cache counter; `deco.cache.status` carries the outcome.
const cacheRequests = meter.createCounter(METRIC_DECO_CACHE_REQUESTS, {
unit: "{request}",
valueType: ValueType.DOUBLE,
});

Expand Down Expand Up @@ -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);
cacheHit.add(1, {
result,
engine,
span.setAttribute(ATTR_DECO_CACHE_STATUS, result);
cacheRequests.add(1, {
[ATTR_DECO_CACHE_STATUS]: result,
[ATTR_DECO_CACHE_ENGINE]: engine,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
return isMatch;
} catch (err) {
Expand Down
Loading