Skip to content
82 linesCodeBlameRaw
1/**
2 * What each app on g1t.page carries for observability (services/telemetry),
3 * set as it is uploaded:
4 *
5 * - the telemetry service as its Tail Worker, so every request it answers
6 * arrives there with its logs and exceptions;
7 * - tags naming its project and deployment, which arrive with each event;
8 * - OpenTelemetry's standard variables, so an SDK in the app sends spans,
9 * logs and metrics to the project without configuration: the endpoint,
10 * the project's deployments token, the service's name and a resource
11 * naming the environment, deployment and commit. A variable the project
12 * sets itself wins: an app can send elsewhere.
13 * - under its hostname in `DOMAINS`, what the dispatcher writes on each
14 * request's line, so requests a site's files answered are filed too.
15 */
16import { TELEMETRY_OTLP_ENDPOINT, TELEMETRY_TAG } from "@g1t/contracts/telemetry";
17
18/** The telemetry service's Worker, named as each app's Tail Worker. */
19export const TELEMETRY_WORKER = "g1t-telemetry";
20
21export const TAIL_CONSUMERS = [{ service: TELEMETRY_WORKER }];
22
23/**
24 * Cloudflare's own tracing of an app (a span for each fetch, KV, D1 and
25 * other call it makes), sent to the Workers Observability destination
26 * named `destination`, which points at the telemetry service's
27 * `/cloudflare/v1/traces`. Traces only: the app's logs already arrive
28 * through its Tail Worker, and nothing is kept in Cloudflare's own
29 * storage. Nothing at all without a destination.
30 */
31export function observabilityMetadata(destination: string | null | undefined): Record<string, unknown> {
32 if (!destination) return {};
33 return {
34 observability: {
35 enabled: true,
36 head_sampling_rate: 1,
37 logs: { enabled: false, invocation_logs: false },
38 traces: { enabled: true, destinations: [destination], head_sampling_rate: 1, persist: false },
39 },
40 };
41}
42
43export function telemetryTags(projectId: string, deploymentId: string): string[] {
44 return [`${TELEMETRY_TAG.project}${projectId}`, `${TELEMETRY_TAG.deployment}${deploymentId}`];
45}
46
47export type AppTelemetry = {
48 project: string;
49 deployment: string;
50 environment: string;
51 service: string;
52 commit: string | null;
53};
54
55type Runtime = { secrets: Record<string, string>; variables: Record<string, string> };
56
57/** OpenTelemetry's resource attributes, written as its variable takes them. */
58export function resourceAttributes(app: AppTelemetry): string {
59 const pairs: [string, string][] = [
60 ["deployment.environment.name", app.environment],
61 ["g1t.deployment.id", app.deployment],
62 ["g1t.project.id", app.project],
63 ];
64 if (app.commit) pairs.push(["vcs.ref.head.revision", app.commit]);
65 return pairs.map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join(",");
66}
67
68/** The app's runtime with OpenTelemetry's variables added, where the project set none of its own. */
69export function withOtel(runtime: Runtime, app: AppTelemetry, token: string | null): Runtime {
70 const has = (name: string) => name in runtime.variables || name in runtime.secrets;
71 const variables = { ...runtime.variables };
72 const secrets = { ...runtime.secrets };
73 // An app the project points elsewhere is left to it, whole.
74 if (has("OTEL_EXPORTER_OTLP_ENDPOINT") || !token) return runtime;
75 variables.OTEL_EXPORTER_OTLP_ENDPOINT = TELEMETRY_OTLP_ENDPOINT;
76 if (!has("OTEL_EXPORTER_OTLP_PROTOCOL")) variables.OTEL_EXPORTER_OTLP_PROTOCOL = "http/json";
77 if (!has("OTEL_SERVICE_NAME")) variables.OTEL_SERVICE_NAME = app.service;
78 const resource = resourceAttributes(app);
79 variables.OTEL_RESOURCE_ATTRIBUTES = runtime.variables.OTEL_RESOURCE_ATTRIBUTES ? `${resource},${runtime.variables.OTEL_RESOURCE_ATTRIBUTES}` : resource;
80 if (!has("OTEL_EXPORTER_OTLP_HEADERS")) secrets.OTEL_EXPORTER_OTLP_HEADERS = `authorization=Bearer ${token}`;
81 return { variables, secrets };
82}