Skip to content

Metrics

Nils Lehnen edited this page Aug 26, 2026 · 1 revision

Metrics

The plugin publishes counters about the sign-in path, so an operator can alert on a rate of failed logins, of account provisioning, or of failed calls out to an identity provider, instead of grepping the Jellyfin log for them.

This page says where the counters are, what each one means, and what exposing them costs.

The endpoint

GET /SSO/Metrics

Relative to the Jellyfin base URL, so a server published under a subpath carries that prefix here too. The response is Prometheus text exposition, served as text/plain; version=0.0.4; charset=utf-8. It is read-only and changes nothing.

It is not anonymous, and that is deliberate

Conventional /metrics endpoints are open. This one is elevation-gated like every other operator surface the plugin exposes, because the exposition names which identity providers a server has and how often logins against them fail. For a caller who cannot sign in, that is reconnaissance: the provider inventory, plus a live view of their own attempts landing. A scraper is given a token like any other Jellyfin API client instead.

There is no separate credential for the plugin. Present a Jellyfin administrator access token, or an API key issued from the dashboard, in the header form Jellyfin already uses:

Authorization: MediaBrowser Token="<token>"

It is not rate-limited

The plugin's per-client limiter fronts the login, callback, link and admin-write endpoints. This one is not among them, so polling it on a scrape interval is expected rather than tolerated. (The limiter is opt-in and off until an administrator enables it, but that is not what makes this endpoint safe to poll - it is simply not gated.)

The counters

Every metric is a Prometheus counter: it only ever rises. Each carries at most one label.

Metric Label Label values What it counts
sso_login_success_total provider the provider name as configured; a nameless provider is recorded as unnamed rather than as an empty label Logins that minted a session. Counted at the mint, which is the only place that knows a session was actually issued.
sso_login_failure_total reason UnknownProvider, InvalidState, AccountLinkForbidden, SsoResponseInvalid, SamlResponseInvalid, PkceNotSupported, EmailNotVerified, AwaitingApproval, AcrNotSatisfied, AuthTooOld, AccessExpired, Denied Login attempts the plugin refused, by the public reason the caller was given. Denied is the login refused for want of permission rather than for a stated protocol reason.
sso_account_provisioned_total outcome Created, Adopted Jellyfin accounts an SSO login brought into existence (Created), and pre-existing accounts it adopted by linking them to an SSO identity on that identity's first login (Adopted).
sso_provider_fetch_error_total stage Discovery, Token Failed server-to-provider fetches, by which fetch failed: the OpenID discovery document, or the token endpoint.
sso_request_throttled_total class challenge, callback, auth, test, metadata, unregister, link, logout, export Requests the rate limiter refused, by endpoint class. Each class carries its own budget, so the breakdown says which surface is being hit.
sso_metrics_series_refused_total none - Counter increments dropped because they named a series beyond the store's cap. See below.

Every counter is emitted even at zero. A series that appears only once it is non-zero cannot be alerted on with a rate rule until the thing being alerted on has already happened, so the whole set is present on the first scrape of a fresh server.

What a label never carries

No username, no identity-provider subject, no claim value. That is a property of the code rather than a rule somebody remembers: every breakdown except the provider name takes a value constrained to a closed enumeration, so an identity string does not compile at the call site. The provider name is the one free-form label, and it is configuration an administrator typed, never request input. Label values are escaped on the way out, so a name containing a quote or a newline cannot forge a line in a scrape.

The consequence for a monitoring system: this exposition is safe to ship to a metrics backend that is less protected than the Jellyfin server itself, as far as personal data goes. It still names your providers, which is the reason the endpoint needs a token.

The series cap

The store holds at most 512 distinct series. Past that, an increment naming a new series is dropped and counted in sso_metrics_series_refused_total; a series already known keeps counting, so the counters you are alerting on do not go silent when the cap is reached.

Every label value the plugin passes today comes from the configured provider set or from a closed vocabulary, so the cap is a backstop rather than a working limit - a server with twenty providers publishes roughly twenty provider series plus the fixed vocabularies. A non-zero sso_metrics_series_refused_total therefore means a defect rather than a large configuration, and is worth alerting on for its own sake: it says the scrape you are reading is missing a breakdown.

The counts are process-local

They live in the Jellyfin process and are not persisted. Every counter starts again at zero when the server restarts, and there is no aggregation across servers. That is what a monitoring system expects of a process-local counter, and Prometheus' rate() and increase() handle the reset for you - but it is worth saying before somebody reads a graph dropping to zero as an outage.

A scrape config to paste

scrape_configs:
  - job_name: jellyfin-sso
    metrics_path: /SSO/Metrics
    scheme: https
    static_configs:
      - targets: ["jellyfin.example.org"]
    authorization:
      type: MediaBrowser
      credentials: 'Token="REPLACE_WITH_TOKEN"'

The authorization block renders exactly the header Jellyfin expects. Keep the token in a file and use credentials_file instead if your Prometheus configuration is checked into source control.

To check the endpoint by hand before wiring a scraper to it:

curl -sS -H 'Authorization: MediaBrowser Token="REPLACE_WITH_TOKEN"' \
  https://jellyfin.example.org/SSO/Metrics

A 401 means the token is not an administrator's; a 404 means the plugin is not loaded, or the base path is wrong.

Rules worth having

# Sign-ins are failing faster than they are succeeding.
sum(rate(sso_login_failure_total[15m])) > sum(rate(sso_login_success_total[15m]))

# A provider stopped answering: discovery or token fetches are erroring.
sum by (stage) (rate(sso_provider_fetch_error_total[15m])) > 0

# Accounts are being created at a rate nobody planned for.
sum(increase(sso_account_provisioned_total{outcome="Created"}[1h])) > 10

# The exposition is incomplete - a defect, not a load problem.
increase(sso_metrics_series_refused_total[1h]) > 0

The thresholds are examples. Only the last one has a correct value that is not a judgement about your deployment: any increase at all is wrong.

See also

Clone this wiki locally