@ancplua/qyl-api-schema 11.5.0 → 11.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,12 +4,42 @@ The TypeSpec source of truth for Qyl's client-visible product API. This reposito
4
4
  emits the public schema and contract artifacts; it does not implement the server,
5
5
  storage engine, or OTLP receiver.
6
6
 
7
- Qyl stores and exposes traces, logs, and metrics. The metric surface is read-only.
8
- `GET /api/v1/metrics` lists the metric descriptors recorded for a project,
9
- `GET /api/v1/metrics/{metric_name}/series` lists the distinct attribute streams
10
- under one metric name, and `GET /api/v1/metrics/{metric_name}/query` aggregates one
11
- metric over a time range into buckets. Metric points are written over OTLP, which
12
- this contract does not describe. Profiles are not supported.
7
+ The contract covers every client-visible surface of a qyl collector and runner:
8
+
9
+ - **Traces** — `GET /api/v1/traces`, one trace, and its spans (`api/routes.tsp`,
10
+ `otel/span.tsp`).
11
+ - **Logs** — `GET /api/v1/logs` and the live SSE stream `GET /api/v1/stream/logs`
12
+ with its heartbeat (`api/streaming.tsp`, `otel/logs.tsp`).
13
+ - **Metrics**, read-only — `GET /api/v1/metrics` lists the metric descriptors
14
+ recorded for a project, `GET /api/v1/metrics/{metric_name}/series` lists the
15
+ distinct attribute streams under one metric name, and
16
+ `GET /api/v1/metrics/{metric_name}/query` aggregates one metric over a time range
17
+ into buckets (`models/metrics.tsp`). Metric points are written over OTLP, which
18
+ this contract does not describe. Profiles are not supported.
19
+ - **Sessions** — `GET /api/v1/sessions`, one session, its traces and `/stats`
20
+ (`models/session.tsp`).
21
+ - **Health** — `GET /health` and `GET /alive`, whose `HealthReport` carries the
22
+ `contract_revision` handshake (`models/health.tsp`).
23
+ - **MCP tools** — the input and structured-output models of the qyl MCP tools
24
+ (`models/mcp-tools.tsp`).
25
+ - **Workbench** — the local MCP workbench: sessions, workspaces, servers, protocol
26
+ streams, executions, test cases, suites, evaluation runs and exports
27
+ (`api/workbench.tsp`, `models/workbench.tsp`).
28
+ - **Runner** — the local runner's loopback-only `/runner/resources` snapshot, SSE
29
+ streams, logs and restart/stop actions (`api/runner.tsp`, `models/runner.tsp`).
30
+ - **Shared** — `ProblemDetails` errors, cursor pagination and the common scalars
31
+ (`common/`), plus the OTel resource, span and log projections (`otel/`).
32
+
33
+ Telemetry itself is never changed or deleted through this contract; retention
34
+ removes it. The only writable resources are the ones a user creates:
35
+ annotations (`/api/v1/annotations`, a note of at most 2000 characters on a trace,
36
+ span or session) and saved queries (`/api/v1/saved-queries`, a named structured
37
+ filter for traces, logs or metrics, run with `POST …/{saved_query_id}:run`). Both
38
+ extend the shared write base in `common/resource.tsp`: server-assigned `id`,
39
+ `created_at`, `updated_at` and `etag`; `Idempotency-Key` on create, which answers
40
+ `201` with `Location`; `If-Match` on `PATCH` and `DELETE`, refused with `428` when
41
+ missing and `412` when stale. Collector keys carry the roles `ingest`, `read`,
42
+ `write` and `delete` (`CollectorKeyRole`); writes need `write`, deletes `delete`.
13
43
 
14
44
  ## Contract pipeline
15
45
 
@@ -56,6 +86,18 @@ rather than from prose here; this line moves faster than a README is revised.
56
86
  `main.tsp` is the local compile entry point and includes emitter routing. `index.tsp`
57
87
  is the published TypeSpec entry point and contains only the client-facing contract.
58
88
 
