-
-
Notifications
You must be signed in to change notification settings - Fork 4
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.
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.
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>"
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.)
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.
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 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.
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.
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/MetricsA 401 means the token is not an administrator's; a 404 means the plugin is not loaded, or the
base path is wrong.
# 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.
- Security Model - how the login path fails closed, and what the rate limiter does.
- Troubleshooting - what a specific rejection reason means to the user who hit it.
- Hardening & Options Reference - every configuration option, including the rate-limit settings.
Repository · Issues · Releases · Security policy - report vulnerabilities privately, never in a public issue. Pages describe what is implemented today; if the wiki disagrees with the code, the code wins.
Getting started
- Installation
- Provider Setup
- Hardening & Options Reference
- Linked Accounts
- Config as code
- Migrating from 9p4
- Troubleshooting
- Rollback
- Server Migration
How it works
Security
- Security Model
- Security Conformance (ASVS / RFC 9700)
- SSO-Only Login - design record
- Single Logout - design record
Standards & process (internal / maintainer)