Last active
March 16, 2025 01:56
-
-
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" | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Nice @abe-kanofsky, these are excellent suggestions! I'll update the gist and blogpost to reflect them.