89
+ ### Package exports
90
+
91
+ | Subpath | Content |
92
+ | --- | --- |
93
+ | `@ancplua/qyl-api-schema` | `index.tsp`, the whole TypeSpec contract |
94
+ | `@ancplua/qyl-api-schema/otel` | `otel/otel-conventions.tsp`: only the OTel-shaped resource, span and log models, for TypeSpec consumers that do not want the qyl API |
95
+ | `@ancplua/qyl-api-schema/types` | Generated TypeScript contract types and the `CONTRACT_REVISION` constant |
96
+ | `@ancplua/qyl-api-schema/openapi` | `qyl.openapi.json` |
97
+ | `@ancplua/qyl-api-schema/json-schema` | The bundled JSON Schema |
98
+ | `@ancplua/qyl-api-schema/zod` | Runtime validators, below |
99
+ | `@ancplua/qyl-api-schema/runtime` | Presentation helpers over the generated `AttributeValue` matcher (`decodeAttributeValue`, `formatAttributeValue`, `attributeString`, `attributeNumber`, `attributeIdentity`, `matchAttributeValue`) and the `UnixNanos` string helpers (`parseUnixNanos`, `compareUnixNanos`, `unixNanosDelta`, `unixNanosToEpochMs`, `unixNanosToIso`, `epochMsToUnixNanos`). Authored in `src/runtime/`, compiled to `generated/runtime/` |
100
+
59
101
  ## Runtime validation
60
102
 
61
103
  `@ancplua/qyl-api-schema/zod` builds Zod validators from the bundled JSON Schema at
package/api/routes.tsp CHANGED
@@ -9,6 +9,10 @@ import "../otel/logs.tsp";
9
9
  import "../models/session.tsp";
10
10
  import "../models/health.tsp";
11
11
  import "../models/metrics.tsp";
12
+ import "../common/resource.tsp";
13
+ import "../common/auth.tsp";
14
+ import "../models/annotations.tsp";
15
+ import "../models/saved-queries.tsp";
12
16
 
13
17
  using TypeSpec.Http;
14
18
  using TypeSpec.OpenAPI;
@@ -22,10 +26,13 @@ using Qyl.Api.Contracts.OTel.Metrics;
22
26
  using Qyl.Api.Contracts.OTel.Enums;
23
27
  using Qyl.Api.Contracts.Domains.Observe.Session;
24
28
  using Qyl.Api.Contracts.Health;
29
+ using Qyl.Api.Contracts.Annotations;
30
+ using Qyl.Api.Contracts.SavedQueries;
25
31
 
