Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Projects have Observability: their requests, logs, traces, metrics and errors, from apps on g1t.page with nothing to set up, and from software anywhere else over OpenTelemetry. | 1 | /** |
| 2 | * Observability: what a project's running software did, as requests, logs, | |
| 3 | * traces and metrics. The telemetry service (`services/telemetry`) keeps | |
| 4 | * one store per project (a Durable Object with its own SQLite), filled from | |
| 5 | * two doors: | |
| 6 | * | |
| 7 | * - **Deployments on g1t.page**, with nothing to set up: every app the | |
| 8 | * deployments service uploads names the telemetry service as its Tail | |
| 9 | * Worker, so each request it answers arrives with its logs, exceptions, | |
| 10 | * status, CPU and wall time. Its tags (`TELEMETRY_TAG`) say which project | |
| 11 | * and deployment it belongs to. | |
| 12 | * - **OpenTelemetry (OTLP over HTTP)** from anywhere, in JSON or protobuf: | |
| 13 | * `https://otel.g1t.sh/v1/{traces,logs,metrics}` with a project's ingest | |
| 14 | * token. Apps on g1t.page get the endpoint and a token as | |
| 15 | * `OTEL_EXPORTER_OTLP_*` variables, so an OpenTelemetry SDK in them sends | |
| 16 | * spans without configuration; software hosted anywhere else sends the | |
| 17 | * same way. | |
| 18 | * | |
| 19 | * Wire shapes are snake_case end to end. Times are microseconds since the | |
| 20 | * Unix epoch (`*_us`): exact enough for spans, and safe in a JSON number. | |
| 21 | */ | |
| 22 | import type { ServiceBinding } from "./clients"; | |
| 23 | ||
| 24 | /** Where telemetry is sent from outside: OTLP's base, under which SDKs add `/v1/traces` and so on. */ | |
| 25 | export const TELEMETRY_OTLP_ENDPOINT = "https://otel.g1t.sh"; | |
| 26 | ||
| 27 | /** The prefix of a project's ingest token, so a leaked one is recognisable. */ | |
| 28 | export const TELEMETRY_TOKEN_PREFIX = "g1to_"; | |
| 29 | ||
| 30 | /** | |
| 31 | * The tags each app on g1t.page carries for its telemetry (script tags in | |
| 32 | * the dispatch namespace; they arrive with every tail event): | |
| 33 | * `g1t-p:<project id>` and `g1t-d:<deployment id>`. The environment is | |
| 34 | * the tag `production` or `preview` the app already has. | |
| 35 | */ | |
| 36 | export const TELEMETRY_TAG = { project: "g1t-p:", deployment: "g1t-d:" } as const; | |
| 37 | ||
| 38 | /** Where a record came from: an app on g1t.page, or OTLP from an SDK or a collector. */ | |
| 39 | export type TelemetrySource = "cloudflare" | "otlp"; | |
| 40 | ||
| 41 | /** Log levels, as OpenTelemetry's severity numbers group them. */ | |
| 42 | export type LogSeverity = "trace" | "debug" | "info" | "warn" | "error" | "fatal"; | |
| 43 | export const LOG_SEVERITIES: readonly LogSeverity[] = ["trace", "debug", "info", "warn", "error", "fatal"]; | |
| 44 | ||
| 45 | /** An attribute's value, as OpenTelemetry allows: arrays and maps flattened to JSON where they nest. */ | |
| 46 | export type AttributeValue = string | number | boolean | null | AttributeValue[] | { [key: string]: AttributeValue }; | |
| 47 | export type Attributes = Record<string, AttributeValue>; | |
| 48 | ||
| 49 | /** | |
| 50 | * What every record is filed under: which software sent it (`service`, | |
| 51 | * OpenTelemetry's `service.name`; an app on g1t.page is its project's | |
| 52 | * name), where it ran (`environment`: production, preview, or what | |
| 53 | * `deployment.environment.name` said) and, on g1t.page, which deployment. | |
| 54 | */ | |
| 55 | export type TelemetryOrigin = { | |
| 56 | service: string; | |
| 57 | environment: string | null; | |
| 58 | deployment_id: string | null; | |
| 59 | /** The commit it was built from, when known (`vcs.ref.head.revision` or the deployment's). */ | |
| 60 | commit_sha: string | null; | |
| 61 | source: TelemetrySource; | |
| 62 | }; | |
| 63 | ||
| 64 | /** | |
| 65 | * One request (or other invocation) an app on g1t.page answered: the tail | |
| 66 | * event, with the logs it wrote under it. Also made from each server span | |
| 67 | * sent over OTLP that has no parent here, so software hosted elsewhere | |
| 68 | * shows the same numbers. | |
| 69 | */ | |
| 70 | export type TelemetryRequest = TelemetryOrigin & { | |
| 71 | id: string; | |
| 72 | time_us: number; | |
| 73 | duration_us: number; | |
| 74 | /** fetch, scheduled, queue, alarm, email, rpc or other. */ | |
| 75 | trigger: string; | |
| 76 | method: string | null; | |
| 77 | /** Host and path, without the query. */ | |
| 78 | url: string | null; | |
| 79 | /** The path with ids taken out (`/users/:id`), what the numbers group by. */ | |
| 80 | route: string | null; | |
| 81 | status: number | null; | |
| 82 | /** ok, exception, exceededCpu, canceled and the rest, as Cloudflare says; ok or error over OTLP. */ | |
| 83 | outcome: string; | |
| 84 | cpu_ms: number | null; | |
| 85 | /** Where it ran: Cloudflare's colo, or `cloud.region`. */ | |
| 86 | region: string | null; | |
| 87 | country: string | null; | |
| 88 | trace_id: string | null; | |
| 89 | /** How many log lines it wrote; `logs` fills them in on the request's own page. */ | |
| 90 | log_count: number; | |
| 91 | error: { name: string; message: string } | null; | |
| 92 | }; | |
| 93 | ||
| 94 | export type TelemetryLog = TelemetryOrigin & { | |
| 95 | id: string; | |
| 96 | time_us: number; | |
| 97 | severity: LogSeverity; | |
| 98 | /** OpenTelemetry's 1–24. */ | |
| 99 | severity_number: number; | |
| 100 | body: string; | |
| 101 | trace_id: string | null; | |
| 102 | span_id: string | null; | |
| 103 | /** The request it was written under, for an app on g1t.page. */ | |
| 104 | request_id: string | null; | |
| 105 | attributes: Attributes; | |
| 106 | }; | |
| 107 | ||
| 108 | export type SpanKind = "unspecified" | "internal" | "server" | "client" | "producer" | "consumer"; | |
| 109 | export type SpanStatus = "unset" | "ok" | "error"; | |
| 110 | ||
| 111 | export type TelemetrySpan = TelemetryOrigin & { | |
| 112 | trace_id: string; | |
| 113 | span_id: string; | |
| 114 | parent_span_id: string | null; | |
| 115 | name: string; | |
| 116 | kind: SpanKind; | |
| 117 | start_us: number; | |
| 118 | duration_us: number; | |
| 119 | status: SpanStatus; | |
| 120 | status_message: string | null; | |
| 121 | attributes: Attributes; | |
| 122 | resource: Attributes; | |
| 123 | events: { time_us: number; name: string; attributes: Attributes }[]; | |
| 124 | links: { trace_id: string; span_id: string }[]; | |
| 125 | }; | |
| 126 | ||
| 127 | /** A trace in a list: its root (or earliest) span, and what the rest add up to. */ | |
| 128 | export type TraceSummary = TelemetryOrigin & { | |
| 129 | trace_id: string; | |
| 130 | name: string; | |
| 131 | start_us: number; | |
| 132 | duration_us: number; | |
| 133 | span_count: number; | |
| 134 | error_count: number; | |
| 135 | services: string[]; | |
| 136 | status: number | null; | |
| 137 | }; | |
| 138 | ||
| 139 | export type TraceDetail = { | |
| 140 | trace_id: string; | |
| 141 | spans: TelemetrySpan[]; | |
| 142 | logs: TelemetryLog[]; | |
| 143 | }; | |
| 144 | ||
| 145 | export type MetricKind = "gauge" | "sum" | "histogram" | "summary"; | |
| 146 | ||
| 147 | export type MetricInfo = { | |
| 148 | name: string; | |
| 149 | kind: MetricKind; | |
| 150 | unit: string; | |
| 151 | description: string; | |
| 152 | services: string[]; | |
| 153 | last_us: number; | |
| 154 | }; | |
| 155 | ||
| 156 | /** A metric over time: one value per step, by the attribute it was grouped by. */ | |
| 157 | export type MetricSeries = { | |
| 158 | name: string; | |
| 159 | unit: string; | |
| 160 | step_s: number; | |
| 161 | series: { group: string | null; points: { time_us: number; value: number | null }[] }[]; | |
| 162 | }; | |
| 163 | ||
| 164 | /** The latency (and CPU) percentiles of a set of requests, in milliseconds. */ | |
| 165 | export type Percentiles = { p50: number | null; p75: number | null; p95: number | null; p99: number | null }; | |
| 166 | ||
| 167 | /** One step of the overview's charts. */ | |
| 168 | export type TelemetryPoint = { | |
| 169 | time_us: number; | |
| 170 | requests: number; | |
| 171 | errors: number; | |
| 172 | status_2xx: number; | |
| 173 | status_3xx: number; | |
| 174 | status_4xx: number; | |
| 175 | status_5xx: number; | |
| 176 | latency: Percentiles; | |
| 177 | cpu: Percentiles; | |
| 178 | }; | |
| 179 | ||
| 180 | /** Numbers for a period, and the same for the one before it, to compare. */ | |
| 181 | export type TelemetryTotals = { | |
| 182 | requests: number; | |
| 183 | errors: number; | |
| 184 | error_rate: number; | |
| 185 | latency: Percentiles; | |
| 186 | cpu: Percentiles; | |
| 187 | }; | |
| 188 | ||
| 189 | export type TelemetryOverview = { | |
| 190 | from_us: number; | |
| 191 | to_us: number; | |
| 192 | step_s: number; | |
| 193 | totals: TelemetryTotals; | |
| 194 | previous: TelemetryTotals; | |
| 195 | points: TelemetryPoint[]; | |
| 196 | routes: { route: string; method: string | null; requests: number; errors: number; latency: Percentiles }[]; | |
| 197 | /** Each deployment's numbers over the period, newest first: a regression points at the deployment it began with. */ | |
| 198 | deployments: { deployment_id: string; environment: string | null; commit_sha: string | null; first_us: number; requests: number; errors: number; latency: Percentiles }[]; | |
| 199 | services: { service: string; source: TelemetrySource; requests: number; errors: number; last_us: number }[]; | |
| 200 | regions: { region: string; requests: number }[]; | |
| 201 | }; | |
| 202 | ||
| 203 | /** | |
| 204 | * Errors, grouped: the same exception (name, message with its numbers and | |
| 205 | * ids taken out, and the top frame of its stack) is one group. | |
| 206 | */ | |
| 207 | export type ErrorGroup = { | |
| 208 | fingerprint: string; | |
| 209 | name: string; | |
| 210 | message: string; | |
| 211 | stack: string | null; | |
| 212 | service: string; | |
| 213 | environment: string | null; | |
| 214 | first_us: number; | |
| 215 | last_us: number; | |
| 216 | count: number; | |
| 217 | /** The deployment it was first seen on: what likely brought it in. */ | |
| 218 | first_deployment_id: string | null; | |
| 219 | last_request_id: string | null; | |
| 220 | last_trace_id: string | null; | |
| 221 | /** resolved, ignored or open. A resolved group that happens again opens again. */ | |
| 222 | state: "open" | "resolved" | "ignored"; | |
| 223 | /** The issue made for it, so an agent can take it. */ | |
| 224 | issue_number: number | null; | |
| 225 | }; | |
| 226 | ||
| 227 | /** What every list query can narrow by. */ | |
| 228 | export type TelemetryFilter = { | |
| 229 | from_us?: number | null; | |
| 230 | to_us?: number | null; | |
| 231 | services?: string[] | null; | |
| 232 | environments?: string[] | null; | |
| 233 | deployment_id?: string | null; | |
| 234 | source?: TelemetrySource | null; | |
| 235 | }; | |
| 236 | ||
| 237 | export type LogQuery = TelemetryFilter & { | |
| 238 | /** Words to find in the body (all of them), or a phrase in quotes. */ | |
| 239 | text?: string | null; | |
| 240 | severities?: LogSeverity[] | null; | |
| 241 | trace_id?: string | null; | |
| 242 | request_id?: string | null; | |
| 243 | /** Where to carry on from: the last page's `next`. */ | |
| 244 | cursor?: string | null; | |
| 245 | limit?: number | null; | |
| 246 | }; | |
| 247 | ||
| 248 | export type RequestQuery = TelemetryFilter & { | |
| 249 | text?: string | null; | |
| 250 | /** 2xx, 3xx, 4xx, 5xx, or a status. */ | |
| 251 | status?: string[] | null; | |
| 252 | /** Only those that failed: a 5xx, or an outcome other than ok. */ | |
| 253 | errors_only?: boolean | null; | |
| 254 | route?: string | null; | |
| 255 | min_duration_ms?: number | null; | |
| 256 | cursor?: string | null; | |
| 257 | limit?: number | null; | |
| 258 | }; | |
| 259 | ||
| 260 | export type TraceQuery = TelemetryFilter & { | |
| 261 | text?: string | null; | |
| 262 | errors_only?: boolean | null; | |
| 263 | min_duration_ms?: number | null; | |
| 264 | cursor?: string | null; | |
| 265 | limit?: number | null; | |
| 266 | }; | |
| 267 | ||
| 268 | export type Page<T> = { items: T[]; next: string | null }; | |
| 269 | ||
| 270 | /** A project's ingest token, as listed: the secret itself is shown once, when made. */ | |
| 271 | export type IngestToken = { | |
| 272 | id: string; | |
| 273 | name: string; | |
| 274 | /** The first characters, to tell tokens apart. */ | |
| 275 | hint: string; | |
| 276 | created_by: string | null; | |
| 277 | created_us: number; | |
| 278 | last_used_us: number | null; | |
| 279 | /** Made by g1t for its deployments; replaced on its own, not by people. */ | |
| 280 | managed: boolean; | |
| 281 | }; | |
| 282 | ||
| 283 | /** | |
| 284 | * Where a project's telemetry is also sent, as it arrives: any OTLP/HTTP | |
| 285 | * endpoint (a collector, or a vendor that takes OTLP), with the headers it | |
| 286 | * needs. Header values are secrets: written, never read back. | |
| 287 | */ | |
| 288 | export type TelemetryExport = { | |
| 289 | id: string; | |
| 290 | name: string; | |
| 291 | endpoint: string; | |
| 292 | /** Which signals go: traces, logs (with requests as log records) and metrics. */ | |
| 293 | signals: ("traces" | "logs" | "metrics")[]; | |
| 294 | header_names: string[]; | |
| 295 | enabled: boolean; | |
| 296 | last_error: string | null; | |
| 297 | last_sent_us: number | null; | |
| 298 | }; | |
| 299 | ||
| 300 | export type TelemetrySettings = { | |
| 301 | /** Days requests, logs and spans are kept. Charts are kept longer, as per-minute counts. */ | |
| 302 | retention_days: number; | |
| 303 | /** Whether apps on g1t.page send. On by default; off keeps the tokens and history. */ | |
| 304 | deployments: boolean; | |
| 305 | }; | |
| 306 | ||
| 307 | export const DEFAULT_TELEMETRY_SETTINGS: TelemetrySettings = { retention_days: 7, deployments: true }; | |
| 308 | ||
| 309 | /** Retention a project can choose, in days. */ | |
| 310 | export const RETENTION_CHOICES = [1, 3, 7, 14, 30] as const; | |
| 311 | ||
| 312 | /** How much a project has stored and sent this month, for the page and for billing. */ | |
| 313 | export type TelemetryUsage = { | |
| 314 | month: string; | |
| 315 | /** Bytes received, after decoding: what ingest is metered by. */ | |
| 316 | ingested_bytes: number; | |
| 317 | requests: number; | |
| 318 | logs: number; | |
| 319 | spans: number; | |
| 320 | metric_points: number; | |
| 321 | stored_bytes: number; | |
| Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them. | 322 | /** Bytes received today (UTC): a workspace without the plan sends a thirtieth of its monthly observability a day. */ |
| 323 | today_bytes?: number; | |
| Projects have Observability: their requests, logs, traces, metrics and errors, from apps on g1t.page with nothing to set up, and from software anywhere else over OpenTelemetry. | 324 | }; |
| 325 | ||
| 326 | /** Whom a call is for: the site and the API check access before they ask. */ | |
| Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them. | 327 | type ProjectRef = { project_id: string; /** The project's workspace, by slug: the store remembers it, for a free workspace's cap. */ workspace?: string }; |
| Projects have Observability: their requests, logs, traces, metrics and errors, from apps on g1t.page with nothing to set up, and from software anywhere else over OpenTelemetry. | 328 | |
| 329 | export interface TelemetryApi { | |
| 330 | overview(args: ProjectRef & TelemetryFilter & { step_s?: number | null }): Promise<TelemetryOverview>; | |
| 331 | requests(args: ProjectRef & RequestQuery): Promise<Page<TelemetryRequest>>; | |
| 332 | request(args: ProjectRef & { id: string }): Promise<{ request: TelemetryRequest; logs: TelemetryLog[] } | null>; | |
| 333 | logs(args: ProjectRef & LogQuery): Promise<Page<TelemetryLog>>; | |
| 334 | traces(args: ProjectRef & TraceQuery): Promise<Page<TraceSummary>>; | |
| 335 | trace(args: ProjectRef & { trace_id: string }): Promise<TraceDetail | null>; | |
| 336 | errors(args: ProjectRef & TelemetryFilter & { state?: ErrorGroup["state"] | null }): Promise<ErrorGroup[]>; | |
| 337 | setError(args: ProjectRef & { fingerprint: string; state?: ErrorGroup["state"]; issue_number?: number | null }): Promise<ErrorGroup | null>; | |
| 338 | metrics(args: ProjectRef): Promise<MetricInfo[]>; | |
| 339 | metric(args: ProjectRef & TelemetryFilter & { name: string; aggregate?: "avg" | "sum" | "min" | "max" | "p50" | "p95" | "p99" | null; group_by?: string | null; step_s?: number | null }): Promise<MetricSeries>; | |
| 340 | /** What is in the store: services, environments and attribute keys seen, for filters. */ | |
| 341 | facets(args: ProjectRef): Promise<{ services: string[]; environments: string[]; attribute_keys: string[] }>; | |
| 342 | settings(args: ProjectRef): Promise<TelemetrySettings>; | |
| 343 | setSettings(args: ProjectRef & Partial<TelemetrySettings>): Promise<TelemetrySettings>; | |
| 344 | tokens(args: ProjectRef): Promise<IngestToken[]>; | |
| 345 | createToken(args: ProjectRef & { name: string; created_by: string }): Promise<{ token: IngestToken; secret: string }>; | |
| 346 | revokeToken(args: ProjectRef & { id: string }): Promise<boolean>; | |
| 347 | /** | |
| 348 | * The token g1t's own deployments of the project send with, made the | |
| 349 | * first time: the deployments service binds it to each app it uploads. | |
| 350 | */ | |
| 351 | deploymentToken(args: ProjectRef): Promise<{ secret: string }>; | |
| 352 | exports(args: ProjectRef): Promise<TelemetryExport[]>; | |
| 353 | putExport(args: ProjectRef & { id?: string | null; name: string; endpoint: string; signals: TelemetryExport["signals"]; headers?: Record<string, string> | null; enabled: boolean }): Promise<TelemetryExport>; | |
| 354 | deleteExport(args: ProjectRef & { id: string }): Promise<boolean>; | |
| 355 | usage(args: ProjectRef): Promise<TelemetryUsage>; | |
| 356 | /** Everything kept for a project, gone: when the project is deleted. */ | |
| 357 | purge(args: ProjectRef): Promise<boolean>; | |
| 358 | } | |
| 359 | ||
| 360 | /** | |
| 361 | * The site's live tail: `GET /live?project_id=…` on the service, a | |
| 362 | * WebSocket the site opens after checking the viewer may read the | |
| 363 | * project. Each message is one `LiveItem`. | |
| 364 | */ | |
| 365 | export type LiveItem = | |
| 366 | | { type: "request"; request: TelemetryRequest } | |
| 367 | | { type: "log"; log: TelemetryLog } | |
| 368 | | { type: "span"; span: TelemetrySpan }; | |
| 369 | ||
| 370 | async function call<T>(service: ServiceBinding, method: string, args: object): Promise<T> { | |
| 371 | const response = await service.fetch(`https://service/rpc/${method}`, { | |
| 372 | method: "POST", | |
| 373 | headers: { "content-type": "application/json" }, | |
| 374 | body: JSON.stringify(args), | |
| 375 | }); | |
| 376 | if (!response.ok) throw new Error(`telemetry ${method} failed with status ${response.status}: ${await response.text()}`); | |
| 377 | return (await response.json()) as T; | |
| 378 | } | |
| 379 | ||
| 380 | export function telemetryClient(service: ServiceBinding): TelemetryApi { | |
| 381 | const c = <T>(method: string) => (args: object) => call<T>(service, method, args); | |
| 382 | return { | |
| 383 | overview: c("overview"), | |
| 384 | requests: c("requests"), | |
| 385 | request: c("request"), | |
| 386 | logs: c("logs"), | |
| 387 | traces: c("traces"), | |
| 388 | trace: c("trace"), | |
| 389 | errors: c("errors"), | |
| 390 | setError: c("set_error"), | |
| 391 | metrics: c("metrics"), | |
| 392 | metric: c("metric"), | |
| 393 | facets: c("facets"), | |
| 394 | settings: c("settings"), | |
| 395 | setSettings: c("set_settings"), | |
| 396 | tokens: c("tokens"), | |
| 397 | createToken: c("create_token"), | |
| 398 | revokeToken: c("revoke_token"), | |
| 399 | deploymentToken: c("deployment_token"), | |
| 400 | exports: c("exports"), | |
| 401 | putExport: c("put_export"), | |
| 402 | deleteExport: c("delete_export"), | |
| 403 | usage: c("usage"), | |
| 404 | purge: c("purge"), | |
| 405 | }; | |
| 406 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.