@ancplua/qyl-api-schema 7.3.0 → 8.0.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
@@ -59,6 +59,28 @@ rather than from prose here; this line moves faster than a README is revised.
59
59
  `main.tsp` is the local compile entry point and includes emitter routing. `index.tsp`
60
60
  is the published TypeSpec entry point and contains only the client-facing contract.
61
61
 
62
+ ## Runtime validation
63
+
64
+ `@ancplua/qyl-api-schema/zod` builds Zod validators from the bundled JSON Schema at
65
+ runtime. It is a translation layer over `z.fromJSONSchema` — authored in `src/zod/`
66
+ and compiled to `generated/zod-runtime/` — not a second hand-written schema, so a
67
+ contract change cannot leave a validator describing the previous shape.
68
+
69
+ ```ts
70
+ import { publishedContractSchema } from "@ancplua/qyl-api-schema/zod";
71
+ import type { Span } from "@ancplua/qyl-api-schema/types";
72
+
73
+ const SpanSchema = publishedContractSchema<Span>("OTel.Traces.Span");
74
+ const span = SpanSchema.parse(await response.json());
75
+ ```
76
+
77
+ `zod` is an optional peer dependency (`>=4.5.0 <5`): a consumer that does not
78
+ install it simply never imports this subpath, and the other subpaths do not depend
79
+ on it. Definition names are the published `$defs` keys — the same names the OpenAPI
80
+ components and the generated C# and TypeScript DTOs carry — and
81
+ `contractDefinitionNames()` lists all of them. The type argument is not checked
82
+ against the schema, so pair it with the matching type from `./types`.
83
+
62
84
  ## Contract revision
63
85
 
64
86
  `scripts/emit-contract-revision.mjs` stamps a deterministic revision — `sha256:` plus
package/api/routes.tsp CHANGED
@@ -8,6 +8,7 @@ import "../otel/span.tsp";
8
8
  import "../otel/logs.tsp";
9
9
  import "../models/session.tsp";
10
10
  import "../models/health.tsp";
11
+ import "../models/metrics.tsp";
11
12
 
12
13
  using TypeSpec.Http;
13
14
  using TypeSpec.OpenAPI;
@@ -17,6 +18,7 @@ using Qyl.Api.Contracts.Common.Errors;
17
18
  using Qyl.Api.Contracts.Common.Pagination;
18
19
  using Qyl.Api.Contracts.OTel.Traces;
19
20
  using Qyl.Api.Contracts.OTel.Logs;
21
+ using Qyl.Api.Contracts.OTel.Metrics;
20
22
  using Qyl.Api.Contracts.OTel.Enums;
21
23
  using Qyl.Api.Contracts.Domains.Observe.Session;
22
24
  using Qyl.Api.Contracts.Health;
@@ -104,6 +106,113 @@ interface LogsApi {
104
106
  | InternalServerError;
105
107
  }
106
108
 
