Initial Commit
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
export {
|
||||
createScalarInstruments,
|
||||
type RecordUiRequestAttributes,
|
||||
recordUiRequest,
|
||||
type ScalarInstruments,
|
||||
type ScalarInstrumentsConfig,
|
||||
UI_LABEL_VALUE,
|
||||
} from "./instruments.js";
|
||||
@@ -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.
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user