Initial Commit
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
export {
|
||||
createGraphiqlInstruments,
|
||||
type GraphiqlInstruments,
|
||||
type GraphiqlInstrumentsConfig,
|
||||
recordUiRequest,
|
||||
UI_LABEL_VALUE,
|
||||
} from "./instruments.js";
|
||||
@@ -0,0 +1,72 @@
|
||||
# `instruments`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Owns the OpenTelemetry instrument bundle emitted by the GraphiQL UI
|
||||
plugin. A single `Counter` (`graphql.ui.requests_total`) is constructed
|
||||
at mount time and incremented on each successful HTML response. The
|
||||
counter is always labelled `{ ui: "graphiql" }` so dashboards can sum
|
||||
across sibling UI plugins (`api-graphql-ui-altair` etc.) without
|
||||
collision.
|
||||
|
||||
Asset sub-requests (`<uiEndpoint>/assets/...`) do NOT increment this
|
||||
counter — they are covered by the host router's standard request
|
||||
metrics.
|
||||
|
||||
## Features
|
||||
|
||||
- Single `Counter` instrument — `graphql.ui.requests_total`.
|
||||
- Optional `meterProvider` parameter — defaults to the OTel global
|
||||
(which falls back to a no-op when no SDK is installed).
|
||||
- Frozen instrument bundle — meter + counter created once at plugin
|
||||
mount time.
|
||||
- `recordUiRequest(instruments)` helper — applies the `{ ui: "graphiql" }`
|
||||
label automatically so call sites never get it wrong.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
createGraphiqlInstruments,
|
||||
recordUiRequest,
|
||||
} from "@suaveplan/api-graphql-ui-graphiql";
|
||||
|
||||
const instruments = createGraphiqlInstruments();
|
||||
recordUiRequest(instruments);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { MeterProvider } from "@opentelemetry/sdk-metrics";
|
||||
import { createGraphiqlInstruments } from "@suaveplan/api-graphql-ui-graphiql";
|
||||
|
||||
const meterProvider = new MeterProvider({ /* readers, views, ... */ });
|
||||
const instruments = createGraphiqlInstruments({ meterProvider });
|
||||
// instruments.meter / instruments.requestsTotal are now wired to the
|
||||
// caller-controlled provider rather than the OTel global.
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface GraphiqlInstrumentsConfig { meterProvider?: MeterProvider }`.
|
||||
- `interface GraphiqlInstruments { meter: Meter; requestsTotal: Counter }`
|
||||
— frozen at construction.
|
||||
- `createGraphiqlInstruments(config?)` — returns the frozen bundle.
|
||||
- `recordUiRequest(instruments)` — increments the counter with the
|
||||
canonical `{ ui: "graphiql" }` label.
|
||||
- `UI_LABEL_VALUE: "graphiql"` — the literal used in every observation.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`UI_LABEL_VALUE` is exported as a named constant so the sibling Altair
|
||||
package can reference it via a `not-equal` predicate in shared tests
|
||||
("the two UI plugins MUST emit distinct `ui` labels"). The counter name
|
||||
is shared across UI plugins by design — operators get a single rollup of
|
||||
"explorer requests" without per-UI configuration. The Stage-5
|
||||
testing-kit's in-memory `MeterProvider` is used in tests; production
|
||||
operators register their preferred SDK once at process boot.
|
||||
@@ -0,0 +1,95 @@
|
||||
import type { Meter, MeterProvider } from "@opentelemetry/api";
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import {
|
||||
createGraphiqlInstruments,
|
||||
recordUiRequest,
|
||||
UI_LABEL_VALUE,
|
||||
} from "./instruments.js";
|
||||
|
||||
interface AddCall {
|
||||
readonly value: number;
|
||||
readonly attrs?: Readonly<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
interface FakeInstrument {
|
||||
readonly name: string;
|
||||
readonly addCalls: AddCall[];
|
||||
}
|
||||
|
||||
interface CapturedMeter {
|
||||
readonly name: string;
|
||||
readonly instruments: FakeInstrument[];
|
||||
}
|
||||
|
||||
function buildFakeMeterProvider(): {
|
||||
provider: MeterProvider;
|
||||
meters: CapturedMeter[];
|
||||
} {
|
||||
const meters: CapturedMeter[] = [];
|
||||
const provider: MeterProvider = {
|
||||
getMeter(name: string): Meter {
|
||||
const captured: CapturedMeter = { name, instruments: [] };
|
||||
meters.push(captured);
|
||||
const meter: Meter = {
|
||||
createCounter(n) {
|
||||
const inst: FakeInstrument = { name: n, addCalls: [] };
|
||||
captured.instruments.push(inst);
|
||||
return {
|
||||
add(v: number, a?: Readonly<Record<string, unknown>>) {
|
||||
inst.addCalls.push({ value: v, attrs: a });
|
||||
},
|
||||
} as ReturnType<Meter["createCounter"]>;
|
||||
},
|
||||
} as unknown as Meter;
|
||||
return meter;
|
||||
},
|
||||
} as MeterProvider;
|
||||
return { provider, meters };
|
||||
}
|
||||
|
||||
describe("createGraphiqlInstruments", () => {
|
||||
it("creates the graphql.ui.requests_total counter on the package meter", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const { provider, meters } = buildFakeMeterProvider();
|
||||
const instruments = createGraphiqlInstruments({
|
||||
meterProvider: provider,
|
||||
});
|
||||
expect(meters[0]?.name).toBe("@suaveplan/api-graphql-ui-graphiql");
|
||||
expect(meters[0]?.instruments.map((i) => i.name)).toEqual([
|
||||
"graphql.ui.requests_total",
|
||||
]);
|
||||
expect(instruments.requestsTotal).toBeDefined();
|
||||
expect(instruments.meter).toBeDefined();
|
||||
});
|
||||
|
||||
it("falls back to OTel globals when no provider supplied", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const instruments = createGraphiqlInstruments();
|
||||
expect(instruments.requestsTotal).toBeDefined();
|
||||
});
|
||||
|
||||
it("UI HTML increments graphql.ui.requests_total", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const { provider, meters } = buildFakeMeterProvider();
|
||||
const instruments = createGraphiqlInstruments({
|
||||
meterProvider: provider,
|
||||
});
|
||||
recordUiRequest(instruments);
|
||||
recordUiRequest(instruments);
|
||||
const counter = meters[0]?.instruments.find(
|
||||
(i) => i.name === "graphql.ui.requests_total"
|
||||
);
|
||||
expect(counter?.addCalls).toHaveLength(2);
|
||||
expect(counter?.addCalls[0]?.value).toBe(1);
|
||||
expect(counter?.addCalls[0]?.attrs).toEqual({ ui: UI_LABEL_VALUE });
|
||||
});
|
||||
|
||||
it("UI_LABEL_VALUE is the literal 'graphiql' so dashboards can sum across UIs", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(UI_LABEL_VALUE).toBe("graphiql");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { Counter, Meter, MeterProvider } from "@opentelemetry/api";
|
||||
import { metrics } from "@opentelemetry/api";
|
||||
|
||||
/**
|
||||
* Config consumed by {@link createGraphiqlInstruments}. The provider
|
||||
* defaults to the OTel global, which falls back to a no-op
|
||||
* implementation when no SDK is installed — so the plugin works in test
|
||||
* envs without ceremony.
|
||||
*/
|
||||
export interface GraphiqlInstrumentsConfig {
|
||||
readonly meterProvider?: MeterProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Frozen bundle of every OTel instrument the GraphiQL UI plugin emits.
|
||||
*
|
||||
* - `graphql.ui.requests_total` — counter labelled `{ ui: "graphiql" }`
|
||||
* incremented on each successful HTML response. Asset sub-requests do
|
||||
* NOT increment this counter (router request metrics cover those).
|
||||
*/
|
||||
export interface GraphiqlInstruments {
|
||||
readonly meter: Meter;
|
||||
readonly requestsTotal: Counter;
|
||||
}
|
||||
|
||||
const METER_NAME = "@suaveplan/api-graphql-ui-graphiql";
|
||||
|
||||
/** UI label attribute attached to every observation. */
|
||||
export const UI_LABEL_VALUE = "graphiql";
|
||||
|
||||
/**
|
||||
* Build the GraphiQL UI plugin's instrument registry. Returns a frozen
|
||||
* bundle whose counter is constructed once at plugin-mount time.
|
||||
*/
|
||||
export function createGraphiqlInstruments(
|
||||
config: GraphiqlInstrumentsConfig = {}
|
||||
): GraphiqlInstruments {
|
||||
const meterProvider = config.meterProvider ?? metrics.getMeterProvider();
|
||||
const meter = meterProvider.getMeter(METER_NAME);
|
||||
const requestsTotal = meter.createCounter("graphql.ui.requests_total");
|
||||
return Object.freeze({ meter, requestsTotal });
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a UI HTML request. Always labels the observation with
|
||||
* `{ ui: "graphiql" }` so dashboards can sum across sibling UI plugins
|
||||
* (e.g. `api-graphql-ui-altair`).
|
||||
*/
|
||||
export function recordUiRequest(instruments: GraphiqlInstruments): void {
|
||||
instruments.requestsTotal.add(1, { ui: UI_LABEL_VALUE });
|
||||
}
|
||||
Reference in New Issue
Block a user