109
+ // -----------------------------------------------------------------------------
110
+ // Metrics
111
+ //
112
+ // Modelled on the Grafana/Prometheus range query — metric, label matchers,
113
+ // range, step — because the primary consumer is a coding agent over MCP. The
114
+ // collector aggregates server-side and returns buckets; raw points are never
115
+ // served, so a payload stays small no matter how dense the underlying stream.
116
+ // -----------------------------------------------------------------------------
117
+
118
+ @route("/api/v1/metrics")
119
+ @tag("Metrics")
120
+ @useAuth(QylApiKeyHeaderAuth)
121
+ interface MetricsApi {
122
+ @doc("List the metric names recorded for a project.")
123
+ @get
124
+ list(
125
+ ...ProjectScopeHeader,
126
+
127
+ @doc("Return only names starting with this prefix.")
128
+ @query("name_prefix")
129
+ namePrefix?: string,
130
+
131
+ @query
132
+ @minValue(1)
133
+ @maxValue(1000)
134
+ limit?: int32 = 200,
135
+ ):
136
+ | CursorPage<MetricDescriptor>
137
+ | ValidationError
138
+ | UnauthorizedError
139
+ | InternalServerError;
140
+
141
+ @doc("List the distinct attribute streams recorded under one metric name.")
142
+ @get
143
+ @route("/{metric_name}/series")
144
+ listSeries(
145
+ ...ProjectScopeHeader,
146
+ @path("metric_name") metricName: string,
147
+
148
+ @doc("""
149
+ Exact attribute match, repeatable, as `key=value`. A series must match
150
+ every matcher given.
151
+ """)
152
+ @query(#{ explode: true })
153
+ attr?: string[],
154
+
155
+ @doc("Attribute prefix match, repeatable, as `key=prefix`.")
156
+ @query(#{ name: "attr_prefix", explode: true })
157
+ attrPrefix?: string[],
158
+
159
+ @query
160
+ @minValue(1)
161
+ @maxValue(1000)
162
+ limit?: int32 = 200,
163
+ ):
164
+ | CursorPage<MetricSeries>
165
+ | ValidationError
166
+ | NotFoundError
167
+ | UnauthorizedError
168
+ | InternalServerError;
169
+
170
+ @doc("""
171
+ Aggregate one metric over a time range into buckets of `step_ms`.
172
+ Percentile aggregations require a histogram kind.
173
+ """)
174
+ @get
175
+ @route("/{metric_name}/query")
176
+ query(
177
+ ...ProjectScopeHeader,
178
+ @path("metric_name") metricName: string,
179
+
180
+ @query("start_time") startTime: utcDateTime,
181
+ @query("end_time") endTime: utcDateTime,
182
+
183
+ @doc("Bucket width in milliseconds. One bucket spanning the range collapses the query to a single value.")
184
+ @query("step_ms")
185
+ @minValue(1)
186
+ @maxValue(86400000)
187
+ stepMs?: int64 = 60000,
188
+
189
+ @query aggregation?: MetricAggregation = MetricAggregation.avg,
190
+
191
+ @doc("Attribute keys to split the result by, repeatable. Omitted, every matching series collapses into one stream.")
192
+ @query(#{ name: "group_by", explode: true })
193
+ groupBy?: string[],
194
+
195
+ @doc("Exact attribute match, repeatable, as `key=value`.")
196
+ @query(#{ explode: true })
197
+ attr?: string[],
198
+
199
+ @doc("Attribute prefix match, repeatable, as `key=prefix`.")
200
+ @query(#{ name: "attr_prefix", explode: true })
201
+ attrPrefix?: string[],
202
+
203
+ @doc("Maximum result streams. A grouping that exceeds this is truncated, not refused.")
204
+ @query("series_limit")
205
+ @minValue(1)
206
+ @maxValue(500)
207
+ seriesLimit?: int32 = 50,
208
+ ):
209
+ | MetricQueryResult
210
+ | ValidationError
211
+ | NotFoundError
212
+ | UnauthorizedError
213
+ | InternalServerError;
214
+ }
215
+
107
216
  // -----------------------------------------------------------------------------
108
217
  // Sessions
109
218
  // -----------------------------------------------------------------------------
@@ -3289,6 +3289,256 @@
3289
3289
  },
3290
3290
  "description": "Log message template with placeholders"
3291
3291
  },
3292
+ "OTel.Metrics.MetricAggregation": {
3293
+ "type": "string",
3294
+ "enum": [
3295
+ "avg",
3296
+ "min",
3297
+ "max",
3298
+ "sum",
3299
+ "count",
3300
+ "last",
3301
+ "p50",
3302
+ "p90",
3303
+ "p95",
3304
+ "p99"
3305
+ ],
3306
+ "description": "Server-side reducer applied to the points falling in one time bucket."
3307
+ },
3308
+ "OTel.Metrics.MetricBucket": {
3309
+ "type": "object",
3310
+ "required": [
3311
+ "bucket_start",
3312
+ "value",
3313
+ "point_count"
3314
+ ],
3315
+ "properties": {
3316
+ "bucket_start": {
3317
+ "type": "string",
3318
+ "format": "date-time",
3319
+ "description": "Inclusive start of the bucket, aligned to the query's step from the query's start."
3320
+ },
3321
+ "value": {
3322
+ "anyOf": [
3323
+ {
3324
+ "type": "number",
3325
+ "format": "double"
3326
+ },
3327
+ {
3328
+ "type": "null"
3329
+ }
3330
+ ],
3331
+ "description": "The aggregation applied to the points in this bucket. Null when the bucket\nholds no value: either no point landed in it, or every point that did was\nan OTLP `NO_RECORDED_VALUE` marker."
3332
+ },
3333
+ "point_count": {
3334
+ "type": "integer",
3335
+ "format": "int64",
3336
+ "description": "Number of stored points folded into this bucket."
3337
+ }
3338
+ },
3339
+ "unevaluatedProperties": {
3340
+ "not": {}
3341
+ },
3342
+ "description": "One aggregated time bucket of a metric range query."
3343
+ },
3344
+ "OTel.Metrics.MetricDescriptor": {
3345
+ "type": "object",
3346
+ "required": [
3347
+ "name",
3348
+ "kind",
3349
+ "temporality",
3350
+ "monotonic",
3351
+ "series_count",
3352
+ "last_seen"
3353
+ ],
3354
+ "properties": {
3355
+ "name": {
3356
+ "type": "string",
3357
+ "description": "Instrument name, e.g. `http.server.request.duration`."
3358
+ },
3359
+ "kind": {
3360
+ "$ref": "#/$defs/OTel.Metrics.MetricKind"
3361
+ },
3362
+ "temporality": {
3363
+ "$ref": "#/$defs/OTel.Metrics.MetricTemporality"
3364
+ },
3365
+ "monotonic": {
3366
+ "type": "boolean",
3367
+ "description": "Whether a sum only ever increases. Meaningless for other kinds."
3368
+ },
3369
+ "unit": {
3370
+ "type": "string",
3371
+ "description": "UCUM unit as reported by the producer, e.g. `s` or `{request}`."
3372
+ },
3373
+ "description": {
3374
+ "type": "string",
3375
+ "description": "Producer-supplied description of the instrument."
3376
+ },
3377
+ "series_count": {
3378
+ "type": "integer",
3379
+ "format": "int64",
3380
+ "description": "Number of distinct attribute streams recorded under this name."
3381
+ },
3382
+ "last_seen": {
3383
+ "type": "string",
3384
+ "format": "date-time",
3385
+ "description": "Timestamp of the most recent point recorded under this name."
3386
+ }
3387
+ },
3388
+ "unevaluatedProperties": {
3389
+ "not": {}
3390
+ },
3391
+ "description": "One metric name recorded for a project, collapsed across all of its series.\nThis is the catalog entry a client reads before it knows which series exist."
3392
+ },
3393
+ "OTel.Metrics.MetricKind": {
3394
+ "type": "string",
3395
+ "enum": [
3396
+ "gauge",
3397
+ "sum",
3398
+ "histogram",
3399
+ "exponential_histogram"
3400
+ ],
3401
+ "description": "Shape of a stored metric stream."
3402
+ },
3403
+ "OTel.Metrics.MetricQueryResult": {
3404
+ "type": "object",
3405
+ "required": [
3406
+ "name",
3407
+ "kind",
3408
+ "aggregation",
3409
+ "step_ms",
3410
+ "start_time",
3411
+ "end_time",
3412
+ "series",
3413
+ "truncated"
3414
+ ],
3415
+ "properties": {
3416
+ "name": {
3417
+ "type": "string",
3418
+ "description": "Instrument name the query ran against."
3419
+ },
3420
+ "kind": {
3421
+ "$ref": "#/$defs/OTel.Metrics.MetricKind"
3422
+ },
3423
+ "unit": {
3424
+ "type": "string"
3425
+ },
3426
+ "aggregation": {
3427
+ "$ref": "#/$defs/OTel.Metrics.MetricAggregation"
3428
+ },
3429
+ "step_ms": {
3430
+ "type": "integer",
3431
+ "format": "int64",
3432
+ "description": "Bucket width actually used, which may be widened from the requested step."
3433
+ },
3434
+ "start_time": {
3435
+ "type": "string",
3436
+ "format": "date-time"
3437
+ },
3438
+ "end_time": {
3439
+ "type": "string",
3440
+ "format": "date-time"
3441
+ },
3442
+ "series": {
3443
+ "type": "array",
3444
+ "items": {
3445
+ "$ref": "#/$defs/OTel.Metrics.MetricSeriesResult"
3446
+ }
3447
+ },
3448
+ "truncated": {
3449
+ "type": "boolean",
3450
+ "description": "Whether streams were dropped because the result exceeded the series limit."
3451
+ }
3452
+ },
3453
+ "unevaluatedProperties": {
3454
+ "not": {}
3455
+ },
3456
+ "description": "The answer to a metric range query: what was asked, and the streams it produced."
3457
+ },
3458
+ "OTel.Metrics.MetricSeries": {
3459
+ "type": "object",
3460
+ "required": [
3461
+ "series_id",
3462
+ "name",
3463
+ "kind",
3464
+ "attributes",
3465
+ "first_seen",
3466
+ "last_seen"
3467
+ ],
3468
+ "properties": {
3469
+ "series_id": {
3470
+ "type": "string",
3471
+ "description": "Stable identifier for this stream, derived from its name, attributes and resource."
3472
+ },
3473
+ "name": {
3474
+ "type": "string",
3475
+ "description": "Instrument name this series belongs to."
3476
+ },
3477
+ "kind": {
3478
+ "$ref": "#/$defs/OTel.Metrics.MetricKind"
3479
+ },
3480
+ "attributes": {
3481
+ "type": "array",
3482
+ "items": {
3483
+ "$ref": "#/$defs/Common.Attribute"
3484
+ },
3485
+ "description": "The attributes that distinguish this series from others under the same name."
3486
+ },
3487
+ "service_name": {
3488
+ "type": "string"
3489
+ },
3490
+ "unit": {
3491
+ "type": "string"
3492
+ },
3493
+ "first_seen": {
3494
+ "type": "string",
3495
+ "format": "date-time"
3496
+ },
3497
+ "last_seen": {
3498
+ "type": "string",
3499
+ "format": "date-time"
3500
+ }
3501
+ },
3502
+ "unevaluatedProperties": {
3503
+ "not": {}
3504
+ },
3505
+ "description": "One distinct attribute stream recorded under a metric name."
3506
+ },
3507
+ "OTel.Metrics.MetricSeriesResult": {
3508
+ "type": "object",
3509
+ "required": [
3510
+ "attributes",
3511
+ "buckets"
3512
+ ],
3513
+ "properties": {
3514
+ "attributes": {
3515
+ "type": "array",
3516
+ "items": {
3517
+ "$ref": "#/$defs/Common.Attribute"
3518
+ },
3519
+ "description": "The grouping key that produced this stream."
3520
+ },
3521
+ "buckets": {
3522
+ "type": "array",
3523
+ "items": {
3524
+ "$ref": "#/$defs/OTel.Metrics.MetricBucket"
3525
+ }
3526
+ }
3527
+ },
3528
+ "unevaluatedProperties": {
3529
+ "not": {}
3530
+ },
3531
+ "description": "One result stream of a metric range query. With no `group_by` the whole\nresult is a single stream whose `attributes` are empty; with `group_by` there\nis one stream per distinct combination of the requested attribute values."
3532
+ },
3533
+ "OTel.Metrics.MetricTemporality": {
3534
+ "type": "string",
3535
+ "enum": [
3536
+ "unspecified",
3537
+ "delta",
3538
+ "cumulative"
3539
+ ],
3540
+ "description": "How successive points of a sum or histogram relate to each other."
3541
+ },
3292
3542
  "OTel.Resource.CloudProvider": {
3293
3543
  "type": "string",
3294
3544
  "enum": [
@@ -8071,6 +8321,106 @@
8071
8321
  "Operations.LogsApi_list.Response.500": {
8072
8322
  "$ref": "#/$defs/Common.Errors.InternalServerError"
8073
8323
  },
8324
+ "Operations.MetricsApi_list.Response.200": {
8325
+ "type": "object",
8326
+ "required": [
8327
+ "items",
8328
+ "has_more"
8329
+ ],
8330
+ "properties": {
8331
+ "items": {
8332
+ "type": "array",
8333
+ "items": {
8334
+ "$ref": "#/$defs/OTel.Metrics.MetricDescriptor"
8335
+ },
8336
+ "description": "List of items in this page"
8337
+ },
8338
+ "next_cursor": {
8339
+ "type": "string",
8340
+ "description": "Cursor for the next page (null if no more pages)"
8341
+ },
8342
+ "prev_cursor": {
8343
+ "type": "string",
8344
+ "description": "Cursor for the previous page (null if first page)"
8345
+ },
8346
+ "has_more": {
8347
+ "type": "boolean",
8348
+ "description": "Whether there are more items available"
8349
+ }
8350
+ },
8351
+ "unevaluatedProperties": {
8352
+ "not": {}
8353
+ },
8354
+ "description": "Cursor-based paginated response wrapper"
8355
+ },
8356
+ "Operations.MetricsApi_list.Response.400": {
8357
+ "$ref": "#/$defs/Common.Errors.ValidationError"
8358
+ },
8359
+ "Operations.MetricsApi_list.Response.401": {
8360
+ "$ref": "#/$defs/Common.Errors.UnauthorizedError"
8361
+ },
8362
+ "Operations.MetricsApi_list.Response.500": {
8363
+ "$ref": "#/$defs/Common.Errors.InternalServerError"
8364
+ },
8365
+ "Operations.MetricsApi_listSeries.Response.200": {
8366
+ "type": "object",
8367
+ "required": [
8368
+ "items",
8369
+ "has_more"
8370
+ ],
8371
+ "properties": {
8372
+ "items": {
8373
+ "type": "array",
8374
+ "items": {
8375
+ "$ref": "#/$defs/OTel.Metrics.MetricSeries"
8376
+ },
8377
+ "description": "List of items in this page"
8378
+ },
8379
+ "next_cursor": {
8380
+ "type": "string",
8381
+ "description": "Cursor for the next page (null if no more pages)"
8382
+ },
8383
+ "prev_cursor": {
8384
+ "type": "string",
8385
+ "description": "Cursor for the previous page (null if first page)"
8386
+ },
8387
+ "has_more": {
8388
+ "type": "boolean",
8389
+ "description": "Whether there are more items available"
8390
+ }
8391
+ },
8392
+ "unevaluatedProperties": {
8393
+ "not": {}
8394
+ },
8395
+ "description": "Cursor-based paginated response wrapper"
8396
+ },
8397
+ "Operations.MetricsApi_listSeries.Response.400": {
8398
+ "$ref": "#/$defs/Common.Errors.ValidationError"
8399
+ },
8400
+ "Operations.MetricsApi_listSeries.Response.401": {
8401
+ "$ref": "#/$defs/Common.Errors.UnauthorizedError"
8402
+ },
8403
+ "Operations.MetricsApi_listSeries.Response.404": {
8404
+ "$ref": "#/$defs/Common.Errors.NotFoundError"
8405
+ },
8406
+ "Operations.MetricsApi_listSeries.Response.500": {
8407
+ "$ref": "#/$defs/Common.Errors.InternalServerError"
8408
+ },
8409
+ "Operations.MetricsApi_query.Response.200": {
8410
+ "$ref": "#/$defs/OTel.Metrics.MetricQueryResult"
8411
+ },
8412
+ "Operations.MetricsApi_query.Response.400": {
8413
+ "$ref": "#/$defs/Common.Errors.ValidationError"
8414
+ },
8415
+ "Operations.MetricsApi_query.Response.401": {
8416
+ "$ref": "#/$defs/Common.Errors.UnauthorizedError"
8417
+ },
8418
+ "Operations.MetricsApi_query.Response.404": {
8419
+ "$ref": "#/$defs/Common.Errors.NotFoundError"
8420
+ },
8421
+ "Operations.MetricsApi_query.Response.500": {
8422
+ "$ref": "#/$defs/Common.Errors.InternalServerError"
8423
+ },
8074
8424
  "Operations.RunnerResourcesApi_logs.Response.200": {
8075
8425
  "type": "array",
8076
8426
  "items": {