@docx-editor.dev/telemetry/hocuspocus
Hocuspocus: room telemetry in one call. It adds one extension that runs before all others and times each load and store from the first hook to the last, and it observes your hooks and the extensions you pass in extensions, so a refusal any of them throws is counted.
Functions (4)
hmacRoomLabelfunctionSource ↗
A stable room label that does not reveal the name: an HMAC of the name with your secret. The same name gives the same label across processes and restarts.
declare function hmacRoomLabel(secret: string | undefined): (name: string) => string;```ts
const rooms = roomTelemetry({
exporters: [metrics],
roomLabel: hmacRoomLabel(process.env.ROOM_LABEL_SECRET),
});
```requireBearerTokenfunctionSource ↗
An authorize check for a shared token, sent as Authorization: Bearer <token>. The comparison takes the same time whatever the token, and an empty secret throws at startup rather than letting every request in.
declare function requireBearerToken(secret: string | undefined): (request: onRequestPayload['request']) => boolean;```ts
serveMetrics(metrics, { authorize: requireBearerToken(process.env.METRICS_TOKEN) });
```serveMetricsfunctionSource ↗
An onRequest hook that serves Prometheus metrics from the Hocuspocus HTTP server itself, for a deployment without a separate web framework. Other requests fall through to the default handler.
declare function serveMetrics(registry: PrometheusTextSource, options?: ServeMetricsOptions): (payload: onRequestPayload) => Promise<void>;```ts
new Server({
...config,
onRequest: serveMetrics(metrics, {
rooms: { telemetry: rooms, path: '/rooms' },
authorize: requireBearerToken(process.env.METRICS_TOKEN),
}),
});
```withCollaborationTelemetryfunctionSource ↗
Add room telemetry to a Hocuspocus configuration.
It records rooms, connections, the people in each room (from awareness), room memory and size, load and store times, and why connections and messages were refused. Your hooks and your extensions run exactly as before, with the same arguments, return values, and this; their errors are rethrown. Extensions added later with server.configure are not observed. Requires @hocuspocus/server 4.6 or later.
declare function withCollaborationTelemetry<C extends Partial<ServerConfiguration>>(config: C, options: RoomTelemetryOptions): CollaborationTelemetry<C>;```ts
import { Server } from '@hocuspocus/server';
import { withCollaborationTelemetry } from '@docx-editor.dev/telemetry/hocuspocus';
import { prometheusRegistry, metricsHandler } from '@docx-editor.dev/telemetry/prometheus';
const metrics = prometheusRegistry();
const { config, rooms } = withCollaborationTelemetry(
{ port: 1234, onAuthenticate, extensions: [new Database({ fetch, store })] },
{ exporters: [metrics] }
);
const server = new Server(config); // telemetry flushes when the server is destroyed
app.get('/metrics', metricsHandler(metrics));
```Interfaces (11)
AwarenessLikeinterfaceSource ↗
The part of a y-protocols Awareness the telemetry reads.
interface AwarenessLike| Member | Type | Summary |
|---|---|---|
| getStates | | |
| off | | |
| on | |
CollaborationTelemetryinterfaceSource ↗
What [withCollaborationTelemetry](withCollaborationTelemetry) returns.
interface CollaborationTelemetry<C>| Member | Type | Summary |
|---|---|---|
| config | C | Your configuration with observed hooks. Pass it to `new Server(config)`. |
| rooms | RoomTelemetry | The room telemetry, for `list()`, `sample()`, and `shutdown()`. |
MetricsHandlerOptionsinterfaceSource ↗
Options for [metricsHandler](metricsHandler) and [fetchMetricsHandler](fetchMetricsHandler).
interface MetricsHandlerOptions| Member | Type | Summary |
|---|---|---|
| merge? | readonly PrometheusTextSource[] | More sources rendered into the same response, such as your `prom-client` registry: `merge: [register]`. |
PrometheusTextSourceinterfaceSource ↗
Anything that renders Prometheus text, such as a prom-client Registry.
interface PrometheusTextSource| Member | Type | Summary |
|---|---|---|
| metrics | |
RoomHandleinterfaceSource ↗
One open room. Returned by [RoomTelemetry.open](RoomTelemetry.open).
interface RoomHandle| Member | Type | Summary |
|---|---|---|
| begin | | Start timing `operation` where `time` cannot wrap it; call the result once it settles. |
| close | | Stop tracking the room, for example when the server unloads it. |
| connected | | |
| disconnected | | |
| label | string | The room's label, as [RoomTelemetryOptions.roomLabel](RoomTelemetryOptions.roomLabel) made it. |
| time | | Run `operation` and record its duration and outcome. Rethrows its error. |
RoomStatsinterfaceSource ↗
One room in [RoomTelemetry.list](RoomTelemetry.list).
interface RoomStats| Member | Type | Summary |
|---|---|---|
| ageSeconds | number | |
| blobBytes | number | null | |
| connections | number | |
| idleSeconds | number | |
| limitRatio | number | null | The largest share of any room limit in use, from 0 to 1. |
| nodes | number | null | |
| participants | number | People present, from awareness states. A person with two tabs counts twice. |
| pendingBytes | number | Bytes of updates applied since the room was last stored. |
| room | string | |
| stateBytes | number | null | Encoded Yjs state at the last sample, or `null` before the first one. |
RoomTelemetryinterfaceSource ↗
Room telemetry for one server process.
interface RoomTelemetry| Member | Type | Summary |
|---|---|---|
| begin | | Start timing an operation that has no room yet; call the result once it settles. |
| flush | | |
| list | | Every open room, for an admin endpoint you protect. |
| migrated | | A room migration finished. |
| open | | Start tracking a loaded room. Pass the room's awareness to count people; without it the number of connections stands in. |
| refused | | A message refused before it applied, such as `room-generation-changed`. |
| rejected | | A connection refused at admission. `reason` can be a string or an error with `reason`. |
| sample | | Sample people, memory, and size now. |
| shutdown | | Stop the timer, close every room, and shut the exporters down. |
| time | | Time an operation that happens before the room is open, such as a load that fails. |
RoomTelemetryOptionsinterfaceSource ↗
Options for [roomTelemetry](roomTelemetry).
interface RoomTelemetryOptions extends TelemetryPipelineOptions| Member | Type | Summary |
|---|---|---|
| measureState? | boolean | Measure each room's encoded Yjs state. This encodes the room once per sample, and only when it changed since the last one. Defaults to `true`. |
| perRoomSeries? | boolean | Also record each room's people and memory as gauges labeled `room`. Off by default: a label per room makes one series per room, and a large server has many rooms. A closed room's series is dropped. Only the default label or `hmacRoomLabel` is accepted, so no room name can become a label; any other `roomLabel` throws. |
| reasonOf? | (error: unknown) => string | undefined | Your own reading of a refusal, for errors whose `reason` or `code` is free text. Return a common reason such as `unauthorized`, a collaboration failure code, or one of `reasons`; anything else is counted as `other`. Return undefined to use the default reading. |
| reasons? | readonly string[] | Refusal reasons to keep as metric labels. Compared without case and recorded in lowercase. Collaboration failure codes are always kept. Any other reason is counted as `other`, so a free-text error never becomes a time series. |
| roomLabel? | (name: string) => string | How a room appears in [RoomTelemetry.list](RoomTelemetry.list). Defaults to an index that is valid only inside this process, such as `room-3`. Use `(name) => name` to see real names, or [hmacRoomLabel](hmacRoomLabel) for a stable label that does not reveal the name. |
| sampleInterval? | number | false | How often to sample people, memory, and room size, in milliseconds. Defaults to 60 000. `false` turns the timer off; call `sample()` yourself, for example before each scrape. |
ServeMetricsOptionsinterfaceSource ↗
Options for [serveMetrics](serveMetrics).
interface ServeMetricsOptions extends MetricsHandlerOptions| Member | Type | Summary |
|---|---|---|
| authorize? | (request: onRequestPayload['request']) => boolean | Return false to answer 403. Use [requireBearerToken](requireBearerToken) for a shared scrape token. Applies to both paths. |
| next? | (payload: onRequestPayload) => Promise<unknown> | Your own `onRequest`, called for every request this hook does not answer. |
| path? | string | The path Prometheus scrapes. Defaults to `/metrics`. |
| rooms? | {
readonly telemetry: RoomTelemetry;
readonly path: string;
} | Also serve `rooms.list()` as JSON at this path. It lists every room, so `authorize` is required with it. |
| sampleBeforeScrape? | RoomTelemetry | Sample this room telemetry before a scrape, for a server that sets `sampleInterval: false`. At most one sample is taken every 5 seconds, so several scrapers do not add extra samples. |
TelemetryExporterinterfaceSource ↗
Where telemetry goes. Implement event, metric, or both.
An exporter never throws into the editor or the server: a failure is caught and reported once through onExporterError.
interface TelemetryExporter```ts
const toMyLogger: TelemetryExporter = {
name: 'my-logger',
event: (event) => myLogger.warn(event.kind, { code: 'code' in event ? event.code : undefined }),
};
```| Member | Type | Summary |
|---|---|---|
| attach | | Called once when a telemetry client or room server takes the exporter. An exporter that holds events, such as a batching one, reads `consented()` before it sends, so events queued before consent was withdrawn are discarded, not sent. |
| declare | | Start a series at zero before its first measurement. A scraper that sees a counter first at 1 cannot count that first increase, so the room server declares its known series when it starts. Optional: an exporter without it creates each series at its first measurement. |
| event | | |
| flush | | Send anything buffered. Called by `flush()` and when a page is hidden. |
| forget | | Drop a series that no longer exists, such as a closed room's per-room gauge. Optional: an exporter without it keeps the series until it expires on its own. |
| metric | | |
| name | string | Shown in `onExporterError` reports. |
| shutdown | | Flush and release resources. Called by `shutdown()`. |
TelemetryPipelineOptionsinterfaceSource ↗
Options shared by the editor client and the room server.
interface TelemetryPipelineOptions| Member | Type | Summary |
|---|---|---|
| consent? | boolean | (() => boolean) | Whether the viewer agreed to telemetry. Read before each event and metric, so a consent banner can change it at any time. Events before consent are dropped, not buffered. Defaults to `true`: without this option, events are sent. Pass it whenever your app asks for consent. |
| exporters | readonly TelemetryExporter[] | Where telemetry goes. Nothing is recorded or sent without at least one exporter. |
| metricLabels? | readonly string[] | Keys of your own attributes that may also label metrics, such as `tenant.tier` or `app.region`. Only for values with few distinct values: each value makes a new series. After [MAX_METRIC_LABEL_VALUES](MAX_METRIC_LABEL_VALUES) distinct values of one key, later values become `other`. Never list a user, document, or room id here; put those on events instead. |
| onExporterError? | (error: unknown, exporter: string) => void | Told once per exporter when it throws. Defaults to one `console.warn`. |
| resource? | Attributes | Values added to every event, such as `{ 'service.name': 'contracts-app' }`. Metrics do not carry them: set resource attributes on your OpenTelemetry SDK or Prometheus target instead. You own these values: do not put a user, a document, or a room name here. |
Type aliases (2)
RoomOperationtypeSource ↗
What a timed room operation is.
type RoomOperation = 'load' | 'store';SettleOperationtypeSource ↗
Records a started operation's duration with its outcome. Later calls do nothing.
type SettleOperation = (outcome: 'success' | 'failure') => void;