Observability

Open Pryv.io v2 ships an optional observability layer that reports per-method usage metrics and server-side error reports to a monitoring backend of your choice, over OTLP/HTTP. It is off by default.

No third-party monitoring agent runs inside the process, and nothing is auto-instrumented. Telemetry is constructed by the platform from a fixed allow-list and then sent. That distinction is the whole design: what can leave your deployment is a property of the platform’s source code, not of an external agent’s defaults, so it does not change when a dependency is upgraded.

Data handling note. Enabling observability means performance metadata leaves your deployment for wherever you point it. The emitted surface is enumerated below and is deliberately anonymous, but you own the decision and — unless you point it at a collector you host yourself — it is a processing arrangement with a third party. Read What is sent before enabling this on a deployment holding personal data.

Table of contents

  1. Overview
  2. What is sent
  3. What cannot be sent
  4. How anonymous is it, exactly
  5. Choosing a backend
  6. Enabling it
  7. The reporting interval
  8. Verifying what your backend holds
  9. Rotating credentials
  10. Disabling
  11. Caveats

Overview

A single emitter inside the core builds every datapoint from a compile-time vocabulary, validates it, aggregates it in memory, and posts it to your OTLP endpoint on a timer. Anything that does not match the vocabulary is dropped and counted, never sent.

Configuration is cluster-wide, stored in PlatformDB, and managed with bin/observability.js. Credentials are encrypted at rest. Changes require a rolling restart of your cores: workers receive the resolved configuration from the master process when they start.

What is sent

This is the complete list. There is no “and other diagnostic data”.

Metrics, aggregated per reporting interval:

Metric Labels
api.method.calls method.id, status.class
api.method.duration (histogram, ms) method.id, status.class
api.method.errors method.id, error.code
telemetry.dropped reason

Process identity, attached to every batch: service name (the label you set), service version, the machine hostname, and a worker index.

Error reports, for server-side faults only (5xx, unexpected and unclassifiable errors; client mistakes such as a rejected token are counted, not reported):

What cannot be sent

Request URLs, query parameters, route parameters, request and response bodies, HTTP headers of any kind, usernames, stream / event / attachment identifiers, application log records, and error message text.

None of these has a key in the emitted vocabulary, so no code path can emit them.

Error messages are excluded permanently and on purpose. Messages routinely interpolate whatever failed — ENOENT: ... open '/var-pryv/users/<id>/...', “user <email> not found”, validation errors quoting the submitted value. The error code travels; the message stays in your own logs, where identifying data belongs.

Stack frames are rebuilt rather than filtered. Each frame is decomposed into a function name and a source location, both checked against an allow-list, then reassembled. A frame that is not a repository-relative location or a Node internal is replaced by at <external>, so paths from outside the installation — a global module directory, an operator’s own plugin folder, a home directory — are never emitted even in part.

How anonymous is it, exactly

The claim is: anonymous by construction, with a residual correlation risk at very low traffic volumes. We state it that way rather than as an unqualified guarantee, because the qualification is real.

Two design choices carry it beyond simply omitting identifiers:

The residual: on a very low-traffic instance, “one error in this interval” can still correlate to the only active user. That follows from traffic volume rather than from what is emitted, so no schema change removes it. Widening the reporting interval reduces it.

If your deployment cannot accept even that, point the endpoint at a collector inside your own infrastructure — then no third party is involved at all.

Choosing a backend

Any service that ingests OTLP/HTTP works: New Relic, Grafana, Datadog, Honeycomb, Elastic, or an OpenTelemetry Collector you run yourself. The platform sends the same payload to all of them, so the data-protection posture does not vary by vendor.

A backend that only speaks gRPC or a proprietary protocol can be reached by putting an OpenTelemetry Collector in front of it. Running that collector inside your own trust boundary is also the recommended pattern when you want monitoring without a third-party processor.

Enabling it

From any core, with PlatformDB reachable:

# 1. where telemetry goes (the standard OTLP paths /v1/metrics and
#    /v1/logs are appended to this base URL)
node bin/observability.js set-endpoint https://otlp.eu01.nr-data.net

# 2. whatever auth header that backend expects, stored encrypted at rest
node bin/observability.js set-header api-key YOUR-KEY-HERE

# 3. a label to tell your deployments apart in the backend's UI
node bin/observability.js set-app-name 'open-pryv.io (example.com)'

# 4. turn it on, then rolling-restart every core
node bin/observability.js enable

Plain http:// is accepted when the collector is host-local: loopback, private (RFC1918) or link-local space, so a collector running as a sidecar container reached on the bridge gateway (http://172.17.0.1:4318, say) needs no certificate for a hop that never crosses a network. Cleartext to any routable address is refused, and the same rule is applied by the emitter at startup, not only by this command, so writing the value straight into PlatformDB or exporting it in the environment cannot get plaintext telemetry onto the wire.

Check the effective configuration at any time — credential values are never echoed:

node bin/observability.js show

show also prints the emitted surface, so an operator can answer “what does my backend see?” without reading source.

Keep the app name free of anything identifying. It is the one label you choose rather than the platform, it is validated only for shape, and it is attached to every batch.

The reporting interval

node bin/observability.js set-interval 600     # seconds; 60-3600, default 300

This is a privacy control, not only a tuning knob. It sets the granularity at which activity is observable, and it is the only lever on the low-traffic residual described above. Longer intervals weaken correlation and slow alerting; shorter intervals do the reverse. Values outside 60-3600 are clamped.

Verifying what your backend holds

Do not take this page’s word for it. After enabling and restarting, ask the backend to enumerate what it actually received, and scope the query to a time window that starts after your restart — older data will otherwise answer for the previous configuration.

With New Relic, for example:

-- every metric name the account holds for this deployment
SELECT uniques(metricName) FROM Metric
WHERE service.name = 'open-pryv.io (example.com)' SINCE '<restart time>'

-- every attribute key present on those metrics
SELECT keyset() FROM Metric
WHERE service.name = 'open-pryv.io (example.com)' SINCE '<restart time>'

Enumerate rather than spot-check for absence: a query for a specific identifier returning zero proves only that you guessed the identifier’s shape correctly, whereas listing every key present proves the surface. The result should contain only the metric names and label keys listed under What is sent.

Watch telemetry.dropped as well. It should normally be zero or near it; a rising count means datapoints are being refused, and the reason label says why.

Rotating credentials

node bin/observability.js set-header api-key NEW-KEY      # overwrite
node bin/observability.js clear-headers                   # remove all

Then rolling-restart every core. Headers are stored AES-256-GCM encrypted at rest, with key material derived from the platform admin key.

Disabling

node bin/observability.js disable

Then rolling-restart. For an immediate local kill switch that does not depend on PlatformDB, set observability.enabled: false in a core’s own configuration — a local false always wins, on that core, at its next start.

Caveats