diff --git a/ansible/playbooks/indri.yml b/ansible/playbooks/indri.yml index 7d14d92..b6f8086 100644 --- a/ansible/playbooks/indri.yml +++ b/ansible/playbooks/indri.yml @@ -24,3 +24,5 @@ tags: devpi - role: devpi_metrics tags: devpi_metrics + - role: plex_metrics + tags: plex_metrics diff --git a/ansible/roles/alloy/defaults/main.yml b/ansible/roles/alloy/defaults/main.yml index d4b83a2..a81d0ef 100644 --- a/ansible/roles/alloy/defaults/main.yml +++ b/ansible/roles/alloy/defaults/main.yml @@ -61,5 +61,10 @@ alloy_mcquack_logs: service: borgmatic stream: stderr +alloy_plex_logs: + - path: /Users/erichblume/Library/Logs/Plex Media Server/Plex Media Server.log + service: plex + stream: stdout + # Enable log collection (requires Loki to be running) alloy_collect_logs: true diff --git a/ansible/roles/alloy/templates/config.alloy.j2 b/ansible/roles/alloy/templates/config.alloy.j2 index a830533..069d8d4 100644 --- a/ansible/roles/alloy/templates/config.alloy.j2 +++ b/ansible/roles/alloy/templates/config.alloy.j2 @@ -56,6 +56,15 @@ local.file_match "mcquack_logs" { ] } +// Discover log files - Plex Media Server +local.file_match "plex_logs" { + path_targets = [ +{% for log in alloy_plex_logs %} + {__path__ = "{{ log.path }}", service = "{{ log.service }}", stream = "{{ log.stream }}"}, +{% endfor %} + ] +} + // Read and forward brew service logs loki.source.file "brew_logs" { targets = local.file_match.brew_logs.targets @@ -68,6 +77,12 @@ loki.source.file "mcquack_logs" { forward_to = [loki.relabel.add_host.receiver] } +// Read and forward Plex logs +loki.source.file "plex_logs" { + targets = local.file_match.plex_logs.targets + forward_to = [loki.relabel.add_host.receiver] +} + // Add host label to all logs loki.relabel "add_host" { forward_to = [loki.write.loki.receiver] diff --git a/ansible/roles/grafana/files/dashboards/plex.json b/ansible/roles/grafana/files/dashboards/plex.json new file mode 100644 index 0000000..9cd8fc5 --- /dev/null +++ b/ansible/roles/grafana/files/dashboards/plex.json @@ -0,0 +1,679 @@ +{ + "annotations": { + "list": [] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": null, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [ + {"options": {"0": {"color": "red", "index": 0, "text": "DOWN"}}, "type": "value"}, + {"options": {"1": {"color": "green", "index": 1, "text": "UP"}}, "type": "value"} + ], + "thresholds": { + "mode": "absolute", + "steps": [ + {"color": "red", "value": null}, + {"color": "green", "value": 1} + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": {"h": 4, "w": 4, "x": 0, "y": 0}, + "id": 1, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": {"type": "prometheus", "uid": "prometheus"}, + "expr": "plex_up", + "refId": "A" + } + ], + "title": "Plex Status", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + {"color": "blue", "value": null} + ] + }, + "unit": "string" + }, + "overrides": [] + }, + "gridPos": {"h": 4, "w": 6, "x": 4, "y": 0}, + "id": 2, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "/^version$/", + "values": false + }, + "textMode": "value" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": {"type": "prometheus", "uid": "prometheus"}, + "expr": "plex_version_info", + "format": "table", + "instant": true, + "refId": "A" + } + ], + "title": "Version", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + {"color": "green", "value": null}, + {"color": "yellow", "value": 3}, + {"color": "orange", "value": 5} + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": {"h": 4, "w": 4, "x": 10, "y": 0}, + "id": 3, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": {"type": "prometheus", "uid": "prometheus"}, + "expr": "plex_sessions_total", + "refId": "A" + } + ], + "title": "Active Sessions", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + {"color": "green", "value": null}, + {"color": "yellow", "value": 1}, + {"color": "orange", "value": 2} + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": {"h": 4, "w": 4, "x": 14, "y": 0}, + "id": 4, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": {"type": "prometheus", "uid": "prometheus"}, + "expr": "plex_transcode_sessions_total", + "refId": "A" + } + ], + "title": "Transcoding", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + {"color": "purple", "value": null} + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": {"h": 4, "w": 6, "x": 18, "y": 0}, + "id": 5, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": {"type": "prometheus", "uid": "prometheus"}, + "expr": "count(plex_library_items)", + "refId": "A" + } + ], + "title": "Libraries", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "blue", + "mode": "fixed" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + {"color": "blue", "value": null} + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": {"h": 4, "w": 6, "x": 0, "y": 4}, + "id": 6, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": {"type": "prometheus", "uid": "prometheus"}, + "expr": "sum(plex_library_items{type=\"movie\"})", + "refId": "A" + } + ], + "title": "Movies", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "green", + "mode": "fixed" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + {"color": "green", "value": null} + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": {"h": 4, "w": 6, "x": 6, "y": 4}, + "id": 7, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": {"type": "prometheus", "uid": "prometheus"}, + "expr": "sum(plex_library_items{type=\"show\"})", + "refId": "A" + } + ], + "title": "TV Shows", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "orange", + "mode": "fixed" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + {"color": "orange", "value": null} + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": {"h": 4, "w": 6, "x": 12, "y": 4}, + "id": 8, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": {"type": "prometheus", "uid": "prometheus"}, + "expr": "sum(plex_library_items{type=\"artist\"})", + "refId": "A" + } + ], + "title": "Music Artists", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "purple", + "mode": "fixed" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + {"color": "purple", "value": null} + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": {"h": 4, "w": 6, "x": 18, "y": 4}, + "id": 9, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": {"type": "prometheus", "uid": "prometheus"}, + "expr": "sum(plex_library_items{type=\"photo\"})", + "refId": "A" + } + ], + "title": "Photos", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + {"color": "green", "value": null} + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": {"id": "byName", "options": "Playing"}, + "properties": [ + {"id": "color", "value": {"fixedColor": "green", "mode": "fixed"}} + ] + }, + { + "matcher": {"id": "byName", "options": "Paused"}, + "properties": [ + {"id": "color", "value": {"fixedColor": "yellow", "mode": "fixed"}} + ] + } + ] + }, + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 8}, + "id": 10, + "options": { + "legend": { + "calcs": ["mean", "max"], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": {"type": "prometheus", "uid": "prometheus"}, + "expr": "plex_sessions_playing", + "legendFormat": "Playing", + "refId": "A" + }, + { + "datasource": {"type": "prometheus", "uid": "prometheus"}, + "expr": "plex_sessions_paused", + "legendFormat": "Paused", + "refId": "B" + } + ], + "title": "Sessions Over Time", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + {"color": "green", "value": null} + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": {"id": "byName", "options": "Video Transcode"}, + "properties": [ + {"id": "color", "value": {"fixedColor": "red", "mode": "fixed"}} + ] + }, + { + "matcher": {"id": "byName", "options": "Audio Transcode"}, + "properties": [ + {"id": "color", "value": {"fixedColor": "orange", "mode": "fixed"}} + ] + } + ] + }, + "gridPos": {"h": 8, "w": 12, "x": 12, "y": 8}, + "id": 11, + "options": { + "legend": { + "calcs": ["mean", "max"], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": {"type": "prometheus", "uid": "prometheus"}, + "expr": "plex_transcode_video_sessions", + "legendFormat": "Video Transcode", + "refId": "A" + }, + { + "datasource": {"type": "prometheus", "uid": "prometheus"}, + "expr": "plex_transcode_audio_sessions", + "legendFormat": "Audio Transcode", + "refId": "B" + } + ], + "title": "Transcode Sessions", + "type": "timeseries" + }, + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "gridPos": {"h": 8, "w": 24, "x": 0, "y": 16}, + "id": 12, + "options": { + "dedupStrategy": "none", + "enableLogDetails": true, + "prettifyLogMessage": false, + "showCommonLabels": false, + "showLabels": false, + "showTime": true, + "sortOrder": "Descending", + "wrapLogMessage": false + }, + "targets": [ + { + "datasource": {"type": "loki", "uid": "loki"}, + "expr": "{service=\"plex\"}", + "refId": "A" + } + ], + "title": "Plex Logs", + "type": "logs" + } + ], + "refresh": "30s", + "schemaVersion": 38, + "tags": ["plex", "media"], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Plex Media Server", + "uid": "plex", + "version": 1, + "weekStart": "" +} diff --git a/ansible/roles/plex_metrics/defaults/main.yml b/ansible/roles/plex_metrics/defaults/main.yml new file mode 100644 index 0000000..667f641 --- /dev/null +++ b/ansible/roles/plex_metrics/defaults/main.yml @@ -0,0 +1,20 @@ +--- +# Plex metrics collection configuration + +# Plex server URL +plex_url: "http://localhost:32400" + +# Path to file containing Plex token (should have 600 permissions) +plex_token_file: "/Users/erichblume/.plex-token" + +# Metrics collection interval in seconds +plex_metrics_interval: 60 + +# Output directory for prometheus textfile collector +plex_metrics_dir: /opt/homebrew/var/node_exporter/textfile + +# Script installation path +plex_metrics_script: /Users/erichblume/bin/plex-metrics + +# Log directory for metrics script output +plex_metrics_log_dir: /opt/homebrew/var/log diff --git a/ansible/roles/plex_metrics/handlers/main.yml b/ansible/roles/plex_metrics/handlers/main.yml new file mode 100644 index 0000000..128ecad --- /dev/null +++ b/ansible/roles/plex_metrics/handlers/main.yml @@ -0,0 +1,5 @@ +--- +- name: reload plex-metrics + ansible.builtin.shell: | + launchctl unload ~/Library/LaunchAgents/mcquack.eblume.plex-metrics.plist 2>/dev/null || true + launchctl load ~/Library/LaunchAgents/mcquack.eblume.plex-metrics.plist diff --git a/ansible/roles/plex_metrics/meta/main.yml b/ansible/roles/plex_metrics/meta/main.yml new file mode 100644 index 0000000..2925213 --- /dev/null +++ b/ansible/roles/plex_metrics/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: + - role: alloy diff --git a/ansible/roles/plex_metrics/tasks/main.yml b/ansible/roles/plex_metrics/tasks/main.yml new file mode 100644 index 0000000..ea4be15 --- /dev/null +++ b/ansible/roles/plex_metrics/tasks/main.yml @@ -0,0 +1,31 @@ +--- +- name: Ensure bin directory exists + ansible.builtin.file: + path: "{{ plex_metrics_script | dirname }}" + state: directory + mode: '0755' + +- name: Deploy plex metrics collection script + ansible.builtin.template: + src: plex-metrics.sh.j2 + dest: "{{ plex_metrics_script }}" + mode: '0755' + notify: reload plex-metrics + +- name: Deploy plex-metrics LaunchAgent plist + ansible.builtin.template: + src: plex-metrics.plist.j2 + dest: ~/Library/LaunchAgents/mcquack.eblume.plex-metrics.plist + mode: '0644' + notify: reload plex-metrics + +- name: Check if plex-metrics LaunchAgent is loaded + ansible.builtin.command: launchctl list mcquack.eblume.plex-metrics + register: launchctl_check + changed_when: false + failed_when: false + +- name: Load plex-metrics LaunchAgent if not loaded + ansible.builtin.command: launchctl load ~/Library/LaunchAgents/mcquack.eblume.plex-metrics.plist + when: launchctl_check.rc != 0 + failed_when: false diff --git a/ansible/roles/plex_metrics/templates/plex-metrics.plist.j2 b/ansible/roles/plex_metrics/templates/plex-metrics.plist.j2 new file mode 100644 index 0000000..836bee1 --- /dev/null +++ b/ansible/roles/plex_metrics/templates/plex-metrics.plist.j2 @@ -0,0 +1,26 @@ + + + + + + Label + mcquack.eblume.plex-metrics + EnvironmentVariables + + PATH + /opt/homebrew/bin:/usr/bin:/bin + + ProgramArguments + + {{ plex_metrics_script }} + + StartInterval + {{ plex_metrics_interval }} + RunAtLoad + + StandardErrorPath + {{ plex_metrics_log_dir }}/plex-metrics.err.log + StandardOutPath + {{ plex_metrics_log_dir }}/plex-metrics.out.log + + diff --git a/ansible/roles/plex_metrics/templates/plex-metrics.sh.j2 b/ansible/roles/plex_metrics/templates/plex-metrics.sh.j2 new file mode 100644 index 0000000..e2f8b2e --- /dev/null +++ b/ansible/roles/plex_metrics/templates/plex-metrics.sh.j2 @@ -0,0 +1,133 @@ +#!/bin/bash +# {{ ansible_managed }} +# Collects Plex Media Server metrics for node_exporter textfile collector + +set -euo pipefail + +PLEX_URL="{{ plex_url }}" +TOKEN_FILE="{{ plex_token_file }}" +OUTPUT_FILE="{{ plex_metrics_dir }}/plex.prom" +TEMP_FILE="${OUTPUT_FILE}.tmp" + +# Read token from file +get_token() { + if [ -f "$TOKEN_FILE" ]; then + cat "$TOKEN_FILE" | tr -d '\n' + else + echo "" + fi +} + +# Make API request with optional token +api_request() { + local endpoint="$1" + local use_token="${2:-true}" + local token + local url="${PLEX_URL}${endpoint}" + + if [ "$use_token" = "true" ]; then + token=$(get_token) + if [ -n "$token" ]; then + curl -s -H "Accept: application/json" -H "X-Plex-Token: $token" "$url" 2>/dev/null + else + curl -s -H "Accept: application/json" "$url" 2>/dev/null + fi + else + curl -s -H "Accept: application/json" "$url" 2>/dev/null + fi +} + +# Initialize metrics +plex_up=0 +plex_version="" +plex_sessions_total=0 +plex_sessions_playing=0 +plex_sessions_paused=0 +plex_transcode_sessions_total=0 +plex_transcode_video=0 +plex_transcode_audio=0 + +# Library metrics will be built dynamically +library_metrics="" + +# Check server identity (no auth required) +identity=$(api_request "/identity" false) +if echo "$identity" | jq -e '.MediaContainer.machineIdentifier' > /dev/null 2>&1; then + plex_up=1 + plex_version=$(echo "$identity" | jq -r '.MediaContainer.version // ""') +fi + +# If server is up, get additional metrics (require auth) +if [ "$plex_up" -eq 1 ] && [ -f "$TOKEN_FILE" ]; then + # Get library sections + sections=$(api_request "/library/sections") + + # Process each library using jq + while IFS=$'\t' read -r lib_key lib_type lib_title; do + if [ -n "$lib_key" ] && [ -n "$lib_type" ]; then + # Get library details for item count + lib_detail=$(api_request "/library/sections/${lib_key}/all?X-Plex-Container-Start=0&X-Plex-Container-Size=0") + lib_size=$(echo "$lib_detail" | jq -r '.MediaContainer.totalSize // .MediaContainer.size // 0') + + library_metrics="${library_metrics}plex_library_items{library=\"${lib_title}\",type=\"${lib_type}\"} ${lib_size} +" + fi + done < <(echo "$sections" | jq -r '.MediaContainer.Directory[] | [.key, .type, .title] | @tsv' 2>/dev/null || true) + + # Get active sessions + sessions=$(api_request "/status/sessions") + if echo "$sessions" | jq -e '.MediaContainer' > /dev/null 2>&1; then + plex_sessions_total=$(echo "$sessions" | jq -r '.MediaContainer.size // 0') + + # Count playing vs paused + plex_sessions_playing=$(echo "$sessions" | jq -r '[.MediaContainer.Metadata[]? | select(.Player.state == "playing")] | length') + plex_sessions_paused=$(echo "$sessions" | jq -r '[.MediaContainer.Metadata[]? | select(.Player.state == "paused")] | length') + + # Count transcode sessions + plex_transcode_video=$(echo "$sessions" | jq -r '[.MediaContainer.Metadata[]? | select(.TranscodeSession.videoDecision == "transcode")] | length') + plex_transcode_audio=$(echo "$sessions" | jq -r '[.MediaContainer.Metadata[]? | select(.TranscodeSession.audioDecision == "transcode")] | length') + plex_transcode_sessions_total=$(echo "$sessions" | jq -r '[.MediaContainer.Metadata[]? | select(.TranscodeSession)] | length') + fi +fi + +# Write metrics +cat > "$TEMP_FILE" << EOF +# HELP plex_up Plex Media Server is up and responding +# TYPE plex_up gauge +plex_up ${plex_up} + +# HELP plex_version_info Plex Media Server version information +# TYPE plex_version_info gauge +plex_version_info{version="${plex_version}"} 1 + +# HELP plex_sessions_total Total number of active Plex sessions +# TYPE plex_sessions_total gauge +plex_sessions_total ${plex_sessions_total} + +# HELP plex_sessions_playing Number of sessions currently playing +# TYPE plex_sessions_playing gauge +plex_sessions_playing ${plex_sessions_playing} + +# HELP plex_sessions_paused Number of sessions currently paused +# TYPE plex_sessions_paused gauge +plex_sessions_paused ${plex_sessions_paused} + +# HELP plex_transcode_sessions_total Number of sessions being transcoded +# TYPE plex_transcode_sessions_total gauge +plex_transcode_sessions_total ${plex_transcode_sessions_total} + +# HELP plex_transcode_video_sessions Number of sessions transcoding video +# TYPE plex_transcode_video_sessions gauge +plex_transcode_video_sessions ${plex_transcode_video} + +# HELP plex_transcode_audio_sessions Number of sessions transcoding audio +# TYPE plex_transcode_audio_sessions gauge +plex_transcode_audio_sessions ${plex_transcode_audio} + +# HELP plex_library_items Number of items in each Plex library +# TYPE plex_library_items gauge +${library_metrics} +EOF + +# Atomic move +mv "$TEMP_FILE" "$OUTPUT_FILE"