contrib-munin/plugins/icecast/icecast2_stats_

183 lines
6.6 KiB
Python
Executable File

#!/usr/bin/python3
#
# This plugin shows the statistics of every source currently connected to the Icecast2 server.
# See the Icecast2_ plugin for collecting data of specific mountpoints.
#
# An icecast server v2.4 or later is required for this module since it uses the status-json.xsl
# output (see http://www.icecast.org/docs/icecast-2.4.1/server-stats.html).
#
# The following data for each source is available:
# * listeners: current count of listeners
# * duration: the age of the stream/source
#
# Additionally the Icecast service uptime is available.
#
# This plugin requires Python 3 (e.g. for urllib instead of urllib2).
#
#
# Environment variables:
# * status_url: defaults to "http://localhost:8000/status-json.xsl"
#
#
# Copyright (C) 2015 Lars Kruse <devel@sumpfralle.de>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
#
# Magic markers
#%# capabilities=autoconf suggest
#%# family=auto
import datetime
import json
import os
import urllib.request
import sys
status_url = os.getenv("status_url", "http://localhost:8000/status-json.xsl")
PLUGIN_SCOPES = ("sources_listeners", "sources_duration", "service_uptime")
PLUGIN_NAME_PREFIX = "icecast2_stats_"
def clean_fieldname(name):
""" see http://munin-monitoring.org/wiki/notes_on_datasource_names
This function is a bit clumsy as it tries to avoid using a regular
expression for the sake of micropython compatibility.
"""
def get_valid(char, position):
if char == '_':
return '_'
elif 'a' <= char.lower() <= 'z':
return char
elif (position > 0) and ('0' <= char <= '9'):
return char
else:
return '_'
return "".join([get_valid(char, position) for position, char in enumerate(name)])
def parse_iso8601(datestring):
""" try to avoid using an external library for parsing an ISO8601 date string """
if datestring.endswith("Z"):
timestamp_string = datestring[:-1]
time_delta = datetime.timedelta(minutes=0)
else:
# the "offset_text" is something like "+0500" or "-0130"
timestamp_string, offset_text = datestring[:-5], datestring[-5:]
offset_minutes = int(offset_text[1:3]) * 60 + int(offset_text[3:])
if offset_text.startswith("+"):
pass
elif offset_text.startswith("-"):
offset_minutes *= -1
else:
# invalid format
return None
time_delta = datetime.timedelta(minutes=offset_minutes)
local_time = datetime.datetime.strptime(timestamp_string, "%Y-%m-%dT%H:%M:%S")
return local_time + time_delta
def get_iso8601_age_days(datestring):
now = datetime.datetime.now()
timestamp = parse_iso8601(datestring)
if timestamp:
return (now - timestamp).total_seconds() / (24 * 60 * 60)
else:
return None
def _get_json_statistics():
with urllib.request.urlopen(status_url) as conn:
json_body = conn.read()
return json.loads(json_body.decode("utf-8"))
def get_sources():
sources = []
for source in _get_json_statistics()["icestats"]["source"]:
path_name = source["listenurl"].split("/")[-1]
sources.append({"name": path_name,
"fieldname": clean_fieldname(path_name),
"listeners": source["listeners"],
"duration_days": get_iso8601_age_days(source["stream_start_iso8601"])})
sources.sort(key=lambda item: item["name"])
return sources
def get_server_uptime_days():
return get_iso8601_age_days(_get_json_statistics()["icestats"]["server_start_iso8601"])
def get_scope():
called_name = os.path.basename(sys.argv[0])
if called_name.startswith(PLUGIN_NAME_PREFIX):
scope = called_name[len(PLUGIN_NAME_PREFIX):]
if not scope in PLUGIN_SCOPES:
print("Invalid scope requested: {0} (expected: {1})".format(scope, "/".join(PLUGIN_SCOPES)), file=sys.stderr)
sys.exit(2)
else:
print("Invalid filename - failed to discover plugin scope", file=sys.stderr)
sys.exit(2)
return scope
if __name__ == "__main__":
action = sys.argv[1] if (len(sys.argv) > 1) else None
if action == "autoconf":
try:
get_sources()
print("yes")
except OSError:
print("no")
elif action == "suggest":
for scope in PLUGIN_SCOPES:
print(scope)
elif action == "config":
scope = get_scope()
if scope == "sources_listeners":
print("graph_title Total number of listeners")
print("graph_vlabel listeners")
print("graph_category streaming")
for index, source in enumerate(get_sources()):
print("{0}.label {1}".format(source["fieldname"], source["name"]))
print("{0}.draw {1}".format(source["fieldname"], ("AREA" if (index == 0) else "STACK")))
elif scope == "sources_duration":
print("graph_title Duration of sources")
print("graph_vlabel duration in days")
print("graph_category streaming")
for source in get_sources():
print("{0}.label {1}".format(source["fieldname"], source["name"]))
elif scope == "service_uptime":
print("graph_title Icecast service uptime")
print("graph_vlabel uptime in days")
print("graph_category streaming")
print("uptime.label service uptime")
elif action is None:
scope = get_scope()
if scope == "sources_listeners":
for source in get_sources():
print("{0}.value {1}".format(source["fieldname"], source["listeners"]))
elif scope == "sources_duration":
for source in get_sources():
print("{0}.value {1}".format(source["fieldname"], source["duration_days"] or 0))
elif scope == "service_uptime":
print("uptime.value {0}".format(get_server_uptime_days()))
else:
print("Invalid argument given: {0}".format(action), file=sys.stderr)
sys.exit(1)
sys.exit(0)