mirror of
https://github.com/munin-monitoring/contrib.git
synced 2018-11-08 00:59:34 +01:00
6243541d65
This icecast plugin captures a set of data similar to the "icecast2_" and "icecast2_all" plugin. Sadly both are not configurable (manual changes are required for target host and access credentials). Additionally they use the rather old-fashioned (and password restricted) xml status output. This new plugin "icecast2_stats" (written from scratch) contains the following notable differences: * runs with Python 3 * supports a real "autoconf" and "suggest" interface * is configurable (target host and port) via environment settings * uses the json status output instead of the xml data * contains no hard-coded values, names or patterns
183 lines
6.6 KiB
Python
Executable File
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 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)
|