Skip to content

Instantly share code, notes, and snippets.

@dfsnow
Last active April 27, 2024 00:39
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save dfsnow/aad4ec99afb413968c49efb03bdb1ab9 to your computer and use it in GitHub Desktop.
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
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
path: '{ [*] }'
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 }'
values:
is_paused: '{ .PlayState.IsPaused }'
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
{
"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