26
32
  @service(#{ title: "qyl API" })
27
33
  namespace Qyl.Api.Contracts;
28
34
 
35
+ // The query routes require a key with the `read` role (CollectorKeyRole.read).
29
36
  alias QylApiKeyHeaderAuth = ApiKeyAuth<ApiKeyLocation.header, "x-otlp-api-key">;
30
37
 
31
38
  @doc("Optional project scope applied to collector query operations.")
@@ -46,12 +53,28 @@ interface TracesApi {
46
53
  list(
47
54
  ...ProjectScopeHeader,
48
55
 
56
+ @doc("Keep traces that started at or after this time.")
57
+ @query("start_time")
58
+ startTime?: utcDateTime,
59
+
60
+ @doc("Keep traces that started before this time.")
61
+ @query("end_time")
62
+ endTime?: utcDateTime,
63
+
64
+ @doc("Keep traces with at least one span from this service.")
65
+ @query("service_name")
66
+ serviceName?: string,
67
+
68
+ @doc("Keep only traces with at least one error span.")
69
+ @query("errors_only")
70
+ errorsOnly?: boolean,
71
+
49
72
  @query
50
73
  @minValue(1)
51
74
  @maxValue(1000)
52
75
  limit?: int32 = 100,
53
76
 
54
- @query cursor?: string,
77
+ @query cursor?: Cursor,
55
78
  ):
56
79
  | CursorPage<Trace>
57
80
  | ValidationError
@@ -68,8 +91,20 @@ interface TracesApi {
68
91
 
69
92
  @get
70
93
  @route("/{trace_id}/spans")
71
- getSpans(...ProjectScopeHeader, @path("trace_id") traceId: TraceId):
94
+ getSpans(
95
+ ...ProjectScopeHeader,
96
+ @path("trace_id") traceId: TraceId,
97
+
98
+ @doc("Page size. Omitted, every span of the trace is returned in one page.")
99
+ @query
100
+ @minValue(1)
101
+ @maxValue(10000)
102
+ limit?: int32,
103
+
104
+ @query cursor?: Cursor,
105
+ ):
72
106
  | CursorPage<Span>
107
+ | ValidationError
73
108
  | NotFoundError
74
109
  | UnauthorizedError
75
110
  | InternalServerError;
@@ -99,6 +134,8 @@ interface LogsApi {
99
134
  @minValue(1)
100
135
  @maxValue(10000)
101
136
  limit?: int32 = 100,
137
+
138
+ @query cursor?: Cursor,
102
139
  ):
103
140
  | CursorPage<LogRecord>
104
141
  | ValidationError
@@ -132,6 +169,8 @@ interface MetricsApi {
132
169
  @minValue(1)
133
170
  @maxValue(1000)
134
171
  limit?: int32 = 200,
172
+
173
+ @query cursor?: Cursor,
135
174
  ):
136
175
  | CursorPage<MetricDescriptor>
137
176
  | ValidationError
@@ -160,6 +199,8 @@ interface MetricsApi {
160
199
  @minValue(1)
161
200
  @maxValue(1000)
162
201
  limit?: int32 = 200,
202
+
203
+ @query cursor?: Cursor,
163
204
  ):
164
205
  | CursorPage<MetricSeries>
165
206
  | ValidationError
@@ -176,7 +217,6 @@ interface MetricsApi {
176
217
  query(
177
218
  ...ProjectScopeHeader,
178
219
  @path("metric_name") metricName: string,
179
-
180
220
  @query("start_time") startTime: utcDateTime,
181
221
  @query("end_time") endTime: utcDateTime,
182
222
 
@@ -233,7 +273,7 @@ interface SessionsApi {
233
273
  @maxValue(1000)
234
274
  limit?: int32 = 100,
235
275
 
236
- @query cursor?: string,
276
+ @query cursor?: Cursor,
237
277
  ):
238
278
  | CursorPage<SessionEntity>
239
279
  | ValidationError
@@ -250,8 +290,20 @@ interface SessionsApi {
250
290
 
251
291
  @get
252
292
  @route("/{session_id}/traces")
253
- getTraces(...ProjectScopeHeader, @path("session_id") sessionId: SessionId):
293
+ getTraces(
294
+ ...ProjectScopeHeader,
295
+ @path("session_id") sessionId: SessionId,
296
+
297
+ @doc("Page size; the response is bounded even when omitted.")
298
+ @query
299
+ @minValue(1)
300
+ @maxValue(1000)
301
+ limit?: int32 = 100,
302
+
303
+ @query cursor?: Cursor,
304
+ ):
254
305
  | CursorPage<Trace>
306
+ | ValidationError
255
307
  | NotFoundError
256
308
  | UnauthorizedError
257
309
  | InternalServerError;
@@ -265,6 +317,242 @@ interface SessionsApi {
265
317
  ): SessionStats | ValidationError | UnauthorizedError | InternalServerError;
266
318
  }
267
319
 
320
+ // -----------------------------------------------------------------------------
321
+ // Annotations
322
+ //
323
+ // User notes on a trace, span or session (models/annotations.tsp). Reads take
324
+ // a `read` key, create and update a `write` key, delete a `delete` key.
325
+ // -----------------------------------------------------------------------------
326
+
327
+ @route("/api/v1/annotations")
328
+ @tag("Annotations")
329
+ interface AnnotationsApi {
330
+ @doc("Attach a note of at most 2000 characters to a trace, span or session.")
331
+ @useAuth(QylWriteKeyAuth)
332
+ @post
333
+ create(
334
+ ...ProjectScopeHeader,
335
+ ...IdempotencyKeyHeader,
336
+ @body request: AnnotationCreateRequest,
337
+ ):
338
+ | {
339
+ @statusCode statusCode: 201;
340
+
341
+ @doc("URL of the created annotation.")
342
+ @header("Location")
343
+ location: string;
344
+
345
+ @header("ETag") etag: ETag;
346
+ @body body: Annotation;
347
+ }
348
+ | ValidationError
349
+ | UnauthorizedError
350
+ | ForbiddenError
351
+ | NotFoundError
352
+ | ConflictError
353
+ | InternalServerError;
354
+
355
+ @doc("List annotations, newest first, optionally only those attached to one trace, span or session.")
356
+ @useAuth(QylApiKeyHeaderAuth)
357
+ @get
358
+ list(
359
+ ...ProjectScopeHeader,
360
+
361
+ @doc("Annotations on this trace, including those on its spans.")
362
+ @query("trace_id")
363
+ traceId?: TraceId,
364
+
365
+ @doc("Annotations on this span; requires `trace_id`.")
366
+ @query("span_id")
367
+ spanId?: SpanId,
368
+
369
+ @query("session_id") sessionId?: SessionId,
370
+
371
+ @query
372
+ @minValue(1)
373
+ @maxValue(1000)
374
+ limit?: int32 = 100,
375
+
376
+ @query cursor?: Cursor,
377
+ ):
378
+ | CursorPage<Annotation>
379
+ | ValidationError
380
+ | UnauthorizedError
381
+ | InternalServerError;
382
+
383
+ @useAuth(QylApiKeyHeaderAuth)
384
+ @get
385
+ @route("/{annotation_id}")
386
+ get(...ProjectScopeHeader, @path("annotation_id") annotationId: ResourceId):
387
+ | {
388
+ @header("ETag") etag: ETag;
389
+ @body body: Annotation;
390
+ }
391
+ | NotFoundError
392
+ | UnauthorizedError
393
+ | InternalServerError;
394
+
395
+ @doc("Replace the text of an annotation. The target is fixed at creation.")
396
+ @useAuth(QylWriteKeyAuth)
397
+ @patch(#{ implicitOptionality: false })
398
+ @route("/{annotation_id}")
399
+ update(
400
+ ...ProjectScopeHeader,
401
+ ...IfMatchHeader,
402
+ @path("annotation_id") annotationId: ResourceId,
403
+ @body request: AnnotationUpdateRequest,
404
+ ):
405
+ | {
406
+ @header("ETag") etag: ETag;
407
+ @body body: Annotation;
408
+ }
409
+ | ValidationError
410
+ | UnauthorizedError
411
+ | ForbiddenError
412
+ | NotFoundError
413
+ | PreconditionFailedError
414
+ | PreconditionRequiredError
415
+ | InternalServerError;
416
+
417
+ @doc("Delete one annotation. Telemetry is never deleted.")
418
+ @useAuth(QylDeleteKeyAuth)
419
+ @delete
420
+ @route("/{annotation_id}")
421
+ delete(
422
+ ...ProjectScopeHeader,
423
+ ...IfMatchHeader,
424
+ @path("annotation_id") annotationId: ResourceId,
425
+ ):
426
+ | NoContentResponse
427
+ | UnauthorizedError
428
+ | ForbiddenError
429
+ | NotFoundError
430
+ | PreconditionFailedError
431
+ | PreconditionRequiredError
432
+ | InternalServerError;
433
+ }
434
+
435
+ // -----------------------------------------------------------------------------
436
+ // Saved queries
437
+ //
438
+ // Named, structured filters for traces, logs or metrics
439
+ // (models/saved-queries.tsp). Names are unique per project.
440
+ // -----------------------------------------------------------------------------
441
+
442
+ @route("/api/v1/saved-queries")
443
+ @tag("Saved queries")
444
+ interface SavedQueriesApi {
445
+ @doc("Save a named filter. A name already used in the project is refused with 409.")
446
+ @useAuth(QylWriteKeyAuth)
447
+ @post
448
+ create(
449
+ ...ProjectScopeHeader,
450
+ ...IdempotencyKeyHeader,
451
+ @body request: SavedQueryCreateRequest,
452
+ ):
453
+ | {
454
+ @statusCode statusCode: 201;
455
+
456
+ @doc("URL of the created saved query.")
457
+ @header("Location")
458
+ location: string;
459
+
460
+ @header("ETag") etag: ETag;
461
+ @body body: SavedQuery;
462
+ }
463
+ | ValidationError
464
+ | UnauthorizedError
465
+ | ForbiddenError
466
+ | ConflictError
467
+ | InternalServerError;
468
+
469
+ @doc("List the project's saved queries by name.")
470
+ @useAuth(QylApiKeyHeaderAuth)
471
+ @get
472
+ list(
473
+ ...ProjectScopeHeader,
474
+ @query signal?: SavedQuerySignal,
475
+
476
+ @query
477
+ @minValue(1)
478
+ @maxValue(1000)
479
+ limit?: int32 = 100,
480
+
481
+ @query cursor?: Cursor,
482
+ ):
483
+ | CursorPage<SavedQuery>
484
+ | ValidationError
485
+ | UnauthorizedError
486
+ | InternalServerError;
487
+
488
+ @useAuth(QylApiKeyHeaderAuth)
489
+ @get
490
+ @route("/{saved_query_id}")
491
+ get(...ProjectScopeHeader, @path("saved_query_id") savedQueryId: ResourceId):
492
+ | {
493
+ @header("ETag") etag: ETag;
494
+ @body body: SavedQuery;
495
+ }
496
+ | NotFoundError
497
+ | UnauthorizedError
498
+ | InternalServerError;
499
+
500
+ @doc("Change a saved query. Renaming onto a name already used in the project is refused with 409.")
501
+ @useAuth(QylWriteKeyAuth)
502
+ @patch(#{ implicitOptionality: false })
503
+ @route("/{saved_query_id}")
504
+ update(
505
+ ...ProjectScopeHeader,
506
+ ...IfMatchHeader,
507
+ @path("saved_query_id") savedQueryId: ResourceId,
508
+ @body request: SavedQueryUpdateRequest,
509
+ ):
510
+ | {
511
+ @header("ETag") etag: ETag;
512
+ @body body: SavedQuery;
513
+ }
514
+ | ValidationError
515
+ | UnauthorizedError
516
+ | ForbiddenError
517
+ | NotFoundError
518
+ | ConflictError
519
+ | PreconditionFailedError
520
+ | PreconditionRequiredError
521
+ | InternalServerError;
522
+
523
+ @doc("Delete one saved query.")
524
+ @useAuth(QylDeleteKeyAuth)
525
+ @delete
526
+ @route("/{saved_query_id}")
527
+ delete(
528
+ ...ProjectScopeHeader,
529
+ ...IfMatchHeader,
530
+ @path("saved_query_id") savedQueryId: ResourceId,
531
+ ):
532
+ | NoContentResponse
533
+ | UnauthorizedError
534
+ | ForbiddenError
535
+ | NotFoundError
536
+ | PreconditionFailedError
537
+ | PreconditionRequiredError
538
+ | InternalServerError;
539
+
540
+ @doc("Evaluate a saved query against current telemetry. Read-only.")
541
+ @useAuth(QylApiKeyHeaderAuth)
542
+ @post
543
+ @route("/{saved_query_id}:run")
544
+ run(
545
+ ...ProjectScopeHeader,
546
+ @path("saved_query_id") savedQueryId: ResourceId,
547
+ @body request: SavedQueryRunRequest,
548
+ ):
549
+ | SavedQueryRunResult
550
+ | ValidationError
551
+ | UnauthorizedError
552
+ | NotFoundError
553
+ | InternalServerError;
554
+ }
555
+
268
556
  // -----------------------------------------------------------------------------
269
557
  // Health
270
558
  // -----------------------------------------------------------------------------
package/api/runner.tsp CHANGED
@@ -22,6 +22,7 @@ namespace Qyl.Api.Contracts.Runner {
22
22
  message: RunnerLogLine,
23
23
  }
24
24
 
25
+ @doc("Served only by the local qyl runner on a loopback (127.0.0.1) listener. It rejects every request whose remote address or Host is not loopback, and checks the Origin header only on the mutating restart and stop operations, so these operations carry no API-key authentication.")
25
26
  @route("/runner/resources")
26
27
  @tag("Runner resources")
27
28
  interface RunnerResourcesApi {
@@ -81,3 +82,4 @@ namespace Qyl.Api.Contracts.Runner {
81
82
  | InternalServerError;
82
83
  }
83
84
  }
85
+
@@ -0,0 +1,20 @@
1
+ // =============================================================================
2
+ // Collector key security schemes per role
3
+ // =============================================================================
4
+ // Every collector key travels in `x-otlp-api-key`; its roles
5
+ // (Qyl.Api.Contracts.Common.CollectorKeyRole) decide what it may do. The query
6
+ // routes keep the original `ApiKeyAuth` scheme and require the `read` role;
7
+ // the schemes below name the roles the write routes require.
8
+ // =============================================================================
9
+
10
+ import "@typespec/http";
11
+
12
+ using TypeSpec.Http;
13
+
14
+ namespace TypeSpec.Http {
15
+ @doc("Collector API key in `x-otlp-api-key` carrying the `write` role (Qyl.Api.Contracts.Common.CollectorKeyRole). Creates and updates user resources.")
16
+ model QylWriteKeyAuth is ApiKeyAuth<ApiKeyLocation.header, "x-otlp-api-key">;
17
+
18
+ @doc("Collector API key in `x-otlp-api-key` carrying the `delete` role (Qyl.Api.Contracts.Common.CollectorKeyRole). Deletes user resources, never telemetry.")
19
+ model QylDeleteKeyAuth is ApiKeyAuth<ApiKeyLocation.header, "x-otlp-api-key">;
20
+ }
package/common/errors.tsp CHANGED
@@ -130,6 +130,28 @@ model ConflictError extends ProblemDetails {
130
130
  conflictingResource?: string;
131
131
  }
132
132
 
133
+ @doc("Precondition failed - the If-Match entity tag is no longer current (412)")
134
+ @error
135
+ model PreconditionFailedError extends ProblemDetails {
136
+ @statusCode _: 412;
137
+ title: "Precondition Failed";
138
+
139
+ @doc("Entity tag of the current version, when the caller may read it")
140
+ @encodedName("application/json", "current_etag")
141
+ currentEtag?: string;
142
+ }
143
+
144
+ @doc("Precondition required - the request must send If-Match (428)")
145
+ @error
146
+ model PreconditionRequiredError extends ProblemDetails {
147
+ @statusCode _: 428;
148
+ title: "Precondition Required";
149
+
150
+ @doc("The header the request must carry")
151
+ @encodedName("application/json", "required_header")
152
+ requiredHeader?: "If-Match";
153
+ }
154
+
133
155
  @doc("Rate limited - too many requests (429)")
134
156
  @error
135
157
  model RateLimitError extends ProblemDetails {
@@ -209,6 +231,12 @@ union ApiError {
209
231
  @doc("Resource conflict")
210
232
  conflict: ConflictError,
211
233
 
234
+ @doc("Entity tag no longer current")
235
+ preconditionFailed: PreconditionFailedError,
236
+
237
+ @doc("If-Match missing")
238
+ preconditionRequired: PreconditionRequiredError,
239
+
212
240
  @doc("Rate limit exceeded")
213
241
  rateLimit: RateLimitError,
214
242
 
@@ -0,0 +1,106 @@
1
+ // =============================================================================
2
+ // Shared write base for user-created collector resources
3
+ // =============================================================================
4
+ // Every resource a client creates through the collector API (annotations, saved
5
+ // queries) extends `ResourceBase`, and every write operation uses the headers
6
+ // below. Server-assigned members carry `Lifecycle.Read` visibility, so the
7
+ // create and update request models never accept them.
8
+ //
9
+ // Concurrency: a read returns `etag`; PATCH and DELETE send it back in
10
+ // `If-Match`. A missing `If-Match` is refused with 428, a stale one with 412.
11
+ // Retries: a POST that creates a resource takes `Idempotency-Key`; a repeat
12
+ // with the same key and body returns the first result instead of a duplicate.
13
+ // =============================================================================
14
+
15
+ import "@typespec/http";
16
+ import "./types.tsp";
17
+
18
+ using TypeSpec.Http;
19
+
20
+ namespace Qyl.Api.Contracts.Common;
21
+
22
+ @doc("Opaque, server-assigned identifier of a user-created collector resource.")
23
+ @minLength(1)
24
+ @maxLength(128)
25
+ scalar ResourceId extends string;
26
+
27
+ @doc("""
28
+ Opaque entity tag of one resource version. It changes on every successful
29
+ update; send it back in `If-Match` to update or delete that version.
30
+ """)
31
+ @minLength(1)
32
+ @maxLength(128)
33
+ scalar ETag extends string;
34
+
35
+ @doc("""
36
+ Opaque pagination cursor. Pass the `next_cursor` of one page to receive the
37
+ next page of the same query. Its content is not part of the contract and
38
+ must not be parsed or built by a client.
39
+ """)
40
+ scalar Cursor extends string;
41
+
42
+ @doc("""
43
+ Collector API key roles. Every key sent in `x-otlp-api-key` carries one or
44
+ more roles: `ingest` accepts OTLP, `read` serves the query routes, `write`
45
+ creates and updates user resources, and `delete` deletes them. Roles are
46
+ independent, so a leaked ingest key can neither read, change nor delete
47
+ anything.
48
+ """)
49
+ enum CollectorKeyRole {
50
+ @doc("Send OTLP traces, logs and metrics.")
51
+ ingest: "ingest",
52
+
53
+ @doc("Read telemetry and user resources.")
54
+ read: "read",
55
+
56
+ @doc("Create and update annotations and saved queries.")
57
+ write: "write",
58
+
59
+ @doc("Delete annotations and saved queries. Never deletes telemetry; retention does.")
60
+ delete: "delete",
61
+ }
62
+
63
+ @doc("Base of every user-created collector resource.")
64
+ model ResourceBase {
65
+ @doc("Server-assigned identifier.")
66
+ @visibility(Lifecycle.Read)
67
+ id: ResourceId;
68
+
69
+ @doc("When the resource was created.")
70
+ @visibility(Lifecycle.Read)
71
+ @encodedName("application/json", "created_at")
72
+ createdAt: utcDateTime;
73
+
74
+ @doc("When the resource was last changed.")
75
+ @visibility(Lifecycle.Read)
76
+ @encodedName("application/json", "updated_at")
77
+ updatedAt: utcDateTime;
78
+
79
+ @doc("Entity tag of this version; also served as the `ETag` response header.")
80
+ @visibility(Lifecycle.Read)
81
+ etag: ETag;
82
+ }
83
+
84
+ @doc("Retry key for a creating POST.")
85
+ model IdempotencyKeyHeader {
86
+ @doc("""
87
+ Client-chosen retry key, unique per intended create. A repeat with the same
88
+ key and body returns the first result; the same key with a different body
89
+ is refused with 409. Keys expire after 24 hours.
90
+ """)
91
+ @header("Idempotency-Key")
92
+ @minLength(1)
93
+ @maxLength(255)
94
+ idempotencyKey?: string;
95
+ }
96
+
97
+ @doc("Optimistic-concurrency precondition for PATCH and DELETE.")
98
+ model IfMatchHeader {
99
+ @doc("""
100
+ The `etag` of the version being changed. A request without it is refused
101
+ with 428 Precondition Required; a request whose tag is no longer current is
102
+ refused with 412 Precondition Failed.
103
+ """)
104
+ @header("If-Match")
105
+ ifMatch: ETag;
106
+ }