diff --git a/plugins/streaming/icecast2_stats_ b/plugins/streaming/icecast2_stats_ new file mode 100755 index 00000000..ea6a159c --- /dev/null +++ b/plugins/streaming/icecast2_stats_ @@ -0,0 +1,182 @@ +#!/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 +# +# 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 . +# +# +# 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 Icecast") + 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 Icecast") + 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 Icecast") + 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)