Skip to content

cplieger/plex-exporter

plex-exporter

License: GPL-3.0 GitHub release Image Size Platforms base: Distroless

See what your Plex server is doing in Grafana — sessions, libraries, bandwidth, transcoding.

What it does

Connects to your Plex Media Server and exposes metrics (active sessions, library sizes, bandwidth, transcoding status) in a format that Prometheus can scrape and Grafana can visualize.

Key metrics exposed:

  • Library duration, storage, and item counts (movies, episodes, tracks)
  • Active session details (user, device, resolution, stream type)
  • Transcode type detection (video/audio/both) and subtitle handling
  • Session bandwidth and location (LAN/WAN)
  • Host CPU and memory utilization (Plex Pass)
  • Bandwidth transmission totals (Plex Pass)
  • WebSocket connection health
  • Active transcode session count

Why this design

  • WebSocket for real-time session tracking — listens to the Plex notification stream for instant session updates instead of polling on an interval
  • Single binary with no runtime dependencies — only two direct Go dependencies (coder/websocket and prometheus/client_golang), everything else is stdlib
  • Distroless and rootless — runs on gcr.io/distroless/static as UID 65534 with no shell or package manager, minimizing attack surface
  • Prometheus-native — exposes a standard /metrics endpoint that works with any Prometheus-compatible scraper and any Grafana dashboard, no custom visualization layer

Limitations

  • Plex Pass features degrade gracefully. CPU/memory utilization and bandwidth statistics require Plex Pass. Without it, those metrics are simply absent — the exporter still works for all other metrics.
  • WebSocket is required. The exporter uses the Plex WebSocket notification stream for real-time session tracking. If your Plex server is behind a reverse proxy, ensure WebSocket connections are forwarded correctly.
  • Library item counts are cached. Episode, track, and item counts are refreshed every 15 minutes to avoid hammering the Plex API. Counts may lag slightly after large library scans.

Quick start

Available from both ghcr.io/cplieger/plex-exporter and docker.io/cplieger/plex-exporter — identical images and tags.

services:
  plex-exporter:
    image: ghcr.io/cplieger/plex-exporter:latest
    container_name: plex-exporter
    restart: unless-stopped
    user: "1000:1000"  # match your host user

    environment:
      TZ: "Europe/Paris"
      PLEX_SERVER: "http://plex:32400"  # full URL including scheme and port
      PLEX_TOKEN: "your-plex-token"  # admin token from Plex Web settings

    ports:
      - "9594:9594"

Configuration reference

Environment variables

