Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 166 additions & 0 deletions exp/api/openapi/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
# OpenAPI-generated Prometheus API Client (PoC)

Experimental OpenAPI-based HTTP API client for Prometheus, generated by
[oapi-codegen](https://github.com/oapi-codegen/oapi-codegen) from a
hand-crafted OpenAPI 3.0.3 specification.

This is a proof-of-concept for
[client_golang#1998](https://github.com/prometheus/client_golang/issues/1998).

## What's here

| File | Purpose |
|---|---|
| `spec.yaml` | OpenAPI 3.0.3 specification covering all 22 Prometheus v1 API endpoints |
| `oapi-codegen.yaml` | Code generation config |
| `openapi.gen.go` | Generated code (5,217 lines) — **do not edit** |
| `doc.go` | Package documentation |
| `client.go` | High-level wrapper with `model.Value` support |
| `client_test.go` | Tests and benchmarks |

## How to regenerate

```bash
go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@latest
cd exp/api/openapi
oapi-codegen --config oapi-codegen.yaml spec.yaml
```

## Usage

```go
import (
"github.com/prometheus/client_golang/api"
openapi "github.com/prometheus/client_golang/exp/api/openapi"
)

apiClient, _ := api.NewClient(api.Config{Address: "http://localhost:9090"})
client := openapi.NewAPIClient(apiClient)

// Instant query
result, _ := client.InstantQuery(ctx, "up", 0, openapi.QueryOptions{})

// Range query
result, _ := client.RangeQuery(ctx, "rate(up[5m])", openapi.QueryRange{
Start: startTime, End: endTime, Step: "15s",
}, openapi.QueryOptions{})

// Label names
names, _, _, _ := client.LabelNames(ctx, nil, 0, 0)
```

## Key findings

### 1. oapi-codegen is viable for OpenAPI 3.0.3

The tool generates idiomatic, compilable Go code. Generates **5,217 lines**
from a ~700 line YAML spec covering 23 endpoints. The generated code:
- Uses `encoding/json` (not json-iterator)
- Supports GET + POST methods for query endpoints
- Provides typed parameter structs (`GetInstantQueryParams`, etc.)
- Provides `ClientWithResponses` with per-endpoint response wrappers

### 2. Type mapping gaps

The biggest gap: **`QueryData.Result` cannot be typed as `model.Value`**
in the OpenAPI spec. It must be declared as a generic `{}` (which generates
`interface{}`) because the shape depends on `resultType`:

| `resultType` | JSON shape of `result` |
|---|---|
| `scalar` | `[timestamp_number, "value_string"]` — array |
| `vector` | `[{metric: {...}, value: [t, v]}]` — array of objects |
| `matrix` | `[{metric: {...}, values: [[t,v], ...]}]` — array of objects |

Our wrapper (`client.go`) works around this via `dataToModelValue()`,
which round-trips through `json.Marshal` → `json.Unmarshal` against
`model.Scalar`/`model.Vector`/`model.Matrix`.

**Other untyped fields:**
- `RuleGroup.Rules` → `[]map[string]interface{}` (lost alerting/recording discriminator)
- `ActiveTarget.Labels` → `map[string]interface{}` (not `model.LabelSet`)
- `Alert.Labels/Annotations` → `map[string]interface{}` (not `model.LabelSet`)

### 3. Performance comparison

Benchmarks on Apple M1:

| Scenario | This PoC (round-trip) |
|---|---|
| Vector: 100 series | ~89µs, 30KB, 819 allocs |
| Matrix: 10×1000 datapoints | ~2.4ms, 234KB, 101 allocs |
| Histogram: 10×100 datapoints | ~1.2ms, 327KB, 7,101 allocs |

The PoC path involves `json.Unmarshal(body, &QueryResponse)` followed by
`json.Marshal(data.Result)` → `json.Unmarshal(bytes, &model.Vector)`
(double encoding). The existing client reads directly into `model` types
using json-iterator's `unsafe`-based decoders — no intermediate allocations.

**Mitigation:** Custom oapi-codegen templates could inject json-iterator
and custom decoders for the hot-path types (SamplePair, SampleHistogramPair).

### 4. Endpoint coverage

The spec covers all 22 endpoints from the existing `API` interface:

`/query`, `/query_range`, `/query_exemplars`, `/format_query`,
`/labels`, `/label/{name}/values`, `/series`, `/targets`,
`/targets/metadata`, `/metadata`, `/rules`, `/alerts`, `/alertmanagers`,
`/status/config`, `/status/flags`, `/status/buildinfo`, `/status/runtimeinfo`,
`/status/tsdb`, `/status/tsdb/blocks`, `/status/walreplay`,
`/admin/tsdb/snapshot`, `/admin/tsdb/delete_series`, `/admin/tsdb/clean_tombstones`

### 5. Auth and HTTP client support

- Custom HTTP headers (bearer tokens): ✅ via `RequestEditorFn`
- Custom `http.Client` / `RoundTripper`: ✅ via `WithHTTPClient`
- Transport-level integration with `api.Client`: ✅ via our wrapper

### 6. Real OpenAPI 3.1 spec status

The real Prometheus OpenAPI 3.1 spec (saved as `spec_real_31.yaml`, 5,510 lines
from `openapi_3.1_golden.yaml`) generates a 12,739-line client with oapi-codegen
v2.8.0. However, it produces two categories of issues that need resolution before
this client can replace the hand-written one:

**Parse function collisions:** `/query` GET and POST both return `QueryOutputBody`,
generating duplicate `ParseQueryResponse` functions. Same for `ParseQueryPostResponse`.
Fix: add unique response schema wrappers per operation in the spec, e.g.:

```yaml
QueryOutputBody_GET: # distinct from QueryOutputBody_POST
allOf:
- $ref: "#/components/schemas/QueryOutputBody"
```

**Type name mismatches:** Our wrapper (`client.go`) expects `GetInstantQueryParams`,
`GetInstantQueryWithResponse`, etc. — names derived from our hand-crafted spec's
`operationId: getInstantQuery`. The real spec uses `operationId: query`, generating
`QueryParams`, `QueryWithResponse`, etc. Fix: update the wrapper to match real
operation IDs, or add `x-oapi-codegen-extra-tags` to the spec.

**Why we ship a hand-crafted spec for the PoC:** The hand-crafted spec produces
a fully compilable client today. The real spec requires pre-processing to resolve
the above issues — this is implementable, just not prioritized for the initial
investigation since the PoC's goal is to verify oapi-codegen viability, not to
ship a production client.

## Conclusion

oapi-codegen **is viable** for generating a Prometheus HTTP API client.
The generated code is idiomatic and compilable. The main challenges are:

1. **Type fidelity** — the `result` field's dynamic type cannot be expressed
in OpenAPI and must be handled via wrapper code.
2. **Performance** — standard `encoding/json` decoding + wrapper round-trip
is slower than the current json-iterator+unsafe path, but the gap can
be narrowed with template customization. Histogram decoding is especially
allocation-heavy (7,101 allocs for 10×100 datapoints).
3. **Spec fidelity** — the real Prometheus OpenAPI 3.1 spec (5,510 lines from
`openapi_3.1_golden.yaml`) generates 12,739 lines but has name collisions
from shared response types. This is resolvable with oapi-codegen config
and brings the spec 5× larger than our hand-crafted version.

**Recommended next step:** Resolve the operation ID collisions in the real
3.1 spec, add `x-go-name` extensions for cleaner type naming, and inject
json-iterator into the oapi-codegen template for hot-path types.
Loading
Loading