Apps on g1t.page send Cloudflare's own traces to Observability: a span for each fetch, KV, D1, R2 and Durable Object call, with nothing in their code. Each upload names the Workers Observability destination g1t-traces (TRACES_DESTINATION), which sends OTLP to otel.g1t.sh/cloudflare/v1/traces with the telemetry service's export key; traces only, since logs already arrive through the Tail Worker, and nothing is kept in Cloudflare's own storage. The git guide says that with two-factor authentication on, git takes an access token, not the account password.
7 files+86−60/7 viewed
| 30 | 30 | Pushing, and reading private repositories, needs credentials. Use your | |
| 31 | 31 | username, and as the password either your account password or an | |
| 32 | 32 | [access token](https://g1t.sh/settings/tokens). Tokens are recommended: they can be revoked | |
| 33 | − | individually and they also work for the API. | |
| 33 | + | individually and they also work for the API. With | |
| 34 | + | [two-factor authentication](/guides/authentication/#two-factor-authentication) | |
| 35 | + | on, your password is not accepted for git, since it would get past your | |
| 36 | + | second factor: use a token, with Code at read and write to push. | |
| 34 | 37 | ||
| 35 | 38 | To avoid typing it each time, let git store it: | |
| 36 | 39 |
| 127 | 127 | export default instrument({ async fetch(request, env) { /* … */ } }, config); | |
| 128 | 128 | ``` | |
| 129 | 129 | ||
| 130 | + | ### Cloudflare's own spans | |
| 131 | + | ||
| 132 | + | Every app on g1t.page is also traced by Cloudflare itself, with nothing in | |
| 133 | + | its code: a span for each `fetch` it makes, each KV, D1, R2 and Durable | |
| 134 | + | Object call, and its cache, under the request. They arrive in the | |
| 135 | + | request's trace, next to any spans of the app's own SDK. Its logs are not | |
| 136 | + | sent this way: they already arrive with each request. | |
| 137 | + | ||
| 130 | 138 | Set `OTEL_EXPORTER_OTLP_ENDPOINT` yourself, under the project's | |
| 131 | 139 | [secrets and variables](/guides/secrets-and-variables/), to send the | |
| 132 | 140 | app's OpenTelemetry somewhere else instead: g1t then sets none of these. | |
| ⋯ | |||
| 263 | 271 | pages. Log bodies are indexed for full-text search. | |
| 264 | 272 | ||
| 265 | 273 | - **g1t.page.** The dispatcher in front of every app names the telemetry | |
| 266 | − | service as its Tail Worker, and so does each app. The dispatcher's half | |
| 274 | + | service as its Tail Worker, and so does each app. Each app's own | |
| 275 | + | Cloudflare tracing is exported to the service too, over OTLP, and filed | |
| 276 | + | under the app's project and deployment. The dispatcher's half | |
| 267 | 277 | of a request (status, time, place) and the app's half (logs, exceptions, | |
| 268 | 278 | CPU time) arrive apart and are joined by the request's id. | |
| 269 | 279 | - **OTLP.** The endpoint reads the project from the token, checks it | |
| 9 | 9 | * else. | |
| 10 | 10 | */ | |
| 11 | 11 | ||
| 12 | − | import { TAIL_CONSUMERS } from "./telemetry"; | |
| 12 | + | import { TAIL_CONSUMERS, observabilityMetadata } from "./telemetry"; | |
| 13 | 13 | import { APP_CLASS, APP_WORKER, REGISTRY_HOST, applicationName, applicationRequest, pushLoginRequest, rolloutRequest } from "./containers"; | |
| 14 | 14 | import { redirectScript } from "./redirect"; | |
| 15 | 15 | ||
| ⋯ | |||
| 59 | 59 | private readonly token: string, | |
| 60 | 60 | private readonly account: string, | |
| 61 | 61 | readonly namespace: string, | |
| 62 | + | /** The Workers Observability destination apps send their traces to (src/telemetry.ts); none, none sent. */ | |
| 63 | + | private readonly tracesDestination: string | null = null, | |
| 62 | 64 | ) {} | |
| 63 | 65 | ||
| 64 | 66 | private async call<T>(method: string, path: string, body?: BodyInit, contentType?: string): Promise<T> { | |
| ⋯ | |||
| 142 | 144 | tags, | |
| 143 | 145 | // Each request it answers, with its logs, goes to observability (src/telemetry.ts). | |
| 144 | 146 | tail_consumers: TAIL_CONSUMERS, | |
| 147 | + | ...observabilityMetadata(this.tracesDestination), | |
| 145 | 148 | ...(completionJwt ? { assets: { jwt: completionJwt, config: assetsConfig } } : {}), | |
| 146 | 149 | }), | |
| 147 | 150 | ); | |
| ⋯ | |||
| 209 | 212 | /** Puts a container app's Worker (`APP_WORKER`, g1t's own) in the namespace, with `metadata`. */ | |
| 210 | 213 | async putContainerWorker(script: string, metadata: Record<string, unknown>): Promise<void> { | |
| 211 | 214 | const form = new FormData(); | |
| 212 | − | form.append("metadata", JSON.stringify(metadata)); | |
| 215 | + | form.append("metadata", JSON.stringify({ ...metadata, ...observabilityMetadata(this.tracesDestination) })); | |
| 213 | 216 | form.append("index.js", new File([APP_WORKER], "index.js", { type: "application/javascript+module" })); | |
| 214 | 217 | await this.call("PUT", this.scriptPath(script), form); | |
| 215 | 218 | } | |
| 134 | 134 | DOMAINS?: KVNamespace; | |
| 135 | 135 | /** Observability: each project's deployments token (services/telemetry). */ | |
| 136 | 136 | TELEMETRY?: ServiceBinding; | |
| 137 | + | /** | |
| 138 | + | * The Workers Observability destination apps send Cloudflare's own | |
| 139 | + | * traces to (src/telemetry.ts); empty, none are sent. | |
| 140 | + | */ | |
| 141 | + | TRACES_DESTINATION?: string; | |
| 137 | 142 | /** The g1t.page zone, where custom hostnames are added (Cloudflare for SaaS). */ | |
| 138 | 143 | CUSTOM_HOSTNAMES_ZONE_ID?: string; | |
| 139 | 144 | /** | |
| ⋯ | |||
| 373 | 378 | ||
| 374 | 379 | private get cloudflare(): Cloudflare | null { | |
| 375 | 380 | const token = this.env.CLOUDFLARE_API_TOKEN; | |
| 376 | − | return token ? new Cloudflare(token, this.env.CLOUDFLARE_ACCOUNT_ID, this.env.DISPATCH_NAMESPACE) : null; | |
| 381 | + | return token ? new Cloudflare(token, this.env.CLOUDFLARE_ACCOUNT_ID, this.env.DISPATCH_NAMESPACE, this.env.TRACES_DESTINATION || null) : null; | |
| 377 | 382 | } | |
| 378 | 383 | ||
| 379 | 384 | private get db() { | |
| 1 | + | import assert from "node:assert/strict"; | |
| 2 | + | import test from "node:test"; | |
| 3 | + | ||
| 4 | + | import { observabilityMetadata, resourceAttributes, telemetryTags, withOtel } from "./telemetry.ts"; | |
| 5 | + | ||
| 6 | + | const app = { project: "prj_1", deployment: "dpl_1", environment: "production", service: "web", commit: "abc123" }; | |
| 7 | + | ||
| 8 | + | test("an app's OpenTelemetry variables point at the project, unless it set its own", () => { | |
| 9 | + | const given = withOtel({ secrets: {}, variables: { FROM: "config" } }, app, "g1to_prj_1_secret"); | |
| 10 | + | assert.equal(given.variables.OTEL_EXPORTER_OTLP_ENDPOINT, "https://otel.g1t.sh"); | |
| 11 | + | assert.equal(given.variables.OTEL_SERVICE_NAME, "web"); | |
| 12 | + | assert.equal(given.secrets.OTEL_EXPORTER_OTLP_HEADERS, "authorization=Bearer g1to_prj_1_secret"); | |
| 13 | + | assert.match(given.variables.OTEL_RESOURCE_ATTRIBUTES, /g1t\.deployment\.id=dpl_1/); | |
| 14 | + | assert.equal(given.variables.FROM, "config"); | |
| 15 | + | // The project's own endpoint wins, whole. | |
| 16 | + | const own = { secrets: {}, variables: { OTEL_EXPORTER_OTLP_ENDPOINT: "https://collector.example.com" } }; | |
| 17 | + | assert.deepEqual(withOtel(own, app, "g1to_x"), own); | |
| 18 | + | // No token (an untrusted build): nothing added. | |
| 19 | + | assert.deepEqual(withOtel({ secrets: {}, variables: {} }, app, null), { secrets: {}, variables: {} }); | |
| 20 | + | }); | |
| 21 | + | ||
| 22 | + | test("tags and resource attributes name the project and deployment", () => { | |
| 23 | + | assert.deepEqual(telemetryTags("prj_1", "dpl_1"), ["g1t-p:prj_1", "g1t-d:dpl_1"]); | |
| 24 | + | assert.equal(resourceAttributes(app), "deployment.environment.name=production,g1t.deployment.id=dpl_1,g1t.project.id=prj_1,vcs.ref.head.revision=abc123"); | |
| 25 | + | }); | |
| 26 | + | ||
| 27 | + | test("Cloudflare's traces go to the destination, logs stay with the Tail Worker", () => { | |
| 28 | + | assert.deepEqual(observabilityMetadata(null), {}); | |
| 29 | + | const metadata = observabilityMetadata("g1t-traces").observability as Record<string, any>; | |
| 30 | + | assert.equal(metadata.enabled, true); | |
| 31 | + | assert.deepEqual(metadata.traces.destinations, ["g1t-traces"]); | |
| 32 | + | assert.equal(metadata.traces.persist, false); | |
| 33 | + | assert.equal(metadata.logs.enabled, false); | |
| 34 | + | }); |
| 20 | 20 | ||
| 21 | 21 | export const TAIL_CONSUMERS = [{ service: TELEMETRY_WORKER }]; | |
| 22 | 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 | + | */ | |
| 31 | + | export 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 | + | ||
| 23 | 43 | export function telemetryTags(projectId: string, deploymentId: string): string[] { | |
| 24 | 44 | return [`${TELEMETRY_TAG.project}${projectId}`, `${TELEMETRY_TAG.deployment}${deploymentId}`]; | |
| 25 | 45 | } |
| 57 | 57 | // while off, a container build is refused at its registry step. | |
| 58 | 58 | "CONTAINERS": "off", | |
| 59 | 59 | // Where container apps' Workers report how long they ran. | |
| 60 | − | "G1T_API": "https://api.g1t.sh" | |
| 60 | + | "G1T_API": "https://api.g1t.sh", | |
| 61 | + | // Cloudflare's own tracing of each app (a span for every fetch, KV, | |
| 62 | + | // D1 and other call), sent to this Workers Observability destination | |
| 63 | + | // on the account, which points at otel.g1t.sh/cloudflare/v1/traces | |
| 64 | + | // with the telemetry service's CLOUDFLARE_EXPORT_KEY. Empty: none. | |
| 65 | + | "TRACES_DESTINATION": "g1t-traces" | |
| 61 | 66 | }, | |
| 62 | 67 | // Custom domains: hostname to app, written here and read by the | |
| 63 | 68 | // dispatcher (services/pages). Made by scripts/setup-custom-domains.sh. |