Variable Description Default Required
PLEX_SERVER Full URL of your Plex Media Server including scheme and port (e.g. http://192.0.2.100:32400) http://plex:32400 Yes
PLEX_TOKEN Plex authentication token for the server administrator. Get it from Plex Web → Settings → XML view → myPlexAccessToken - Yes
TZ Container timezone Europe/Paris No
LISTEN_ADDRESS Address and port for the metrics HTTP server :9594 No
PLEX_CA_CERT_PATH Path to a PEM file containing your Plex server's CA certificate. When set, that CA is added to the TLS RootCAs pool — TLS verification stays on, pinned to your CA. Required only when (a) your PLEX_SERVER uses https:// and (b) the cert isn't trusted by the OS bundle (i.e. you signed it yourself or with a private CA). Plain http:// URLs and Plex's official *.plex.direct HTTPS URLs need no TLS env var. unset No

TLS / certificate setup

Pick the configuration that matches your Plex server:

Your PLEX_SERVER looks like What to do
http://plex:32400 (Docker network, LAN, etc.) nothing — TLS isn't in use
https://<hash>.plex.direct:32400 (Plex's official cert) nothing — Let's Encrypt is trusted by default
https://192.0.2.100:32400 or https://plex.local (self-signed / private CA) set PLEX_CA_CERT_PATH to the PEM file of the CA that signed your Plex cert

Ports

Port Description
9594 Prometheus metrics endpoint (/metrics) and health check (/api/health)

Metrics reference

HTTP Endpoints

Endpoint Method Description
/metrics GET Prometheus metrics (see below)
/api/health GET Returns {"status":"ok"} when ready, 503 when starting/stopping

Server Metrics

Metric Type Labels Description
plex_server_info Gauge (always 1) server, server_id, version, platform, platform_version, plex_pass Server metadata and Plex Pass status
plex_host_cpu_utilization_ratio Gauge server, server_id Host CPU utilization as a ratio (0.0–1.0). Requires Plex Pass.
plex_host_memory_utilization_ratio Gauge server, server_id Host memory utilization as a ratio (0.0–1.0). Requires Plex Pass.
plex_transmit_bytes_total Counter server, server_id Cumulative bytes transmitted (from Plex bandwidth API). Requires Plex Pass. Resets on container restart — indicative only.
plex_estimated_transmit_bytes_total Counter server, server_id Estimated bytes transmitted based on session bitrates. Resets on container restart — indicative only.
plex_active_transcode_sessions Gauge server, server_id Number of active video transcode sessions (from root endpoint, no Plex Pass needed)
plex_websocket_connected Gauge server, server_id WebSocket connection status: 1 = connected, 0 = disconnected
plex_http_reachable Gauge server, server_id HTTP polling reachability: 1 = last refresh succeeded, 0 = failed
plex_exporter_errors_total Counter server, server_id, type Exporter error count by type. Types: refresh, websocket_dial, websocket_read, invalid_message, sessions_fetch, metadata_fetch, invalid_rating_key, metrics_server, library_items.

Library Metrics

Metric Type Labels Description
plex_library_duration_milliseconds Gauge server, server_id, library_type, library, library_id Total duration of all items in the library (ms)
plex_library_storage_bytes Gauge server, server_id, library_type, library, library_id Total storage used by the library (bytes)
plex_library_items Gauge server, server_id, library_type, library, library_id, content_type Number of items in the library. content_type is movies, episodes, tracks, photos, or items. Refreshed every 15 minutes.

Session Metrics

Metric Type Labels Description
plex_plays_active Gauge server, server_id, library, library_id, library_type, media_type, title, child_title, grandchild_title, stream_type, stream_resolution, stream_file_resolution, device, device_type, user, session, transcode_type, subtitle_action, location, local Currently active play sessions (1 per session). Use count(plex_plays_active) for total stream count. Removed after 60s of inactivity.
plex_play_seconds_total Counter (same as above) Cumulative play time for the session (seconds)
plex_session_bandwidth_kbps Gauge server, server_id, session, user, location Real-time session bandwidth from the Plex Sessions API (kbps)
plex_session_bitrate_kbps Gauge server, server_id, session, user, location Live stream bitrate per session (kbps). Replaces the former stream_bitrate label on plex_plays_active/plex_play_seconds_total, which caused unbounded cardinality as Plex reports changing bitrates during adaptive streaming.

Session Label Reference

Label Values Description
stream_type direct play, copy, transcode How the stream is being delivered
transcode_type none, video, audio, both What is being transcoded
subtitle_action none, burn, copy, transcode How subtitles are handled
location lan, wan Client network location
local true, false Whether the client is on the local network
media_type movie, episode, track, etc. Plex media type

For episodes: title = show name, child_title = season, grandchild_title = episode title. For movies: title = movie name, others are empty.

Healthcheck

The container includes an HTTP health endpoint (/api/health) and a CLI probe (/plex-exporter health) that checks a /tmp/.healthy marker file written once the HTTP server is listening — no shell, HTTP client, or open port required. The container becomes unhealthy only if the initial Plex connection fails or the metrics server fails to start; WebSocket disconnects do not trigger unhealthy status because the exporter reconnects automatically with exponential backoff (monitor via plex_websocket_connected).

Code quality

Metric Value
Test Coverage 81.6%
Tests 188
Cyclomatic Complexity (avg) 4.0
Cognitive Complexity (avg) 3.8
Mutation Efficacy 87.3% (59 runs)
Test Framework Property-based (rapid) + table-driven

Tests cover Prometheus metric collection (all 13 metric descriptors, server/library/session metrics, Plex Pass gating), session tracking (play/stop/resume lifecycle, concurrent sessions, bandwidth accumulation, prune timeouts), transcode detection and subtitle classification, library item counting with artist-type fallback, bandwidth tracking with boundary conditions, HTTP client retry logic, and the full refresh cycle (server info, library items, resources). Property-based tests verify invariants across all pure functions.

Not tested: WebSocket connection management, the main event loop, and ticker-based refresh scheduling — these are I/O-bound runtime paths. WebSocket health is monitored via the plex_websocket_connected Prometheus metric.

Security

No vulnerabilities found. All scans clean across 7 tools.

Tool Result
govulncheck No vulnerabilities in call graph
golangci-lint (gosec, gocritic) 0 issues
trivy 0 vulnerabilities (distroless base)
grype 0 vulnerabilities
gitleaks No secrets detected
semgrep 2 info (false positives)
hadolint Clean

Connects outbound to Plex only. The /metrics endpoint serves read-only Prometheus data (standard for internal exporters). PLEX_TOKEN is never logged or exposed in metrics. Runs as nonroot on a distroless base image with no shell.

Details for advanced users: Plex response bodies capped at 10 MB via io.LimitReader. WebSocket messages capped at 1 MB. All HTTP clients use explicit 10s timeouts; the metrics server sets ReadHeaderTimeout, ReadTimeout, WriteTimeout, IdleTimeout, and MaxHeaderBytes (1 MB). Rating keys validated via strconv.Atoi before URL construction. Explicit MinVersion: tls.VersionTLS12 set on TLS config. Semgrep flags the /tmp/.healthy marker and the opt-in TLS skip (both intentional).

Dependencies

All dependencies are updated automatically via Renovate and pinned by digest or version for reproducibility.

Dependency Version Source
golang 1.26-alpine Go
gcr.io/distroless/static-debian13 nonroot Distroless
github.com/coder/websocket v1.8.14 GitHub
github.com/prometheus/client_golang v1.23.2 GitHub
github.com/prometheus/client_model v0.6.2 GitHub
golang.org/x/sync v0.20.0 Go stdlib
pgregory.net/rapid v1.3.0 pkg.go.dev

Credits

This is an original tool that builds upon prometheus-plex-exporter.

Contributing

Issues and pull requests are welcome. Please open an issue first for larger changes so the approach can be discussed before implementation.

Disclaimer

These images are built with care and follow security best practices, but they are intended for homelab use. No guarantees of fitness for production environments. Use at your own risk.

This project was built with AI-assisted tooling using Claude Opus and Kiro. The human maintainer defines architecture, supervises implementation, and makes all final decisions.

License

This project is licensed under the GNU General Public License v3.0.

About

See what your Plex server is doing in Grafana — sessions, libraries, bandwidth, transcoding

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors