Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
export {
createScalarInstruments,
type RecordUiRequestAttributes,
recordUiRequest,
type ScalarInstruments,
type ScalarInstrumentsConfig,
UI_LABEL_VALUE,
} from "./instruments.js";

View File

@@ -0,0 +1,69 @@
# Scalar UI OTel instruments
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The instruments module wires the Scalar UI handler's OTel instruments
into a frozen bundle constructed once at handler-mount time. The bundle
defaults to the OTel global meter provider, which itself falls back to
a no-op implementation when no SDK is installed, so the package works
in test environments without ceremony. The bundle is intentionally
small — UI shells stay passive and the bulk of observability is
delegated to the surrounding router request metrics.
## Features
- `createScalarInstruments(config?)` — builds the meter + counter.
- `recordUiRequest(instruments, { plugin? })` — adds a single
observation. Always carries `{ ui: "scalar" }`; the per-plugin handler
fans out a second observation labelled with `{ plugin }`.
- `UI_LABEL_VALUE` — exported so siblings (`api-openapi-ui-swagger`,
`api-openapi-ui-redoc`) share the naming convention.
## Usage: Basic
```ts
import {
createScalarInstruments,
recordUiRequest,
} from "@suaveplan/api-openapi-ui-scalar/instruments";
const instruments = createScalarInstruments();
recordUiRequest(instruments);
```
## Usage: Advanced
```ts
import { metrics, type MeterProvider } from "@opentelemetry/api";
import { createScalarInstruments } from "@suaveplan/api-openapi-ui-scalar/instruments";
const meterProvider: MeterProvider = metrics.getMeterProvider();
const instruments = createScalarInstruments({ meterProvider });
// Per-plugin observation — labelled with both `ui` and `plugin` for
// dashboard fan-out across multiple Scalar mounts.
instruments.requestsTotal.add(1, { ui: "scalar", plugin: "billing" });
```
## API Reference
- `ScalarInstrumentsConfig` — `{ meterProvider? }`.
- `ScalarInstruments` — `{ meter, requestsTotal }` (frozen).
- `createScalarInstruments(config?)` — returns frozen `ScalarInstruments`.
- `recordUiRequest(instruments, attrs?)` — labels each observation with
`{ ui: "scalar" }` plus the optional `plugin` attribute.
- `UI_LABEL_VALUE` — the string literal `"scalar"`.
## Implementation Notes
The counter name `openapi.ui.requests_total` is shared across every
sibling Scalar / Swagger / Redoc UI handler so dashboards can sum across
operator choices via the `{ ui }` attribute. Per-plugin shells emit a
second observation rather than overriding the bare-UI one so the
top-line total stays correct. The handler does NOT increment on asset
sub-requests — router request metrics cover those, and double-counting
the assets would inflate the per-request total.

View File

@@ -0,0 +1,74 @@
import type { Counter, Meter, MeterProvider } from "@opentelemetry/api";
import { describe, expect, it } from "@suaveplan/testing/runner";
import {
createScalarInstruments,
recordUiRequest,
UI_LABEL_VALUE,
} from "./instruments.js";
interface RecordedObservation {
readonly value: number;
readonly attributes: Record<string, unknown>;
}
function buildRecordingProvider(): {
readonly provider: MeterProvider;
readonly observations: RecordedObservation[];
} {
const observations: RecordedObservation[] = [];
const counter = {
add(value: number, attributes?: Record<string, unknown>): void {
observations.push({
value,
attributes: { ...(attributes ?? {}) },
});
},
} as unknown as Counter;
const meter = {
createCounter(): Counter {
return counter;
},
} as unknown as Meter;
const provider = {
getMeter(): Meter {
return meter;
},
} as unknown as MeterProvider;
return { provider, observations };
}
describe("instruments", () => {
it("createScalarInstruments wires a counter on the supplied provider", {
timeout: 5000,
}, () => {
const { provider, observations } = buildRecordingProvider();
const instruments = createScalarInstruments({
meterProvider: provider,
});
recordUiRequest(instruments);
expect(observations.length).toBe(1);
expect(observations[0]?.attributes).toEqual({ ui: UI_LABEL_VALUE });
});
it("recordUiRequest labels per-plugin observations", {
timeout: 5000,
}, () => {
const { provider, observations } = buildRecordingProvider();
const instruments = createScalarInstruments({
meterProvider: provider,
});
recordUiRequest(instruments, { plugin: "billing" });
expect(observations[0]?.attributes).toEqual({
ui: UI_LABEL_VALUE,
plugin: "billing",
});
});
it("falls back to the OTel global meter provider", {
timeout: 5000,
}, () => {
const instruments = createScalarInstruments();
expect(instruments.requestsTotal).toBeDefined();
expect(instruments.meter).toBeDefined();
});
});

View File

@@ -0,0 +1,62 @@
import type { Counter, Meter, MeterProvider } from "@opentelemetry/api";
import { metrics } from "@opentelemetry/api";
/**
* Config consumed by {@link createScalarInstruments}. The provider
* defaults to the OTel global, which falls back to a no-op
* implementation when no SDK is installed — so the handler works in
* test envs without ceremony.
*/
export interface ScalarInstrumentsConfig {
readonly meterProvider?: MeterProvider;
}
/**
* Frozen bundle of every OTel instrument the Scalar UI handler emits.
*
* - `openapi.ui.requests_total` — counter labelled `{ ui: "scalar" }`
* incremented on each successful HTML response. Per-plugin shells
* additionally carry `{ plugin: "<name>" }`. Asset sub-requests do NOT
* increment this counter — router request metrics cover those.
*/
export interface ScalarInstruments {
readonly meter: Meter;
readonly requestsTotal: Counter;
}
const METER_NAME = "@suaveplan/api-openapi-ui-scalar";
/** UI label attribute attached to every observation. */
export const UI_LABEL_VALUE = "scalar";
/**
* Build the Scalar UI handler's instrument registry. Returns a frozen
* bundle whose counter is constructed once at handler-mount time.
*/
export function createScalarInstruments(
config: ScalarInstrumentsConfig = {}
): ScalarInstruments {
const meterProvider = config.meterProvider ?? metrics.getMeterProvider();
const meter = meterProvider.getMeter(METER_NAME);
const requestsTotal = meter.createCounter("openapi.ui.requests_total");
return Object.freeze({ meter, requestsTotal });
}
/**
* Record a UI HTML request. Always labels the observation with
* `{ ui: "scalar" }`; the optional `plugin` attribute fans out per-plugin
* mounts.
*/
export interface RecordUiRequestAttributes {
readonly plugin?: string;
}
export function recordUiRequest(
instruments: ScalarInstruments,
attributes: RecordUiRequestAttributes = {}
): void {
const attrs: Record<string, string> = { ui: UI_LABEL_VALUE };
const { plugin } = attributes;
if (plugin !== undefined) attrs["plugin"] = plugin;
instruments.requestsTotal.add(1, attrs);
}