Last active
October 5, 2025 03:21
-
-
Save dfsnow/aad4ec99afb413968c49efb03bdb1ab9 to your computer and use it in GitHub Desktop.
Export Jellyfin playback statistics to Prometheus and Grafana. See https://sno.ws/jellyfin-stats for more info
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| modules: | |
| jellyfin: | |
| headers: | |
| # The Token value here needs to be an API key generated from the | |
| # Jellyfin admin panel. It's hard-coded here but I'm sure there's | |
| # a better way | |
| Authorization: MediaBrowser Token=ADD_TOKEN_HERE | |
| Content-Type: application/json | |
| accept: application/json | |
| # This will return all active sessions regardless of | |
| # whether something is playing. You can use a combination | |
| # of label and value filters in Grafana to only get actively | |
| # playing sessions | |
| metrics: | |
| - name: jellyfin | |
| type: object | |
| help: User playback metrics from Jellyfin | |
| # Only look at sessions with a NowPlayingItem key, per @abe-kanofsky | |
| path: '{ [?(@.NowPlayingItem)] }' | |
| labels: | |
| user_name: '{ .UserName }' | |
| # User PromQL label_join and label_replace to concatenate | |
| # these values into a nice item description | |
| item_type: '{ .NowPlayingItem.Type }' | |
| item_name: '{ .NowPlayingItem.Name }' | |
| item_path: '{ .NowPlayingItem.Path }' | |
| series_name: '{ .NowPlayingItem.SeriesName }' | |
| episode_index: 'e{ .NowPlayingItem.IndexNumber }' | |
| season_index: 's{ .NowPlayingItem.ParentIndexNumber }' | |
| client_name: '{ .Client }' | |
| device_name: '{ .DeviceName }' | |
| # Include the unique session ID in case the above info isn't unique | |
| session_id: '{ .Id }' | |
| values: | |
| is_paused: '{ .PlayState.IsPaused }' |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| scrape_configs: | |
| - job_name: json | |
| metrics_path: /probe | |
| params: | |
| # The name of the module defined by json-exporter-config.yaml | |
| module: [jellyfin] | |
| static_configs: | |
| - targets: | |
| # Use the Sessions endpoint to see actively playing items | |
| - https://jellyfin.example.com/Sessions | |
| relabel_configs: | |
| - source_labels: [__address__] | |
| target_label: __param_target | |
| - source_labels: [__param_target] | |
| target_label: instance | |
| - target_label: __address__ | |
| replacement: HOSTNAME:9115 # The exporter's hostname:port |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "datasource": { | |
| "type": "prometheus", | |
| "uid": "D5GQA944k" | |
| }, | |
| "description": "", | |
| "fieldConfig": { | |
| "defaults": { | |
| "custom": { | |
| "lineWidth": 0, | |
| "fillOpacity": 70, | |
| "spanNulls": false, | |
| "insertNulls": false, | |
| "hideFrom": { | |
| "tooltip": false, | |
| "viz": false, | |
| "legend": false | |
| } | |
| }, | |
| "color": { | |
| "mode": "continuous-GrYlRd" | |
| }, | |
| "mappings": [], | |
| "thresholds": { | |
| "mode": "absolute", | |
| "steps": [ | |
| { | |
| "color": "green", | |
| "value": null | |
| }, | |
| { | |
| "color": "red", | |
| "value": 80 | |
| } | |
| ] | |
| } | |
| }, | |
| "overrides": [] | |
| }, | |
| "gridPos": { | |
| "h": 6, | |
| "w": 13, | |
| "x": 0, | |
| "y": 9 | |
| }, | |
| "id": 348, | |
| "options": { | |
| "mergeValues": true, | |
| "showValue": "auto", | |
| "alignValue": "left", | |
| "rowHeight": 0.9, | |
| "legend": { | |
| "showLegend": true, | |
| "displayMode": "list", | |
| "placement": "bottom" | |
| }, | |
| "tooltip": { | |
| "mode": "single", | |
| "sort": "none" | |
| } | |
| }, | |
| "pluginVersion": "10.1.1", | |
| "targets": [ | |
| { | |
| "datasource": { | |
| "type": "prometheus", | |
| "uid": "D5GQA944k" | |
| }, | |
| "editorMode": "code", | |
| "expr": "sum by (user_name, item_index_clean, item_type) (label_replace(label_join(label_join(jellyfin_is_paused{job=\"json\"}, \"item_index\", \"\", \"season_index\", \"episode_index\"), \"full_name\", \" - \", \"series_name\", \"item_index\", \"item_name\"), \"item_index_clean\", \"$1\", \"full_name\", \"^[- ]*(.*?)[- ]*$\"))", | |
| "instant": false, | |
| "legendFormat": "__auto", | |
| "range": true, | |
| "refId": "A" | |
| } | |
| ], | |
| "title": "Jellyfin", | |
| "transformations": [ | |
| { | |
| "id": "seriesToRows", | |
| "options": {} | |
| }, | |
| { | |
| "id": "extractFields", | |
| "options": { | |
| "keepTime": false, | |
| "replace": false, | |
| "source": "Metric" | |
| } | |
| }, | |
| { | |
| "id": "filterByValue", | |
| "options": { | |
| "filters": [ | |
| { | |
| "config": { | |
| "id": "isNotNull", | |
| "options": {} | |
| }, | |
| "fieldName": "user_name" | |
| } | |
| ], | |
| "match": "any", | |
| "type": "include" | |
| } | |
| }, | |
| { | |
| "id": "calculateField", | |
| "options": { | |
| "alias": "Item", | |
| "mode": "reduceRow", | |
| "reduce": { | |
| "include": [ | |
| "item_index_clean" | |
| ], | |
| "reducer": "firstNotNull" | |
| } | |
| } | |
| }, | |
| { | |
| "id": "calculateField", | |
| "options": { | |
| "alias": "Name", | |
| "mode": "reduceRow", | |
| "reduce": { | |
| "include": [ | |
| "user_name" | |
| ], | |
| "reducer": "firstNotNull" | |
| } | |
| } | |
| }, | |
| { | |
| "id": "calculateField", | |
| "options": { | |
| "alias": "Temp", | |
| "mode": "reduceRow", | |
| "reduce": { | |
| "include": [ | |
| "Item" | |
| ], | |
| "reducer": "firstNotNull" | |
| }, | |
| "replaceFields": false | |
| } | |
| }, | |
| { | |
| "id": "filterByValue", | |
| "options": { | |
| "filters": [ | |
| { | |
| "config": { | |
| "id": "regex", | |
| "options": { | |
| "value": "(Movie|Episode)" | |
| } | |
| }, | |
| "fieldName": "item_type" | |
| } | |
| ], | |
| "match": "any", | |
| "type": "include" | |
| } | |
| }, | |
| { | |
| "id": "filterByValue", | |
| "options": { | |
| "filters": [ | |
| { | |
| "config": { | |
| "id": "notEqual", | |
| "options": { | |
| "value": 1 | |
| } | |
| }, | |
| "fieldName": "Value" | |
| } | |
| ], | |
| "match": "any", | |
| "type": "include" | |
| } | |
| }, | |
| { | |
| "id": "partitionByValues", | |
| "options": { | |
| "fields": [ | |
| "Name" | |
| ] | |
| } | |
| }, | |
| { | |
| "id": "filterFieldsByName", | |
| "options": { | |
| "include": { | |
| "pattern": "Temp|Time (.*)" | |
| } | |
| } | |
| }, | |
| { | |
| "id": "renameByRegex", | |
| "options": { | |
| "regex": "Temp (.*)", | |
| "renamePattern": "$1" | |
| } | |
| } | |
| ], | |
| "type": "state-timeline" | |
| } |
Another thing: modules.jellyfin.metrics[0].path should be '{ [?(@.NowPlayingItem)] } to look at only sessions that actually have a NowPlayingItem key. Otherwise, the exporter also tries to read sessions without that key and spits out a pretty verbose error each time it encounters one, causing the log file to blow up.
Author
Nice @abe-kanofsky, these are excellent suggestions! I'll update the gist and blogpost to reflect them.
Hi, thanks a lot for the code and the blog post. Great Work!
Unfortunately i couldn't work the Grafana dashboard is there any update on it ?
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This is super useful stuff!
Just wanna mention an issue I ran into with this setup. It seems that it's possible for Jellyfin to report a user as having more than one session with the same client, device, and
NowPlayingItem. When this happens, the JSON exporter doesn't know what to do because it has no way to differentiate the two session JSON objects given the labels it's configured to look at. This causes all requests to theprobeendpoint to return an error like this:I fixed this by adding
session_id: '{ .Id }'to themodules.jellyfin.metrics[0].labelsobject in the JSON exporter config which is enough to allow it to defferentiate between the different sessions, sinceIdis always unique even when the user, client, device, andNowPlayingItemare the same. After making this change, I resarted the exporter and it worked normally again.