Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
406 linesCodeBlameRaw

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 */
22import type { ServiceBinding } from "./clients";
23
24/** Where telemetry is sent from outside: OTLP's base, under which SDKs add `/v1/traces` and so on. */
25export const TELEMETRY_OTLP_ENDPOINT = "https://otel.g1t.sh";
26
27/** The prefix of a project's ingest token, so a leaked one is recognisable. */
28export 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 */
36export 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. */
39export type TelemetrySource = "cloudflare" | "otlp";
40
41/** Log levels, as OpenTelemetry's severity numbers group them. */
42export type LogSeverity = "trace" | "debug" | "info" | "warn" | "error" | "fatal";
43export 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. */
46export type AttributeValue = string | number | boolean | null | AttributeValue[] | { [key: string]: AttributeValue };
47export 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 */
55export 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 */
70export 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
94export 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
108export type SpanKind = "unspecified" | "internal" | "server" | "client" | "producer" | "consumer";
109export type SpanStatus = "unset" | "ok" | "error";
110
111export 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. */
128export 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
139export type TraceDetail = {
140 trace_id: string;
141 spans: TelemetrySpan[];
142 logs: TelemetryLog[];
143};
144
145export type MetricKind = "gauge" | "sum" | "histogram" | "summary";
146
147export 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. */
157export 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. */
165export type Percentiles = { p50: number | null; p75: number | null; p95: number | null; p99: number | null };
166
167/** One step of the overview's charts. */
168export 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. */
181export type TelemetryTotals = {
182 requests: number;
183 errors: number;
184 error_rate: number;
185 latency: Percentiles;
186 cpu: Percentiles;
187};
188
189export 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 */
207export 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. */
228export 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
237export 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
248export 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
260export 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
268export 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. */
271export 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 */
288export 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
300export 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
307export const DEFAULT_TELEMETRY_SETTINGS: TelemetrySettings = { retention_days: 7, deployments: true };
308
309/** Retention a project can choose, in days. */
310export 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. */
313export 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.327type 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
329export 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 */
365export type LiveItem =
366 | { type: "request"; request: TelemetryRequest }
367 | { type: "log"; log: TelemetryLog }
368 | { type: "span"; span: TelemetrySpan };
369
370async 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
380export 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.