{"id":48890950,"url":"https://github.com/cplieger/plex-exporter","last_synced_at":"2026-06-16T09:00:34.784Z","repository":{"id":344077472,"uuid":"1180344569","full_name":"cplieger/plex-exporter","owner":"cplieger","description":"See what your Plex server is doing in Grafana — sessions, libraries, bandwidth, transcoding","archived":false,"fork":false,"pushed_at":"2026-06-12T10:18:53.000Z","size":339,"stargazers_count":2,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-12T12:03:16.814Z","etag":null,"topics":["distroless","docker","golang","grafana","homelab","metrics","monitoring","plex","plex-media-server","prometheus","prometheus-exporter","websocket"],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/cplieger.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-13T00:20:40.000Z","updated_at":"2026-06-12T10:18:56.000Z","dependencies_parsed_at":"2026-05-28T16:03:23.878Z","dependency_job_id":"5aa913f4-a6eb-4720-aa1e-6b49e58afed8","html_url":"https://github.com/cplieger/plex-exporter","commit_stats":null,"previous_names":["cplieger/docker-plex-exporter","cplieger/plex-exporter"],"tags_count":32,"template":false,"template_full_name":null,"purl":"pkg:github/cplieger/plex-exporter","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cplieger%2Fplex-exporter","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cplieger%2Fplex-exporter/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cplieger%2Fplex-exporter/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cplieger%2Fplex-exporter/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cplieger","download_url":"https://codeload.github.com/cplieger/plex-exporter/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cplieger%2Fplex-exporter/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34398408,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-16T02:00:06.860Z","response_time":126,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["distroless","docker","golang","grafana","homelab","metrics","monitoring","plex","plex-media-server","prometheus","prometheus-exporter","websocket"],"created_at":"2026-04-16T08:00:56.393Z","updated_at":"2026-06-16T09:00:34.765Z","avatar_url":"https://github.com/cplieger.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# plex-exporter\n\n[![Image Size](https://ghcr-badge.egpl.dev/cplieger/plex-exporter/size)](https://github.com/cplieger/plex-exporter/pkgs/container/plex-exporter)\n![Platforms](https://img.shields.io/badge/platforms-amd64%20%7C%20arm64-blue)\n![base: Distroless](https://img.shields.io/badge/base-Distroless_nonroot-4285F4?logo=google)\n[![Go Report Card](https://goreportcard.com/badge/github.com/cplieger/plex-exporter)](https://goreportcard.com/report/github.com/cplieger/plex-exporter)\n[![Test coverage](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/cplieger/plex-exporter/badges/coverage.json)](https://github.com/cplieger/plex-exporter/actions/workflows/coverage.yml)\n[![Mutation](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/cplieger/plex-exporter/badges/mutation.json)](https://github.com/cplieger/plex-exporter/issues?q=label%3Agremlins-tracker)\n[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/13216/badge)](https://www.bestpractices.dev/projects/13216)\n[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/cplieger/plex-exporter/badge)](https://scorecard.dev/viewer/?uri=github.com/cplieger/plex-exporter)\n[![SBOM](https://img.shields.io/badge/SBOM-SPDX-1D4ED8)](https://github.com/cplieger/plex-exporter/releases)\n\nSee what your Plex server is doing in Grafana — sessions, libraries, bandwidth, transcoding.\n\n## What it does\n\nConnects 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.\n\n**Key metrics exposed:**\n\n- Library duration, storage, and item counts (movies, episodes, tracks)\n- Active session details (user, device, resolution, stream type)\n- Transcode type detection (video/audio/both) and subtitle handling\n- Session bandwidth and location (LAN/WAN)\n- Host CPU and memory utilization (Plex Pass)\n- Bandwidth transmission totals (Plex Pass)\n- WebSocket connection health\n- Active transcode session count\n\n### Why this design\n\n- **WebSocket for real-time session tracking** — listens to the Plex notification stream for instant session updates instead of polling on an interval\n- **Single binary with no runtime dependencies** — minimal direct Go dependencies (`coder/websocket` and `prometheus/client_golang`), everything else is stdlib\n- **Distroless and rootless** — runs on `gcr.io/distroless/static` as UID 65534 with no shell or package manager, minimizing attack surface\n- **Prometheus-native** — exposes a standard `/metrics` endpoint that works with any Prometheus-compatible scraper and any Grafana dashboard, no custom visualization layer\n\n### Limitations\n\n- **Plex Pass features degrade gracefully.** CPU/memory utilization\n  and bandwidth statistics require Plex Pass. Without it, those\n  metrics are simply absent — the exporter still works for all\n  other metrics.\n- **WebSocket is required.** The exporter uses the Plex WebSocket\n  notification stream for real-time session tracking. If your Plex\n  server is behind a reverse proxy, ensure WebSocket connections\n  are forwarded correctly.\n- **Library item counts are cached.** Episode, track, and item\n  counts are refreshed every 15 minutes to avoid hammering the\n  Plex API. Counts may lag slightly after large library scans.\n\n## Quick start\n\nAvailable from both `ghcr.io/cplieger/plex-exporter` and `docker.io/cplieger/plex-exporter` — identical images and tags.\n\n```yaml\nservices:\n  plex-exporter:\n    image: ghcr.io/cplieger/plex-exporter:latest\n    container_name: plex-exporter\n    restart: unless-stopped\n    user: \"1000:1000\"  # match your host user\n\n    environment:\n      TZ: \"Europe/Paris\"\n      PLEX_SERVER: \"http://plex:32400\"  # full URL including scheme and port\n      PLEX_TOKEN: \"your-plex-token\"  # admin token from Plex Web settings\n\n    ports:\n      - \"9594:9594\"\n```\n\n## Configuration reference\n\n### Environment variables\n\n| Variable | Description | Default | Required |\n|----------|-------------|---------|----------|\n| `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 |\n| `PLEX_TOKEN` | Plex authentication token for the server administrator. Get it from Plex Web → Settings → XML view → myPlexAccessToken | - | Yes |\n| `TZ` | Container timezone | `Europe/Paris` | No |\n| `LISTEN_ADDRESS` | Address and port for the metrics HTTP server | `:9594` | No |\n| `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 |\n\n### TLS / certificate setup\n\nPick the configuration that matches your Plex server:\n\n| Your `PLEX_SERVER` looks like | What to do |\n|---|---|\n| `http://plex:32400` (Docker network, LAN, etc.) | nothing — TLS isn't in use |\n| `https://\u003chash\u003e.plex.direct:32400` (Plex's official cert) | nothing — Let's Encrypt is trusted by default |\n| `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 |\n\n### Ports\n\n| Port | Description |\n|------|-------------|\n| `9594` | Prometheus metrics endpoint (`/metrics`) and health check (`/api/health`) |\n\n## Metrics reference\n\n### HTTP Endpoints\n\n| Endpoint | Method | Description |\n|---|---|---|\n| `/metrics` | GET | Prometheus metrics (see below) |\n| `/api/health` | GET | Returns `{\"status\":\"OK\"}` when ready, 503 when starting/stopping |\n\n### Server Metrics\n\n| Metric | Type | Labels | Description |\n|---|---|---|---|\n| `plex_server_info` | Gauge (always 1) | `server`, `server_id`, `version`, `platform`, `platform_version`, `plex_pass` | Server metadata and Plex Pass status |\n| `plex_host_cpu_utilization_ratio` | Gauge | `server`, `server_id` | Host CPU utilization as a ratio (0.0–1.0). Requires Plex Pass. |\n| `plex_host_memory_utilization_ratio` | Gauge | `server`, `server_id` | Host memory utilization as a ratio (0.0–1.0). Requires Plex Pass. |\n| `plex_transmit_bytes_total` | Counter | `server`, `server_id` | Cumulative bytes transmitted (from Plex bandwidth API). Requires Plex Pass. Resets on container restart — indicative only. |\n| `plex_estimated_transmit_bytes_total` | Counter | `server`, `server_id` | Estimated bytes transmitted based on session bitrates. Resets on container restart — indicative only. |\n| `plex_active_transcode_sessions` | Gauge | `server`, `server_id` | Number of active video transcode sessions (from root endpoint, no Plex Pass needed) |\n| `plex_websocket_connected` | Gauge | `server`, `server_id` | WebSocket connection status: `1` = connected, `0` = disconnected |\n| `plex_http_reachable` | Gauge | `server`, `server_id` | HTTP polling reachability: `1` = last refresh succeeded, `0` = failed |\n| `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`. |\n\n### Library Metrics\n\n| Metric | Type | Labels | Description |\n|---|---|---|---|\n| `plex_library_duration_milliseconds` | Gauge | `server`, `server_id`, `library_type`, `library`, `library_id` | Total duration of all items in the library (ms) |\n| `plex_library_storage_bytes` | Gauge | `server`, `server_id`, `library_type`, `library`, `library_id` | Total storage used by the library (bytes) |\n| `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. |\n\n### Session Metrics\n\n| Metric | Type | Labels | Description |\n|---|---|---|---|\n| `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. |\n| `plex_play_seconds_total` | Counter | _(same as above)_ | Cumulative play time for the session (seconds) |\n| `plex_session_bandwidth_kbps` | Gauge | `server`, `server_id`, `session`, `user`, `location` | Real-time session bandwidth from the Plex Sessions API (kbps) |\n| `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. |\n\n### Session Label Reference\n\n| Label | Values | Description |\n|---|---|---|\n| `stream_type` | `direct play`, `copy`, `transcode` | How the stream is being delivered |\n| `transcode_type` | `none`, `video`, `audio`, `both` | What is being transcoded |\n| `subtitle_action` | `none`, `burn`, `copy`, `transcode` | How subtitles are handled |\n| `location` | `lan`, `wan` | Client network location |\n| `local` | `true`, `false` | Whether the client is on the local network |\n| `media_type` | `movie`, `episode`, `track`, etc. | Plex media type |\n\nFor episodes: `title` = show name, `child_title` = season,\n`grandchild_title` = episode title. For movies: `title` = movie\nname, others are empty.\n\n## Healthcheck\n\nThe 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`).\n\n## Security\n\n**No vulnerabilities found.** All scans clean.\n\n| Tool | Result |\n|------|--------|\n| [govulncheck](https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck) | No vulnerabilities in call graph |\n| [golangci-lint](https://golangci-lint.run/) (gosec, gocritic) | 0 issues |\n| [trivy](https://trivy.dev/) | 0 vulnerabilities (distroless base) |\n| [grype](https://github.com/anchore/grype) | 0 vulnerabilities |\n| [gitleaks](https://github.com/gitleaks/gitleaks) | No secrets detected |\n| [semgrep](https://semgrep.dev/) | 2 info (false positives) |\n| [hadolint](https://github.com/hadolint/hadolint) | Clean |\n\nConnects outbound to Plex only. The `/metrics` endpoint serves\nread-only Prometheus data (standard for internal exporters).\n`PLEX_TOKEN` is never logged or exposed in metrics. Runs as\n`nonroot` on a distroless base image with no shell.\n\n**Details for advanced users:** Plex response bodies capped at\n10 MB via `io.LimitReader`. WebSocket messages capped at 1 MB.\nAll HTTP clients use explicit 10s timeouts; the metrics server\nsets `ReadHeaderTimeout`, `ReadTimeout`, `WriteTimeout`,\n`IdleTimeout`, and `MaxHeaderBytes` (1 MB). Rating keys\nvalidated via `strconv.Atoi` before URL construction. Explicit\n`MinVersion: tls.VersionTLS12` set on TLS config. Semgrep flags\nthe `/tmp/.healthy` marker and the opt-in TLS skip (both\nintentional).\n\n## Dependencies\n\nAll dependencies are updated automatically via [Renovate](https://github.com/renovatebot/renovate) and pinned by digest or version for reproducibility.\n\n| Dependency | Source |\n|------------|--------|\n| golang | [Go](https://hub.docker.com/_/golang) |\n| gcr.io/distroless/static | [Distroless](https://github.com/GoogleContainerTools/distroless) |\n| github.com/coder/websocket | [GitHub](https://github.com/coder/websocket) |\n| github.com/prometheus/client_golang | [GitHub](https://github.com/prometheus/client_golang) |\n| github.com/prometheus/client_model | [GitHub](https://github.com/prometheus/client_golang) |\n| golang.org/x/sync | [Go stdlib](https://pkg.go.dev/golang.org/x/sync) |\n| pgregory.net/rapid | [pkg.go.dev](https://pkg.go.dev/pgregory.net/rapid) |\n\n## Credits\n\nThis is an original tool that builds upon [prometheus-plex-exporter](https://github.com/jsclayton/prometheus-plex-exporter).\n\n- Grafana Hackathon 2022\n  — the original hackathon project that started it all\n- [prometheus-plex-exporter](https://github.com/jsclayton/prometheus-plex-exporter)\n  by [@jsclayton](https://github.com/jsclayton) — the post-hackathon\n  fork that added graceful shutdown and Go module updates\n- [prometheus-plex-exporter](https://github.com/timothystewart6/prometheus-plex-exporter)\n  by [@timothystewart6](https://github.com/timothystewart6) — the\n  actively maintained upstream with multi-package architecture,\n  transcode tracking, and configurable library refresh\n- [Plex Media Server API](https://developer.plex.tv/pms/) — the\n  official API documentation\n- [coder/websocket](https://github.com/coder/websocket) — Go\n  WebSocket implementation\n- [prometheus/client_golang](https://github.com/prometheus/client_golang)\n  — Prometheus instrumentation library for Go\n\n## Contributing\n\nIssues and pull requests are welcome. Please open an issue first for\nlarger changes so the approach can be discussed before implementation.\n\n## Disclaimer\n\nThese 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.\n\nThis project was built with AI-assisted tooling using [Claude Opus](https://www.anthropic.com/claude) and [Kiro](https://kiro.dev). The human maintainer defines architecture, supervises implementation, and makes all final decisions.\n\n## License\n\nThis project is licensed under the [GNU General Public License v3.0](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcplieger%2Fplex-exporter","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcplieger%2Fplex-exporter","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcplieger%2Fplex-exporter/lists"}