Compare commits

...

3 Commits

13 changed files with 3221 additions and 3069 deletions

View File

@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "GULIK" "1" "Oct 06, 2018" "0.0.0.1" "gulik"
.TH "GULIK" "1" "Feb 01, 2019" "0.0.0.2" "gulik"
.SH NAME
gulik \- gulik Documentation
.
@ -168,13 +168,13 @@ interpreter.
.IP \(bu 2
\fBNETDATA_HOSTS\fP (\fBlist\fP): \fI\%netdata\fP hosts to connect to. Hosts as hostnames or \fB(host, port)\fP tuples. Default value: \fB[]\fP
.IP \(bu 2
\fBNETDATA_RETRY\fP (\fBint\fP or \fBfloat\fP): How long a defective \fI\%NetdataMonitor\fP will wait before retrying to contact the \fBnetdata\fP server, in seconds. Default value: \fB5\fP
\fBNETDATA_RETRY\fP (\fBint\fP or \fBfloat\fP): How long a defective \fBNetdataMonitor\fP will wait before retrying to contact the \fBnetdata\fP server, in seconds. Default value: \fB5\fP
.IP \(bu 2
\fBBSD_ACCURATE_MEMORY\fP (\fBbool\fP): Use accurate but expensive memory data collection on BSD. Default value: \fBFalse\fP
.IP \(bu 2
\fBMARGIN\fP (\fBint\fP or \fBfloat\fP): Margin around all \fI\%Visualizer\fPs. Default value: \fB5\fP
\fBMARGIN\fP (\fBint\fP or \fBfloat\fP): Margin around all \fBVisualizer\fPs. Default value: \fB5\fP
.IP \(bu 2
\fBPADDING\fP (\fBint\fP or \fBfloat\fP): Padding around all \fI\%Visualizer\fPs. Default value: \fB5\fP
\fBPADDING\fP (\fBint\fP or \fBfloat\fP): Padding around all \fBVisualizer\fPs. Default value: \fB5\fP
.IP \(bu 2
\fBFONT\fP (\fBstr\fP): Font family to use in captions, legends and the like. Default value: \fB"Orbitron"\fP
.IP \(bu 2
@ -182,33 +182,33 @@ interpreter.
.IP \(bu 2
\fBFONT_SIZE\fP (\fBint\fP or \fBfloat\fP): Font size in pixels. Default value: \fB10\fP
.IP \(bu 2
\fBCOLOR_WINDOW_BACKGROUND\fP (\fI\%Color\fP): Background color of the window. Default value: \fBColor(0.05, 0.05, 0.05, 0.8)\fP
\fBCOLOR_WINDOW_BACKGROUND\fP (\fBColor\fP): Background color of the window. Default value: \fBColor(0.05, 0.05, 0.05, 0.8)\fP
.IP \(bu 2
\fBCOLOR_BACKGROUND\fP (\fI\%Color\fP): Background color for \fI\%Visualizer\fPs. Default value: \fBColor(1,1,1, 0.1)\fP
\fBCOLOR_BACKGROUND\fP (\fBColor\fP): Background color for \fBVisualizer\fPs. Default value: \fBColor(1,1,1, 0.1)\fP
.IP \(bu 2
\fBCOLOR_FOREGROUND\fP (\fI\%Color\fP): Foreground color. This is used as base color for most \fI\%palette\fPs. Default value: \fBColor(0.5, 1, 0, 0.6)\fP
\fBCOLOR_FOREGROUND\fP (\fBColor\fP): Foreground color. This is used as base color for most \fI\%palette\fPs. Default value: \fBColor(0.5, 1, 0, 0.6)\fP
.IP \(bu 2
\fBCOLOR_CAPTION\fP (\fI\%Color\fP): Text color for captions. Default value: \fBColor(1,1,1, 0.6)\fP
\fBCOLOR_CAPTION\fP (\fBColor\fP): Text color for captions. Default value: \fBColor(1,1,1, 0.6)\fP
.IP \(bu 2
\fBPALETTE\fP (\fBfunction\fP): The default \fI\%palette\fP generator. Default value: \fBfunctools.partial(\fP \fI\%palette_hue()\fP \fB, distance=\-120)\fP
\fBPALETTE\fP (\fBfunction\fP): The default \fI\%palette\fP generator. Default value: \fBfunctools.partial(\fP \fBpalette_hue()\fP \fB, distance=\-120)\fP
.IP \(bu 2
\fBPATTERN\fP (\fBfunction\fP): The default \fI\%pattern\fP generator. Default value: \fBstripe45\fP
\fBPATTERN\fP (\fBfunction\fP): The default \fI\%pattern\fP generator. Default value: \fBpatterns.stripe45\fP
.IP \(bu 2
\fBCAPTION_PLACEMENT\fP (\fBstr\fP): \fB"padding"\fP to have captions placed in the paddings of \fI\%Visualizer\fPs, \fB"inner"\fP to place them within the drawing region of the \fI\%Visualizer\fP\&. Default value: \fB"inner"\fP
\fBCAPTION_PLACEMENT\fP (\fBstr\fP): \fB"padding"\fP to have captions placed in the paddings of \fBVisualizer\fPs, \fB"inner"\fP to place them within the drawing region of the \fBVisualizer\fP\&. Default value: \fB"inner"\fP
.IP \(bu 2
\fBLEGEND\fP (\fBbool\fP): Whether \fI\%Visualizer\fPs should attempt automatically creating a legend for themselves in their bottom padding. Default value: \fBTrue\fP
\fBLEGEND\fP (\fBbool\fP): Whether \fBVisualizer\fPs should attempt automatically creating a legend for themselves in their bottom padding. Default value: \fBTrue\fP
.IP \(bu 2
\fBLEGEND_ORDER\fP (\fBstr\fP): Whether to reverse the legend order. Can be \fB"normal"\fP or \fB"reverse"\fP\&. Default value: \fB"normal"\fP
.IP \(bu 2
\fBLEGEND_SIZE\fP (\fBint\fP or \fBfloat\fP): Pixel height of one legend cell, including its own margin and padding. Legend font size is inferred from this. Default value: \fB20\fP
.IP \(bu 2
\fBLEGEND_PLACEMENT\fP (\fBstr\fP): Where to place legends within the \fI\%Visualizer\fPs drawing region. Can be \fB"inner"\fP or \fB"padding"\fP\&. Default value: \fB"padding"\fP
\fBLEGEND_PLACEMENT\fP (\fBstr\fP): Where to place legends within the \fBVisualizer\fPs drawing region. Can be \fB"inner"\fP or \fB"padding"\fP\&. Default value: \fB"padding"\fP
.IP \(bu 2
\fBLEGEND_MARGIN\fP (\fBint\fP or \fBfloat\fP): Margin around legends. Default value: \fB2.5\fP
.IP \(bu 2
\fBLEGEND_PADDING\fP: Padding around legends. Default value: \fB0\fP
.IP \(bu 2
\fBOPERATOR\fP: The blending operator used by \fI\%Visualizer\fPs. Default value: \fBOperator.OVER\fP
\fBOPERATOR\fP: The blending operator used by \fBVisualizer\fPs. Default value: \fBOperator.OVER\fP
.UNINDENT
.SS Explaining ALL THE ᴀʟʟ ᴛʜᴇ ᴄᴏɴꜰɪɢᴜʀᴀᴛɪᴏɴ ᴏᴘᴛɪᴏɴꜱ (ya rly)
.sp
@ -218,7 +218,7 @@ you can set, as \fBgulik\fP features a perversion of cascading styles.
Every single option except for \fBFPS\fP, \fBX\fP, \fBY\fP, \fBNETDATA_HOSTS\fP,
\fBNETDATA_RETRY\fP and \fBBSD_ACCURATE_MEMORY\fP can be overriden on a per\-class
basis by appending an underscore and the class name in uppercase to the
variable name. To disable legends on all \fI\%Plot\fPs for example, you
variable name. To disable legends on all \fBPlot\fPs for example, you
would use \fBLEGEND_PLOT = False\fP\&.
.sp
Additionally, \fBMARGIN\fP and \fBPADDING\fP can be set for each side by
@ -230,7 +230,7 @@ pattern \fI<name>_<class>_<subname>\fP, for example \fBPADDING_PLOT_RIGHT\fP or
.SS Custom setups
.sp
By default, gulik will run \fBGulik.autosetup()\fP to set up a reasonable
collection of \fI\%Visualizer\fPs that gives you a good overview of your
collection of \fBVisualizer\fPs that gives you a good overview of your
system but you can add your own \fBsetup\fP function to the configuration
file that will be used in stead of the autosetup.
.INDENT 0.0
@ -264,26 +264,26 @@ The name of the setup function \fImust\fP be \fBsetup\fP, otherwise \fBgulik\fP
The passed \fBapp\fP parameter is a \fI\%Gulik\fP object.
.TP
.B \fBbox = app.box()\fP
\fBGulik.box()\fP creates a \fI\%Box\fP, a little helper to make layouting
\fBGulik.box()\fP creates a \fBBox\fP, a little helper to make layouting
easier. You can limit its size via \fBwidth\fP and \fBheight\fP keyword parameters.
If these aren\(aqt supplied, the box will fill the whole window.
.TP
.B \fBbox.place(\fP
\fI\%Box.place()\fP places a new \fI\%visualizer\fP\&. \fI\%Box\fP orders
\fBBox.place()\fP places a new \fI\%visualizer\fP\&. \fBBox\fP orders
visualizers from left to right and top to bottom.
.TP
.B \fB\(aqcpu\(aq\fP
This is the monitored \fI\%component\fP we want to visualize \fI\%element\fPs
of. It is needed to look up the right \fI\%Monitor\fP object for
\fI\%Visualizer\fP instantiation.
of. It is needed to look up the right \fBMonitor\fP object for
\fBVisualizer\fP instantiation.
.TP
.B \fBgulik.Plot\fP
The \fI\%visualizer\fP class to be instantiated.
A subclass of \fI\%Visualizer\fP\&.
A subclass of \fBVisualizer\fP\&.
.UNINDENT
.sp
All keyword arguments below this are just passed on to the visualizer class,
so in this case, \fI\%Plot\fP\&. Here, these are:
so in this case, \fBPlot\fP\&. Here, these are:
.INDENT 0.0
.TP
.B \fBelements=[\(aqcore_0\(aq, \(aqcore_1\(aq],\fP
@ -299,20 +299,20 @@ because by default, \fBgulik\fP adds 40 pixel bottom padding to every
visualizer to allow 2 lines of legend.
.UNINDENT
.sp
\fI\%Box.place()\fP uses the \fBwidth\fP and \fBheight\fP keyword parameters
\fBBox.place()\fP uses the \fBwidth\fP and \fBheight\fP keyword parameters
(if passed) to make its layouting decisions. If they aren\(aqt passed, all
remaining space is used.
.sp
And with that, you hopefully know enough to get started with your custom
setup. You can consult \fI\%Monitor\fP and its subclasses to find out
\fIwhat\fP you can visualize and \fI\%Visualizer\fP and its subclasses to
setup. You can consult \fBMonitor\fP and its subclasses to find out
\fIwhat\fP you can visualize and \fBVisualizer\fP and its subclasses to
find out \fIhow\fP you can visualize that data.
.sp
If you want to extend the original setup instead of doing a completely
custom one, you can call \fBGulik.autosetup()\fP from your custom \fBsetup\fP
function and limit its area by using \fBwidth\fP and \fBheight\fP as well as
\fBx\fP and \fBy\fP keyword parameters.
.SH MODULE REFERENCE
.SH PACKAGE REFERENCE
.SS Architecture
.INDENT 0.0
.INDENT 3.5
@ -342,13 +342,13 @@ function and limit its area by using \fBwidth\fP and \fBheight\fP as well as
.UNINDENT
.sp
In \fBgulik\fP, there is one central \fI\%Gulik\fP object.
It manages \fI\%Monitor\fPs and \fI\%Visualizer\fPs.
It manages \fI\%monitor\fP\-\fI\%collector\fP pairs and \fI\%visualizer\fPs.
.sp
Visualizers use the \fBMonitor.normalize()\fP and \fBMonitor.caption()\fP
Visualizers use the \fI\%monitors.Monitor.normalize()\fP and \fI\%monitors.Monitor.caption()\fP
functions to utilize the collected data.
.sp
Communication between the \fI\%Monitor\fPs within the gulik process and
\fBCollector\fP processes is done via queues. Every monitor/collector
Communication between the \fI\%monitor\fPs within the gulik process and
\fI\%collector\fP processes is done via queues. Every monitor/collector
pair shares two queues. One "update queue" that monitors use to send update
requests to collectors and one "data queue" that collectors use to send the
next datapoint to their respective monitor.
@ -390,44 +390,44 @@ one important distinction: margins and paddings are included in
.SS Concepts
.SS visualizer
.sp
Visualizers are instances of any subclass of \fI\%Visualizer\fP\&.
Visualizers are instances of any subclass of \fI\%visualizers.Visualizer\fP\&.
.sp
A visualizer is assigned a \fI\%Monitor\fP and a list of \fI\%element\fPs.
A visualizer is assigned a \fI\%monitor\fP and a list of \fI\%element\fPs.
.sp
\fI\%Gulik\fP will periodically call all visualizers \fBupdate\fP methods
(see source of \fI\%Visualizer.update()\fP).
(see source of \fI\%visualizers.Visualizer.update()\fP).
.sp
What exactly happens in the \fBupdate\fP function of a visualizer differs
between the different classes, but usually it queries the instance\-assigned
monitor for the most recent data about its \fI\%element\fPs by calling
\fBMonitor.normalize()\fP and then does some drawing on the passed
\fBmonitor.Monitor.normalize()\fP and then does some drawing on the passed
\fBcairo.Context\fP to visualize the data in some manner.
.INDENT 0.0
.TP
.B Currently, there are 7 built\-in visualizers:
.INDENT 7.0
.IP \(bu 2
\fI\%Text\fP
\fI\%visualizers.Text\fP
.IP \(bu 2
\fI\%Rect\fP
\fI\%visualizers.Rect\fP
.IP \(bu 2
\fI\%Arc\fP
\fI\%visualizers.Arc\fP
.IP \(bu 2
\fI\%Plot\fP
\fI\%visualizers.Plot\fP
.IP \(bu 2
\fI\%MirrorRect\fP
\fI\%visualizers.MirrorRect\fP
.IP \(bu 2
\fI\%MirrorArc\fP
\fI\%visualizers.MirrorArc\fP
.IP \(bu 2
\fI\%MirrorPlot\fP
\fI\%visualizers.MirrorPlot\fP
.UNINDENT
.UNINDENT
.sp
You are, however, welcome to implement your own visualizers by subclassing
\fI\%Visualizer\fP and overriding \fBVisualizer.draw()\fP\&.
\fI\%visualizers.Visualizer\fP and overriding \fBvisualizers.Visualizer.draw()\fP\&.
.SS monitor
.sp
Monitors are instances of any subclass of \fI\%Monitor\fP,
Monitors are instances of any subclass of \fI\%monitors.Monitor\fP,
which itself is a subclass of \fBmultithreading.Thread\fP\&.
.sp
Every monitor acts as one half of a monitor\-\fI\%collector\fP pair,
@ -435,28 +435,28 @@ each of which collects and transforms data on a specific \fI\%component\fP\&.
.sp
The monitors responsibility in this pair is to take data from the collector
and offer it in a form that is usable by \fI\%visualizer\fPs. This is mainly
done through the functions \fBMonitor.normalize()\fP and \fBMonitor.caption()\fP\&.
done through the functions \fI\%monitors.Monitor.normalize()\fP and \fI\%monitors.Monitor.caption()\fP\&.
.INDENT 0.0
.TP
.B Currently, there are 6 built\-in monitors:
.INDENT 7.0
.IP \(bu 2
\fI\%CPUMonitor\fP
\fI\%monitors.CPUMonitor\fP
.IP \(bu 2
\fI\%MemoryMonitor\fP
\fI\%monitors.MemoryMonitor\fP
.IP \(bu 2
\fI\%NetworkMonitor\fP
\fI\%monitors.NetworkMonitor\fP
.IP \(bu 2
\fI\%BatteryMonitor\fP
\fI\%monitors.BatteryMonitor\fP
.IP \(bu 2
\fI\%DiskMonitor\fP
\fI\%monitors.DiskMonitor\fP
.IP \(bu 2
\fI\%NetdataMonitor\fP
\fI\%monitors.NetdataMonitor\fP
.UNINDENT
.UNINDENT
.SS collector
.sp
Collectors are instances of any subclass of \fBCollector\fP,
Collectors are instances of any subclass of \fBcollectors.Collector\fP,
which itself is a subclass of \fBmultiprocessing.Process\fP\&.
.sp
Every collector acts as one half of a \fI\%monitor\fP\-collector pair.
@ -488,7 +488,7 @@ exists in the \fBNETDATA_HOSTS\fP configuration option.
.SS element
.sp
A string identifying a (sub) element of a data source.
Valid values are defined within the respective \fI\%Monitor\fPs.
Valid values are defined within the respective \fI\%monitor\fPs.
.SS alignment
.INDENT 0.0
.TP
@ -502,10 +502,10 @@ Valid values are defined within the respective \fI\%Monitor\fPs.
.UNINDENT
.sp
alignments are used both for text positioning relative to its respective
allowed borders as well as positioning captions within \fI\%Visualizer\fPs.
allowed borders as well as positioning captions within \fI\%visualizer\fPs.
.SS caption description
.sp
A dictionary describing a caption to be rendered by a \fI\%Visualizer\fP\&.
A dictionary describing a caption to be rendered by a \fI\%visualizer\fP\&.
.INDENT 0.0
.TP
.B Required items:
@ -530,25 +530,25 @@ A dictionary describing a caption to be rendered by a \fI\%Visualizer\fP\&.
.UNINDENT
.SS pattern
.sp
A function taking one \fI\%Color\fP as parameter returning a cairo surface for
use as fill. See \fBstripe45()\fP for an example.
A function taking one \fI\%helpers.Color\fP as parameter returning a cairo surface for
use as fill. See \fI\%patterns.stripe45()\fP for an example.
.SS palette
.sp
A function taking one \fI\%Color\fP and one \fIint\fP parameter returning a
\fBlist\fP of \fI\%Color\fP objects with its length being equal to the passed
A function taking one \fI\%helpers.Color\fP and one \fIint\fP parameter returning a
\fBlist\fP of \fI\%helpers.Color\fP objects with its length being equal to the passed
\fIint\fP parameter.
.sp
\fBNOTE:\fP
.INDENT 0.0
.INDENT 3.5
\fI\%palette_hue()\fP and \fI\%palette_value()\fP have extra parameters
\fI\%palettes.hue()\fP and \fI\%palettes.value()\fP have extra parameters
you won\(aqt be able to use without wrapping them in \fBfunctools.partial()\fP
first!
.UNINDENT
.UNINDENT
.SS combination
.sp
A string denoting how multiple \fI\%element\fPs are displayed within a \fI\%Visualizer\fP\&.
A string denoting how multiple \fI\%element\fPs are displayed within a \fI\%visualizer\fP\&.
.INDENT 0.0
.TP
.B Valid values are:
@ -563,103 +563,55 @@ A string denoting how multiple \fI\%element\fPs are displayed within a \fI\%Visu
.UNINDENT
.INDENT 0.0
.TP
.B class gulik.Color(red=None, green=None, blue=None, alpha=None, hue=None, saturation=None, value=None)
Magic color class implementing and supplying on\-the\-fly manipulation of
RGB and HSV (and alpha) attributes.
.INDENT 7.0
.TP
.B tuple_rgb()
return color (without alpha) as tuple, channels being float 0.0\-1.0
.UNINDENT
.INDENT 7.0
.TP
.B tuple_rgba()
return color (\fIwith\fP alpha) as tuple, channels being float 0.0\-1.0
.UNINDENT
.B class gulik.Gulik(configpath)
The main object thingamabob.
.UNINDENT
.SS \fIgulik.collectors\fP
.SS \fIgulik.monitors\fP
.INDENT 0.0
.TP
.B class gulik.DotDict
A dictionary with its data being readable through faked attributes.
Used to avoid [[[][][][][]] in caption formatting.
.UNINDENT
.INDENT 0.0
.TP
.B gulik.palette_hue(base, count, distance=180)
Creates a hue\-rotation palette.
.INDENT 7.0
.TP
.B Parameters
.INDENT 7.0
.IP \(bu 2
\fBbase\fP (\fI\%Color\fP) \-\- Color on which the palette will be based (i.e. the starting point of the hue\-rotation).
.IP \(bu 2
\fBcount\fP (\fIint\fP) \-\- number of colors the palette should hold.
.IP \(bu 2
\fBdistance\fP (\fIint\fP\fI or \fP\fIfloat\fP) \-\- angular distance on a 360° hue circle thingamabob.
.UNINDENT
.TP
.B Returns
A list of length \fBcount\fP of \fI\%Color\fP objects.
.TP
.B Return type
list
.UNINDENT
.UNINDENT
.INDENT 0.0
.TP
.B gulik.palette_value(base, count, min=None, max=None)
Creates a value\-stepped palette
.INDENT 7.0
.TP
.B Parameters
.INDENT 7.0
.IP \(bu 2
\fBbase\fP (\fI\%Color\fP) \-\- Color on which the palette will be based (i.e. source of hue and saturation)
.IP \(bu 2
\fBcount\fP (\fIint\fP) \-\- number of colors the palette should hold
.IP \(bu 2
\fBmin\fP (\fIfloat >= 0 and <= 1\fP) \-\- minimum value (the v in hsv)
.IP \(bu 2
\fBmax\fP (\fIfloat >= 0 and <= 1\fP) \-\- maximum value
.UNINDENT
.TP
.B Returns
A list of length \fBcount\fP of \fI\%Color\fP objects.
.TP
.B Return type
list
.UNINDENT
.UNINDENT
.INDENT 0.0
.TP
.B gulik.pretty_si(number)
Return a SI\-postfixed string representation of a number (int or float).
.UNINDENT
.INDENT 0.0
.TP
.B gulik.pretty_bytes(bytecount)
Return a human\-readable representation given a size in bytes.
.UNINDENT
.INDENT 0.0
.TP
.B gulik.pretty_bits(bytecount)
Return a human\-readable representation in bits given a size in bytes.
.UNINDENT
.INDENT 0.0
.TP
.B gulik.ignore_none(*args)
Return the first passed value that isn\(aqt \fBNone\fP\&.
.UNINDENT
.INDENT 0.0
.TP
.B class gulik.Monitor(app, component)
.B class gulik.monitors.Monitor(app, component)
The base class for all \fI\%monitor\fPs.
.INDENT 7.0
.TP
.B normalize(element)
Return most current datapoint about \fIelement\fP,
normalized to a float between 0 and 1.
.INDENT 7.0
.TP
.B Parameters
\fBelement\fP (\fIstr\fP) \-\- An \fI\%element\fP that is valid in the context of this monitor.
.UNINDENT
.sp
\fBNOTE:\fP
.INDENT 7.0
.INDENT 3.5
This function has to be overriden in custom monitors.
.UNINDENT
.UNINDENT
.UNINDENT
.INDENT 7.0
.TP
.B caption(fmt)
Return a given string with placeholders filled in with current values of this monitor.
.INDENT 7.0
.TP
.B Parameters
\fBfmt\fP (\fIstr\fP) \-\- A format string; The \fItext\fP item of a \fI\%caption description\fP\&.
.UNINDENT
.sp
\fBNOTE:\fP
.INDENT 7.0
.INDENT 3.5
This function has to be overridden in custom monitors.
.UNINDENT
.UNINDENT
.UNINDENT
.UNINDENT
.INDENT 0.0
.TP
.B class gulik.CPUMonitor(app, component)
Memory for CPU usage.
.B class gulik.monitors.CPUMonitor(app, component)
Monitor for CPU usage.
.INDENT 7.0
.TP
.B normalize(element)
@ -693,7 +645,7 @@ Memory for CPU usage.
.UNINDENT
.INDENT 0.0
.TP
.B class gulik.MemoryMonitor(app, component)
.B class gulik.monitors.MemoryMonitor(app, component)
Monitor for memory usage
.INDENT 7.0
.TP
@ -734,16 +686,18 @@ Monitor for memory usage
.UNINDENT
.INDENT 0.0
.TP
.B class gulik.NetworkMonitor(app, component)
.B class gulik.monitors.NetworkMonitor(app, component)
Monitor for network interfaces.
.INDENT 7.0
.TP
.B count_sec(interface, key)
get a specified count for a given interface
as calculated for the last second.
.sp
EXAMPLE: \fBself.count_sec(\(aqeth0\(aq, \(aqbytes_sent\(aq)\fP
Example.nf
\fBself.count_sec(\(aqeth0\(aq, \(aqbytes_sent\(aq)\fP
(will return count of bytes sent in the last second)
.fi
.sp
.UNINDENT
.INDENT 7.0
.TP
@ -810,7 +764,7 @@ count of network interfaces as \fBif_count\fP\&.
.UNINDENT
.INDENT 0.0
.TP
.B class gulik.BatteryMonitor(app, component)
.B class gulik.monitors.BatteryMonitor(app, component)
Monitor laptop batteries.
.INDENT 7.0
.TP
@ -839,7 +793,7 @@ the current fill of the battery.
.UNINDENT
.INDENT 0.0
.TP
.B class gulik.DiskMonitor(*args, **kwargs)
.B class gulik.monitors.DiskMonitor(*args, **kwargs)
Monitors disk I/O and partitions.
.INDENT 7.0
.TP
@ -896,17 +850,13 @@ Exposed keys are the same as for \fI\%DiskMonitor.normalize()\fP\&.
.UNINDENT
.INDENT 0.0
.TP
.B class gulik.NetdataMonitor(app, component, host, port)
.B class gulik.monitors.NetdataMonitor(app, component, host, port)
Monitor that interfaces with (remote) netdata instances.
.INDENT 7.0
.TP
.B normalize(element)
Exposed elements correspond to \fIchart names\fP and their datapoint
.nf
*
.fi
dimension*s. For a list of valid chart and dimensions names, consult
\fIdimension\fPs. For a list of valid chart and dimensions names, consult
\fB/api/v1/charts\fP of the netdata instance in question.
Examples.INDENT 7.0
.IP \(bu 2
@ -921,21 +871,22 @@ Examples.INDENT 7.0
Exposed keys are the same as for \fI\%NetdataMonitor.normalize()\fP\&.
.UNINDENT
.UNINDENT
.SS \fIgulik.visualizers\fP
.INDENT 0.0
.TP
.B class gulik.Visualizer(app, monitor, x=0, y=0, width=None, height=None, margin=None, margin_left=None, margin_right=None, margin_top=None, margin_bottom=None, padding=None, padding_left=None, padding_right=None, padding_top=None, padding_bottom=None, elements=None, captions=None, caption_placement=None, legend=None, legend_order=None, legend_format=None, legend_size=None, legend_placement=None, legend_margin=None, legend_margin_left=None, legend_margin_right=None, legend_margin_top=None, legend_margin_bottom=None, legend_padding=None, legend_padding_left=None, legend_padding_right=None, legend_padding_top=None, legend_padding_bottom=None, foreground=None, background=None, pattern=None, palette=None, combination=None, operator=None)
.B class gulik.visualizers.Visualizer(app, monitor, x=0, y=0, width=None, height=None, margin=None, margin_left=None, margin_right=None, margin_top=None, margin_bottom=None, padding=None, padding_left=None, padding_right=None, padding_top=None, padding_bottom=None, elements=None, captions=None, caption_placement=None, legend=None, legend_order=None, legend_format=None, legend_size=None, legend_placement=None, legend_margin=None, legend_margin_left=None, legend_margin_right=None, legend_margin_top=None, legend_margin_bottom=None, legend_padding=None, legend_padding_left=None, legend_padding_right=None, legend_padding_top=None, legend_padding_bottom=None, foreground=None, background=None, pattern=None, palette=None, combination=None, operator=None)
The base class for all visualizers. Not called widget to avoid naming
confusion with gtk widgets (which aren\(aqt even used in gulik).
.sp
Usually you won\(aqt instantiate this by yourself but use \fI\%Box.place()\fP\&.
Usually you won\(aqt instantiate this by yourself but use \fBgulik.Box.place()\fP\&.
.INDENT 7.0
.TP
.B Parameters
.INDENT 7.0
.IP \(bu 2
\fBapp\fP (\fI\%Gulik\fP) \-\- The app managing visualizers and monitors
\fBapp\fP (\fI\%gulik.Gulik\fP) \-\- The app managing visualizers and monitors
.IP \(bu 2
\fBmonitor\fP (\fI\%Monitor\fP) \-\- The monitor managing data collection for this visualizer
\fBmonitor\fP (\fI\%monitor\fP) \-\- The monitor managing data collection for this visualizer
.IP \(bu 2
\fBx\fP (\fIint\fP) \-\- leftmost coordinate on the x\-axis
.IP \(bu 2
@ -999,9 +950,9 @@ Usually you won\(aqt instantiate this by yourself but use \fI\%Box.place()\fP\&.
.IP \(bu 2
\fBlegend_padding_bottom\fP (\fIint\fP\fI, \fP\fIoptional\fP) \-\- padding applied to the bottom side of the legend
.IP \(bu 2
\fBforeground\fP (\fI\%Color\fP, optional) \-\- The foreground color, base\-color for generated palettes
\fBforeground\fP (\fBgulik,helpers.Color\fP, optional) \-\- The foreground color, base\-color for generated palettes
.IP \(bu 2
\fBbackground\fP (\fI\%Color\fP, optional) \-\- The background color
\fBbackground\fP (\fI\%gulik.helpers.Color\fP, optional) \-\- The background color
.IP \(bu 2
\fBpattern\fP (\fIfunction\fP\fI, \fP\fIoptional\fP) \-\- An executable \fI\%pattern\fP
.IP \(bu 2
@ -1033,18 +984,18 @@ defines colors for legend elements
.UNINDENT
.INDENT 0.0
.TP
.B class gulik.Text(app, monitor, text, speed=25, align=None, **kwargs)
.B class gulik.visualizers.Text(app, monitor, text, speed=25, align=None, **kwargs)
Scrollable text using monitors\(aq \fBcaption\fP function to give textual
representations of values, prettified where necessary.
.UNINDENT
.INDENT 0.0
.TP
.B class gulik.Rect(app, monitor, x=0, y=0, width=None, height=None, margin=None, margin_left=None, margin_right=None, margin_top=None, margin_bottom=None, padding=None, padding_left=None, padding_right=None, padding_top=None, padding_bottom=None, elements=None, captions=None, caption_placement=None, legend=None, legend_order=None, legend_format=None, legend_size=None, legend_placement=None, legend_margin=None, legend_margin_left=None, legend_margin_right=None, legend_margin_top=None, legend_margin_bottom=None, legend_padding=None, legend_padding_left=None, legend_padding_right=None, legend_padding_top=None, legend_padding_bottom=None, foreground=None, background=None, pattern=None, palette=None, combination=None, operator=None)
.B class gulik.visualizers.Rect(app, monitor, x=0, y=0, width=None, height=None, margin=None, margin_left=None, margin_right=None, margin_top=None, margin_bottom=None, padding=None, padding_left=None, padding_right=None, padding_top=None, padding_bottom=None, elements=None, captions=None, caption_placement=None, legend=None, legend_order=None, legend_format=None, legend_size=None, legend_placement=None, legend_margin=None, legend_margin_left=None, legend_margin_right=None, legend_margin_top=None, legend_margin_bottom=None, legend_padding=None, legend_padding_left=None, legend_padding_right=None, legend_padding_top=None, legend_padding_bottom=None, foreground=None, background=None, pattern=None, palette=None, combination=None, operator=None)
[image]
.UNINDENT
.INDENT 0.0
.TP
.B class gulik.MirrorRect(app, monitor, **kwargs)
.B class gulik.visualizers.MirrorRect(app, monitor, **kwargs)
Mirrored variant of \fI\%Rect\fP\&.
.INDENT 7.0
.TP
@ -1054,7 +1005,7 @@ defines colors for legend elements
.UNINDENT
.INDENT 0.0
.TP
.B class gulik.Arc(app, monitor, stroke_width=5, **kwargs)
.B class gulik.visualizers.Arc(app, monitor, stroke_width=5, **kwargs)
[image]
.INDENT 7.0
.TP
@ -1064,12 +1015,12 @@ defines colors for legend elements
.UNINDENT
.INDENT 0.0
.TP
.B class gulik.MirrorArc(app, monitor, **kwargs)
.B class gulik.visualizers.MirrorArc(app, monitor, **kwargs)
Mirrored variant of \fI\%Arc\fP\&.
.UNINDENT
.INDENT 0.0
.TP
.B class gulik.Plot(app, monitor, num_points=None, autoscale=None, markers=None, line=None, grid=None, **kwargs)
.B class gulik.visualizers.Plot(app, monitor, num_points=None, autoscale=None, markers=None, line=None, grid=None, **kwargs)
[image]
.INDENT 7.0
.TP
@ -1086,26 +1037,124 @@ Mirrored variant of \fI\%Arc\fP\&.
.IP \(bu 2
\fBgrid\fP (\fIbool\fP\fI, \fP\fIoptional\fP) \-\- Whether to draw a grid. The grid automatically adapts if \fBautoscale\fP is \fBTrue\fP\&.
.IP \(bu 2
\fB**kwargs\fP \-\- passed on to \fI\%Visualizer\fP\&.
\fB**kwargs\fP \-\- passed up to \fI\%Visualizer\fP\&.
.UNINDENT
.UNINDENT
.UNINDENT
.INDENT 0.0
.TP
.B class gulik.MirrorPlot(app, monitor, scale_lock=True, **kwargs)
.B class gulik.visualizers.MirrorPlot(app, monitor, scale_lock=True, **kwargs)
Mirrored variant of \fI\%Plot\fP\&.
.UNINDENT
.SS \fIgulik.palettes\fP
.INDENT 0.0
.TP
.B class gulik.Box(app, x, y, width, height)
Can wrap multiple \fI\%Visualizer\fPs, used for layouting.
.B gulik.palettes.hue(base, count, distance=180)
Creates a hue\-rotation palette.
.INDENT 7.0
.TP
.B Parameters
.INDENT 7.0
.IP \(bu 2
\fBbase\fP (\fBColor\fP) \-\- Color on which the palette will be based (i.e. the starting point of the hue\-rotation).
.IP \(bu 2
\fBcount\fP (\fIint\fP) \-\- number of colors the palette should hold.
.IP \(bu 2
\fBdistance\fP (\fIint\fP\fI or \fP\fIfloat\fP) \-\- angular distance on a 360° hue circle thingamabob.
.UNINDENT
.TP
.B Returns
A list of length \fBcount\fP of \fBColor\fP objects.
.TP
.B Return type
list
.UNINDENT
.UNINDENT
.INDENT 0.0
.TP
.B gulik.palettes.value(base, count, min=None, max=None)
Creates a value\-stepped palette
.INDENT 7.0
.TP
.B Parameters
.INDENT 7.0
.IP \(bu 2
\fBbase\fP (\fBColor\fP) \-\- Color on which the palette will be based (i.e. source of hue and saturation)
.IP \(bu 2
\fBcount\fP (\fIint\fP) \-\- number of colors the palette should hold
.IP \(bu 2
\fBmin\fP (\fIfloat >= 0 and <= 1\fP) \-\- minimum value (the v in hsv)
.IP \(bu 2
\fBmax\fP (\fIfloat >= 0 and <= 1\fP) \-\- maximum value
.UNINDENT
.TP
.B Returns
A list of length \fBcount\fP of \fBColor\fP objects.
.TP
.B Return type
list
.UNINDENT
.UNINDENT
.SS \fIgulik.patterns\fP
.INDENT 0.0
.TP
.B gulik.patterns.stripe45(color)
A tilable pattern with 45° stripes.
.UNINDENT
.SS \fIgulik.helpers\fP
.INDENT 0.0
.TP
.B gulik.helpers.pretty_si(number)
Return a SI\-postfixed string representation of a number (int or float).
.UNINDENT
.INDENT 0.0
.TP
.B gulik.helpers.pretty_bytes(bytecount)
Return a human\-readable representation given a size in bytes.
.UNINDENT
.INDENT 0.0
.TP
.B gulik.helpers.pretty_bits(bytecount)
Return a human\-readable representation in bits given a size in bytes.
.UNINDENT
.INDENT 0.0
.TP
.B gulik.helpers.ignore_none(*args)
Return the first passed value that isn\(aqt \fBNone\fP\&.
.UNINDENT
.INDENT 0.0
.TP
.B class gulik.helpers.Color(red=None, green=None, blue=None, alpha=None, hue=None, saturation=None, value=None)
Magic color class implementing and supplying on\-the\-fly manipulation of
RGB and HSV (and alpha) attributes.
.INDENT 7.0
.TP
.B tuple_rgb()
return color (without alpha) as tuple, channels being float 0.0\-1.0
.UNINDENT
.INDENT 7.0
.TP
.B tuple_rgba()
return color (\fIwith\fP alpha) as tuple, channels being float 0.0\-1.0
.UNINDENT
.UNINDENT
.INDENT 0.0
.TP
.B class gulik.helpers.DotDict
A dictionary with its data being readable through faked attributes.
Used to avoid [[[][][][][]] in caption formatting.
.UNINDENT
.INDENT 0.0
.TP
.B class gulik.helpers.Box(app, x, y, width, height)
Can wrap multiple \fI\%visualizer\fPs, used for layouting.
Orders added visualizers from left to right and top to bottom.
.sp
This is basically a smart helper for \fBGulik.add_visualizer()\fP\&.
.INDENT 7.0
.TP
.B place(component, cls, **kwargs)
place a new \fI\%Visualizer\fP\&.
place a new \fI\%visualizer\fP\&.
.INDENT 7.0
.TP
.B Parameters
@ -1113,7 +1162,7 @@ place a new \fI\%Visualizer\fP\&.
.IP \(bu 2
\fBcomponent\fP (\fIstr\fP) \-\- The \fI\%component\fP string identifying the data source.
.IP \(bu 2
\fBcls\fP (\fBtype\fP, child of \fI\%Visualizer\fP) \-\- The visualizer class to instantiate.
\fBcls\fP (\fBtype\fP, child of \fBvisualizers.Visualizer\fP) \-\- The visualizer class to instantiate.
.IP \(bu 2
\fB**kwargs\fP \-\- passed on to \fBcls\fP\(aq constructor. width and height defined in here are honored.
.UNINDENT
@ -1121,11 +1170,6 @@ place a new \fI\%Visualizer\fP\&.
.UNINDENT
.UNINDENT
.INDENT 0.0
.TP
.B class gulik.Gulik(configpath)
The main object thingamabob.
.UNINDENT
.INDENT 0.0
.IP \(bu 2
genindex
.IP \(bu 2
@ -1134,6 +1178,6 @@ search
.SH AUTHOR
phryk
.SH COPYRIGHT
YOLD 3184, phryk
YOLD 3184-3185, phryk
.\" Generated by docutils manpage writer.
.

View File

@ -1 +0,0 @@
../../../gulik/gulik.png

Before

Width:  |  Height:  |  Size: 24 B

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 B

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@ -24,6 +24,8 @@ pre {
margin-left: 0;*/
position: sticky;
top: 0;
max-height: 100vh;
overflow-y: scroll;
}
.sphinxsidebar input {
@ -39,7 +41,7 @@ footer#pagefooter {
dt:target,
.viewcode-block:target,
.highlighted {
background-color: rgba(255,255,255, 0.1);
background-color: rgba(255,255,255, 0.25);
}
dl.class > dt > em.property,

View File

@ -62,7 +62,7 @@ master_doc = 'index'
# General information about the project.
project = 'gulik'
copyright = 'YOLD 3184, phryk'
copyright = 'YOLD 3184-3185, phryk'
author = 'phryk'
# The version info for the project you're documenting, acts as replacement for

View File

@ -167,7 +167,7 @@ Explaining ALL THE CONFIGURATION OPTIONS (not really, tho)
* ``COLOR_FOREGROUND`` (:class:`Color`): Foreground color. This is used as base color for most :ref:`palette`\s. Default value: ``Color(0.5, 1, 0, 0.6)``
* ``COLOR_CAPTION`` (:class:`Color`): Text color for captions. Default value: ``Color(1,1,1, 0.6)``
* ``PALETTE`` (``function``): The default :ref:`palette` generator. Default value: ``functools.partial(`` :func:`palette_hue` ``, distance=-120)``
* ``PATTERN`` (``function``): The default :ref:`pattern` generator. Default value: ``stripe45``
* ``PATTERN`` (``function``): The default :ref:`pattern` generator. Default value: ``patterns.stripe45``
* ``CAPTION_PLACEMENT`` (``str``): ``"padding"`` to have captions placed in the paddings of :class:`Visualizer`\s, ``"inner"`` to place them within the drawing region of the :class:`Visualizer`. Default value: ``"inner"``
* ``LEGEND`` (``bool``): Whether :class:`Visualizer`\s should attempt automatically creating a legend for themselves in their bottom padding. Default value: ``True``
* ``LEGEND_ORDER`` (``str``): Whether to reverse the legend order. Can be ``"normal"`` or ``"reverse"``. Default value: ``"normal"``
@ -273,12 +273,48 @@ custom one, you can call :func:`Gulik.autosetup` from your custom ``setup``
function and limit its area by using ``width`` and ``height`` as well as
``x`` and ``y`` keyword parameters.
Module reference
----------------
Package reference
-----------------
.. automodule:: gulik
:members:
`gulik.collectors`
^^^^^^^^^^^^^^^^^^
.. automodule:: gulik.collectors
:members:
`gulik.monitors`
^^^^^^^^^^^^^^^^
.. automodule:: gulik.monitors
:members:
`gulik.visualizers`
^^^^^^^^^^^^^^^^^^^
.. automodule:: gulik.visualizers
:members:
`gulik.palettes`
^^^^^^^^^^^^^^^^
.. automodule:: gulik.palettes
:members:
`gulik.patterns`
^^^^^^^^^^^^^^^^
.. automodule:: gulik.patterns
:members:
`gulik.helpers`
^^^^^^^^^^^^^^^
.. automodule:: gulik.helpers
:members:
Indices and tables
==================

File diff suppressed because it is too large Load Diff

View File

@ -1 +1 @@
version='0.0.0.1'
version='0.0.0.2'

287
gulik/collectors.py Normal file
View File

@ -0,0 +1,287 @@
# -*- coding: utf-8 -*-
import os
import time
import signal
import psutil
import setproctitle
import multiprocessing
from . import helpers
class Collector(multiprocessing.Process):
def __init__(self, app, queue_update, queue_data):
super(Collector, self).__init__()
self.daemon = True
self.app = app
self.queue_update = queue_update
self.queue_data = queue_data
self.elements = []
def terminate(self):
#self.queue_data.close() # closing queues manually actually seems to mess stuff up
# Would've done this cleaner, but after half a day of chasing some
# retarded quantenbug I'm done with this shit. Just nuke the fucking
# things from orbit.
os.kill(self.pid, signal.SIGKILL)
#super(Collector, self).terminate()
def run(self):
setproctitle.setproctitle(f"gulik - {self.__class__.__name__}")
while True:
try:
msg = self.queue_update.get(block=True)
if msg == 'UPDATE':
self.update()
except KeyboardInterrupt: # so we don't randomly explode on ctrl+c
pass
def update(self):
raise NotImplementedError("%s.update not implemented!" % self.__class__.__name__)
class CPUCollector(Collector):
def update(self):
count = psutil.cpu_count()
aggregate = psutil.cpu_percent(percpu=False)
percpu = psutil.cpu_percent(percpu=True)
self.queue_data.put(
{
'count': count,
'aggregate': aggregate,
'percpu': percpu
},
block=True
)
# according to psutil docs, there should at least be 0.1 seconds
# between calls to cpu_percent without sampling interval
time.sleep(0.1)
class MemoryCollector(Collector):
def update(self):
vmem = psutil.virtual_memory()
processes = []
total_use = 0
for process in psutil.process_iter():
if psutil.LINUX or (psutil.BSD and not self.app.config['BSD_ACCURATE_MEMORY']):
if psutil.BSD:
key = 'rss'
else:
key = 'pss'
try:
pmem = process.memory_full_info()._asdict()
processes.append(helpers.DotDict({
'name': process.name(),
'size': pmem[key],
#'shared': pmem.shared,
'percent': pmem[key] / vmem.total * 100
}))
total_use += pmem[key]
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess) as e:
continue # skip to next process
elif psutil.BSD:
try:
resident = 0
size = 0
#shared = 0
try:
for mmap in process.memory_maps():
# assuming everything with a real path is
# "not really in ram", but no clue.
if mmap.path.startswith('['):
size += mmap.private * PAGESIZE
resident += mmap.rss * PAGESIZE
#shared += (mmap.rss - mmap.private) * PAGESIZE # FIXME: probably broken, can yield negative values
except OSError:
pass # probably "device not available"
processes.append(helpers.DotDict({
'name': process.name(),
'size': size,
#'shared': resident - size,
'percent': size / vmem.total * 100,
}))
total_use += size
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess) as e:
pass# TODO: add counter for processes we can't introspect
info = helpers.DotDict({
'total': vmem.total,
'percent': total_use / vmem.total * 100,
'available': vmem.total - total_use
})
processes_sorted = sorted(processes, key=lambda x: x['size'], reverse=True)
for i, process in enumerate(processes_sorted[:3]):
info['top_%d' % (i + 1)] = process
info['other'] = helpers.DotDict({
'name': 'other',
'size': 0,
#'shared': 0,
'count': 0
})
for process in processes_sorted[3:]:
info['other']['size'] += process['size']
#info['other']['shared'] += process['shared']
info['other']['count'] += 1
info['other']['percent'] = info['other']['size'] / vmem.total * 100
self.queue_data.put(info, block=True)
#time.sleep(1) # because this became horribly slow
class NetworkCollector(Collector):
def update(self):
stats = psutil.net_if_stats()
addrs = psutil.net_if_addrs()
counters = psutil.net_io_counters(pernic=True)
connections = psutil.net_connections(kind='all')
self.queue_data.put(
{
'stats': stats,
'addrs': addrs,
'counters': counters,
'connections': connections,
},
block=True
)
class BatteryCollector(Collector):
def update(self):
self.queue_data.put(psutil.sensors_battery(), block=True)
class DiskCollector(Collector):
def __init__(self, *args, **kwargs):
super(DiskCollector, self).__init__(*args, **kwargs)
self.previous_io = {}
def update(self):
data = {}
partitions = psutil.disk_partitions()
data['partitions'] = {}
for partition in partitions:
name = partition.device.split('/')[-1].replace('.', '-')
data['partitions'][name] = partition._asdict()
data['partitions'][name]['name'] = name
data['partitions'][name]['usage'] = psutil.disk_usage(partition.mountpoint)._asdict()
io = psutil.disk_io_counters(perdisk=True)
data['io'] = {}
for disk, info in io.items():
previous_info = self.previous_io.get(disk, None)
if previous_info is None:
data['io'][disk] = {
'read_count': 0,
'write_count': 0,
'read_bytes': 0,
'write_bytes': 0,
'read_time': 0,
'write_time': 0,
'busy_time': 0
}
else:
info = info._asdict()
previous_info = previous_info._asdict()
data['io'][disk] = {}
for k in ['read_count', 'write_count', 'read_bytes', 'write_bytes', 'read_time', 'write_time', 'busy_time']:
data['io'][disk][k] = (info[k] - previous_info[k]) * self.app.config['FPS']
#data['io'][disk] = info._asdict()
self.queue_data.put(data, block=True)
self.previous_io = io
class NetdataCollector(Collector):
def __init__(self, app, queue_update, queue_data, host, port):
super(NetdataCollector, self).__init__(app, queue_update, queue_data)
self.client = netdata.Netdata(host, port=port, timeout=1/self.app.config['FPS'])
def run(self):
setproctitle.setproctitle(f"gulik - {self.__class__.__name__}")
while True:
try:
msg = self.queue_update.get(block=True)
if msg.startswith('UPDATE '):
chart = msg[7:]
self.update(chart)
except KeyboardInterrupt: # so we don't randomly explode on ctrl+c
pass
def update(self, chart):
try:
# get the last second of data condensed to one point
data = self.client.data(chart, points=1, after=-1, options=['absolute'])
except netdata.NetdataException:
pass
else:
self.queue_data.put((chart, data), block=True)

419
gulik/helpers.py Normal file
View File

@ -0,0 +1,419 @@
# -*- coding: utf-8 -*-
import colorsys
def pretty_si(number):
"""
Return a SI-postfixed string representation of a number (int or float).
"""
postfixes = ['', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']
value = number
for postfix in postfixes:
if value / 1000.0 < 1:
break
value /= 1000.0
return "%.2f%s" % (value, postfix)
def pretty_bytes(bytecount):
"""
Return a human-readable representation given a size in bytes.
"""
units = ['Byte', 'kbyte', 'Mbyte', 'Gbyte', 'Tbyte']
value = bytecount
for unit in units:
if value / 1024.0 < 1:
break
value /= 1024.0
return "%.2f %s" % (value, unit)
def pretty_bits(bytecount):
"""
Return a human-readable representation in bits given a size in bytes.
"""
units = ['bit', 'kbit', 'Mbit', 'Gbit', 'Tbit']
value = bytecount * 8 # bytes to bits
for unit in units:
if value / 1024.0 < 1:
break
value /= 1024.0
return "%.2f %s" % (value, unit)
def pretty_time(seconds):
days = int(seconds / 86400)
seconds = seconds % 86400
hours = int(seconds / 3600)
seconds = seconds % 3600
minutes = int(seconds / 60)
seconds = seconds % 60
parts = []
if days > 0:
parts.append(f"{days}d")
if hours > 0:
parts.append(f"{hours}h")
if minutes > 0:
parts.append(f"{minutes}m")
parts.append("{seconds}s")
return " ".join(parts)
def ignore_none(*args):
"""
Return the first passed value that isn't ``None``.
"""
for arg in args:
if not arg is None:
return arg
def condense_addr_parts(items):
matching = []
max_match = min([len(x) for x in items])
for i in range(0, max_match):
s = set()
for item_idx in range(0, len(items)):
s.add(items[item_idx][i])
if len(s) > 1: # lazy hack, means not all items have the same part at index i
break
matching.append(items[item_idx][i])
return '.'.join(matching)
def alignment_offset(align, size):
x_align, y_align = align.split('_')
if x_align == 'left':
x_offset = 0
elif x_align == 'center':
x_offset = -size[0] / 2
elif x_align == 'right':
x_offset = -size[0]
else:
raise ValueError("unknown horizontal alignment: '%s', must be one of: left, center, right" % x_align)
if y_align == 'top':
y_offset = 0
elif y_align == 'center':
y_offset = -size[1] / 2
elif y_align == 'bottom':
y_offset = -size[1]
else:
raise ValueError("unknown horizontal alignment: '%s', must be one of: top, center, bottom" % y_align)
return (x_offset, y_offset)
class Color(object):
"""
Magic color class implementing and supplying on-the-fly manipulation of
RGB and HSV (and alpha) attributes.
"""
def __init__(self, red=None, green=None, blue=None, alpha=None, hue=None, saturation=None, value=None):
rgb_passed = bool(red)|bool(green)|bool(blue)
hsv_passed = bool(hue)|bool(saturation)|bool(value)
if not alpha:
alpha = 0.0
if rgb_passed and hsv_passed:
raise ValueError("Color can't be initialized with RGB and HSV at the same time.")
elif hsv_passed:
if not hue:
hue = 0.0
if not saturation:
saturation = 0.0
if not value:
value = 0.0
super(Color, self).__setattr__('hue', hue)
super(Color, self).__setattr__('saturation', saturation)
super(Color, self).__setattr__('value', value)
self._update_rgb()
else:
if not red:
red = 0
if not green:
green = 0
if not blue:
blue = 0
super(Color, self).__setattr__('red', red)
super(Color, self).__setattr__('green', green)
super(Color, self).__setattr__('blue', blue)
self._update_hsv()
super(Color, self).__setattr__('alpha', alpha)
def __setattr__(self, key, value):
if key in ('red', 'green', 'blue'):
if value > 1.0:
value = value % 1.0
super(Color, self).__setattr__(key, value)
self._update_hsv()
elif key in ('hue', 'saturation', 'value'):
if key == 'hue' and (value >= 360.0 or value < 0):
value = value % 360.0
elif key != 'hue' and value > 1.0:
value = 1.0
super(Color, self).__setattr__(key, value)
self._update_rgb()
else:
if key == 'alpha' and value > 1.0: # TODO: Might this be more fitting in another place?
value = 1.0
super(Color, self).__setattr__(key, value)
def __repr__(self):
return '<%s: red %f, green %f, blue %f, hue %f, saturation %f, value %f, alpha %f>' % (
self.__class__.__name__,
self.red,
self.green,
self.blue,
self.hue,
self.saturation,
self.value,
self.alpha
)
def clone(self):
return Color(red=self.red, green=self.green, blue=self.blue, alpha=self.alpha)
def blend(self, other, mode='normal'):
if self.alpha != 1.0: # no clue how to blend with a translucent bottom layer
self.red = self.red * self.alpha
self.green = self.green * self.alpha
self.blue = self.blue * self.alpha
self.alpha = 1.0
if mode == 'normal':
own_influence = 1.0 - other.alpha
self.red = (self.red * own_influence) + (other.red * other.alpha)
self.green = (self.green * own_influence) + (other.green * other.alpha)
self.blue = (self.blue * own_influence) + (other.blue * other.alpha)
def lighten(self, other):
if isinstance(other, int) or isinstance(other, float):
other = Color(red=other, green=other, blue=other, alpha=1.0)
if self.alpha != 1.0:
self.red = self.red * self.alpha
self.green = self.green * self.alpha
self.blue = self.blue * self.alpha
self.alpha = 1.0
red = self.red + (other.red * other.alpha)
green = self.green + (other.green * other.alpha)
blue = self.blue + (other.blue * other.alpha)
if red > 1.0:
red = 1.0
if green > 1.0:
green = 1.0
if blue > 1.0:
blue = 1.0
self.red = red
self.green = green
self.blue = blue
def darken(self, other):
if isinstance(other, int) or isinstance(other, float):
other = Color(red=other, green=other, blue=other, alpha=1.0)
red = self.red - other.red
green = self.green - other.green
blue = self.blue - other.blue
if red < 0:
red = 0
if green < 0:
green = 0
if blue < 0:
blue = 0
self.red = red
self.green = green
self.blue = blue
def tuple_rgb(self):
""" return color (without alpha) as tuple, channels being float 0.0-1.0 """
return (self.red, self.green, self.blue)
def tuple_rgba(self):
""" return color (*with* alpha) as tuple, channels being float 0.0-1.0 """
return (self.red, self.green, self.blue, self.alpha)
def _update_hsv(self):
hue, saturation, value = colorsys.rgb_to_hsv(self.red, self.green, self.blue)
super(Color, self).__setattr__('hue', hue * 360.0)
super(Color, self).__setattr__('saturation', saturation)
super(Color, self).__setattr__('value', value)
def _update_rgb(self):
red, green, blue = colorsys.hsv_to_rgb(self.hue / 360.0, self.saturation, self.value)
super(Color, self).__setattr__('red', red)
super(Color, self).__setattr__('green', green)
super(Color, self).__setattr__('blue', blue)
class DotDict(dict):
"""
A dictionary with its data being readable through faked attributes.
Used to avoid [[[][][][][]] in caption formatting.
"""
def __getattribute__(self, name):
#data = super(DotDict, self).__getattribute__('data')
keys = super(DotDict, self).keys()
if name in keys:
return self.get(name)
return super(DotDict, self).__getattribute__(name)
class Box(object):
"""
Can wrap multiple :ref:`visualizer`\s, used for layouting.
Orders added visualizers from left to right and top to bottom.
This is basically a smart helper for :func:`Gulik.add_visualizer`.
"""
def __init__(self, app, x, y, width, height):
self._last_right = 0
self._last_top = 0
self._last_bottom = 0
self._next_row_x = 0
self.app = app
self.x = x
self.y = y
self.width = width
self.height = height
def place(self, component, cls, **kwargs):
"""
place a new :ref:`visualizer`.
Parameters:
component (str): The :ref:`component` string identifying the data source.
cls (:class:`type`, child of :class:`visualizers.Visualizer`): The visualizer class to instantiate.
**kwargs: passed on to ``cls``\' constructor. width and height defined in here are honored.
"""
width = kwargs.get('width', None)
height = kwargs.get('height', None)
if width is None:
width = self.width - self._last_right
if height is None:
height = self._last_bottom - self._last_top # same height as previous visualizer
if height == 0: # should only happen on first visualizer
height = self.height
elif height is None:
height = self.height - self._last_bottom
if self._last_right + width > self.width: # move to next "row"
#print("next row", component, cls, kwargs)
x = self._next_row_x
y = self._last_bottom
self._next_row_x = 0 # this will probably break adding a third multi-stacked column, but works for now
else:
x = self._last_right
y = self._last_top
kwargs['x'] = self.x + x
kwargs['y'] = self.y + y
kwargs['width'] = width
kwargs['height'] = height
self.app.add_visualizer(component, cls, **kwargs)
if y + height < self._last_bottom:
self._next_row_x = self._last_right
self._last_right = x + width
self._last_top = y
self._last_bottom = y + height
assert self._last_bottom <= self.y + self.height, f"Box already full! Can't place {cls.__name__}"

825
gulik/monitors.py Normal file
View File

@ -0,0 +1,825 @@
# -*- coding: utf-8 -*-
import collections
import queue
import threading
import multiprocessing # for Queue
import psutil
from . import helpers
from . import collectors
class Monitor(threading.Thread):
"""
The base class for all :ref:`monitor`\s.
"""
collector_type = collectors.Collector
def __init__(self, app, component):
super(Monitor, self).__init__()
self.app = app
self.component = component
self.daemon = True
self.seppuku = False
self.queue_update = multiprocessing.Queue(1)
self.queue_data = multiprocessing.Queue(1)
self.collector = self.collector_type(self.app, self.queue_update, self.queue_data)
self.data = {}
self.defective = False # for future use, mostly for networked monitors (netdata, mpd, …)
def register_elements(self, elements):
pass
def tick(self):
if not self.queue_update.full():
self.queue_update.put('UPDATE', block=True)
def start(self):
self.collector.start()
super(Monitor, self).start()
def run(self):
#while self.collector.is_alive():
while not self.seppuku:
try:
self.data = self.queue_data.get(timeout=1)
except queue.Empty:
# try again, but give thread the ability to die without
# waiting on collector indefinitely
continue
self.commit_seppuku()
def commit_seppuku(self):
print(f"{self.__class__.__name__} committing glorious seppuku!")
#self.queue_update.close()
self.collector.terminate()
self.collector.join()
def normalize(self, element):
"""
Return most current datapoint about `element`,
normalized to a float between 0 and 1.
Parameters:
element (str): An :ref:`element` that is valid in the context of this monitor.
.. note:: This function has to be overriden in custom monitors.
"""
raise NotImplementedError("%s.normalize not implemented!" % self.__class__.__name__)
def caption(self, fmt):
"""
Return a given string with placeholders filled in with current values of this monitor.
Parameters:
fmt (str): A format string; The `text` item of a :ref:`caption-description`.
.. note:: This function has to be overridden in custom monitors.
"""
raise NotImplementedError("%s.caption not implemented!" % self.__class__.__name__)
class CPUMonitor(Monitor):
"""
Monitor for CPU usage.
"""
collector_type = collectors.CPUCollector
def normalize(self, element):
"""
Elements exposed:
* ``aggregate``: average cpu use, sum of all core loads divided by number of cores
* ``core_<n>``: load of core ``<n>``, with possible values of ``<n>`` being 0 to number of cores - 1
"""
if not self.data:
return 0
if element == 'aggregate':
return self.data['aggregate'] / 100.0
# assume core_<n> otherwise
idx = int(element.split('_')[1])
return self.data['percpu'][idx] / 100.0
def caption(self, fmt):
"""
Exposed keys:
* ``aggregate``: average cpu use, sum of all core loads divided by number of cores
* ``core_<n>``: load of core ``<n>``, with possible values of ``<n>`` being 0 to number of cores - 1
* ``count``: number of cores
"""
if not self.data:
return fmt
data = {}
data['count'] = self.data['count']
data['aggregate'] = self.data['aggregate']
for idx, perc in enumerate(self.data['percpu']):
data['core_%d' % idx] = perc
return fmt.format(**data)
class MemoryMonitor(Monitor):
"""
Monitor for memory usage
"""
collector_type = collectors.MemoryCollector
def normalize(self, element):
"""
Elements exposed:
* ``percent``: memory use of all processes.
* ``top_<n>``: memory use of the ``<n>``\th-biggest process. Valid values of ``<n>`` are 1-3.
* ``other``: memory use of all processes except the top 3
"""
if not self.data:
return 0
if element == 'percent':
return self.data.get('percent', 0) / 100.0
return self.data[element].get('percent', 0) / 100.0
def caption(self, fmt):
"""
Exposed keys:
* ``total``: how much memory this machine has in total,
* ``percent``: total memory usage in percent.
* ``available``: how much memory can be malloc'd without going into swap (roughly).
* ``top_<n>``: access information about the 3 "biggest" processes. possible subkeys are ``name``, ``size`` and ``percent``.
* ``other``: aggregate information for all processes *except* the top 3. Same subkeys as those, plus ``'count``.
"""
if not self.data:
return fmt
data = helpers.DotDict()#dict(self.data) # clone
data['total'] = helpers.pretty_bytes(self.data['total'])
data['available'] = helpers.pretty_bytes(self.data['available'])
data['percent'] = self.data['percent']
for k in ['top_1', 'top_2', 'top_3', 'other']:
data[k] = helpers.DotDict()
data[k]['name'] = self.data[k]['name']
data[k]['size'] = helpers.pretty_bytes(self.data[k]['size'])
#data[k]['shared'] = helpers.pretty_bytes(self.data[k]['shared'])
if k == 'other':
data[k]['count'] = self.data[k]['count']
return fmt.format(**data)
class NetworkMonitor(Monitor):
"""
Monitor for network interfaces.
"""
collector_type = collectors.NetworkCollector
def __init__(self, app, component):
super(NetworkMonitor, self).__init__(app, component)
self.interfaces = collections.OrderedDict()
if self.app.config['FPS'] < 2:
# we need a minimum of 2 samples so we can compute a difference
deque_len = 2
else:
# max size equal fps means this holds data of only the last second
deque_len = self.app.config['FPS']
keys = [
'bytes_sent',
'bytes_recv',
'packets_sent',
'packets_recv',
'errin',
'errout',
'dropin',
'dropout'
]
for if_name in psutil.net_io_counters(pernic=True).keys():
self.interfaces[if_name] = {
'addrs': {},
'stats': {},
'counters': {}
}
for key in keys:
self.interfaces[if_name]['counters'][key] = collections.deque([], deque_len)
self.aggregate = {
'if_count': len(self.interfaces),
'if_up': 0,
'speed': 0, # aggregate link speed
'counters': {}
}
for key in keys:
self.aggregate['counters'][key] = collections.deque([], deque_len)
def run(self):
while not self.seppuku:
try:
self.data = self.queue_data.get(timeout=1)
except queue.Empty:
# try again, but give thread the ability to die
# without waiting on collector indefinitely.
continue
aggregates = {}
for key in self.aggregate['counters']:
#self.aggregate['counters'][k] = []
aggregates[key] = 0
self.aggregate['speed'] = 0
for if_name, if_data in self.interfaces.items():
if_has_data = if_name in self.data['counters'] and\
if_name in self.data['stats'] and\
if_name in self.data['addrs']
if if_has_data:
for key, deque in if_data['counters'].items():
value = self.data['counters'][if_name]._asdict()[key]
deque.append(value)
aggregates[key] += value
self.interfaces[if_name]['stats'] = self.data['stats'][if_name]._asdict()
if self.interfaces[if_name]['stats']['speed'] == 0:
self.interfaces[if_name]['stats']['speed'] = 1000 # assume gbit speed per default
self.aggregate['speed'] += self.interfaces[if_name]['stats']['speed']
if if_name in self.data['addrs']:
self.interfaces[if_name]['addrs'] = self.data['addrs'][if_name]
else:
self.interfaces[if_name]['addrs'] = []
for key, value in aggregates.items():
self.aggregate['counters'][key].append(value)
self.commit_seppuku()
def count_sec(self, interface, key):
"""
get a specified count for a given interface
as calculated for the last second.
Example:
| ``self.count_sec('eth0', 'bytes_sent')``
| (will return count of bytes sent in the last second)
"""
if interface == 'aggregate':
deque = self.aggregate['counters'][key]
else:
deque = self.interfaces[interface]['counters'][key]
if len(deque) < 2: # not enough data
return 0
elif self.app.config['FPS'] < 2:
# fps < 1 means data covers 1/fps seconds
return (deque[-1] - deque[0]) / self.app.config['FPS']
else:
# last (most recent) minus first (oldest) item
return deque[-1] - deque[0]
def normalize(self, element):
"""
Exposed elements:
* ``<if>.bytes_sent``: upload of network interface ``<if>``.
* ``<if>.bytes_recv``: download of network interface ``<if>``.
`<if>` can be any local network interface as well as `'aggregate'`.
"""
if_name, key = element.split('.')
if if_name == 'aggregate':
if len(self.aggregate['counters'][key]) >= 2:
link_quality = float(self.aggregate['speed'] * 1024**2)
return (self.count_sec(if_name, key) * 8) / link_quality
elif len(self.interfaces[if_name]['counters'][key]) >= 2:
link_quality = float(self.interfaces[if_name]['stats']['speed'] * 1024**2)
return (self.count_sec(if_name, key) * 8) / link_quality
# program flow should only arrive here if we have less than 2
# datapoints in which case we can't establish used bandwidth.
return 0
def caption(self, fmt):
"""
Exposed keys:
* ``<if>.bytes_sent``: upload of network interface ``<if>``.
* ``<if>.bytes_recv``: download of network interface ``<if>``.
* ``<if>.if_up``: Boolean, whether the interface is up.
* ``<if>.speed``: interface speed in Mbit/s
* ``<if>.counters``: supplies access to interface counters. Possible sub-elements are:
* ``bytes_sent``
* ``bytes_recv``
* ``packets_sent``
* ``packets_recv``
* ``errin``
* ``errout``
* ``dropin``
* ``dropout``
`<if>` can be any local network interface as well as ``'aggregate'``.
Additionally, the ``'aggregate'`` interface exposes the total
count of network interfaces as ``if_count``.
"""
if not self.data:
return fmt
data = {}
data['aggregate'] = helpers.DotDict()
data['aggregate']['if_count'] = self.aggregate['if_count']
data['aggregate']['if_up'] = self.aggregate['if_up']
data['aggregate']['speed'] = self.aggregate['speed']
data['aggregate']['counters'] = helpers.DotDict()
for key in self.aggregate['counters'].keys():
data['aggregate']['counters'][key] = self.count_sec('aggregate', key)
if key.startswith('bytes'):
data['aggregate']['counters'][key] = helpers.pretty_bits(data['aggregate']['counters'][key]) + '/s'
for if_name in self.interfaces.keys():
data[if_name] = helpers.DotDict()
data[if_name]['addrs'] = helpers.DotDict()
all_addrs = []
for idx, addr in enumerate(self.interfaces[if_name]['addrs']):
data[if_name]['addrs'][str(idx)] = addr
all_addrs.append(addr.address)
data[if_name]['all_addrs'] = u"\n".join(all_addrs)
data[if_name]['stats'] = helpers.DotDict(self.interfaces[if_name]['stats'])
data[if_name]['counters'] = helpers.DotDict()
for key in self.interfaces[if_name]['counters'].keys():
data[if_name]['counters'][key] = self.count_sec(if_name, key)
if key.startswith('bytes'):
data[if_name]['counters'][key] = helpers.pretty_bits(data[if_name]['counters'][key]) + '/s'
return fmt.format(**data)
class BatteryMonitor(Monitor):
"""
Monitor laptop batteries.
"""
collector_type = collectors.BatteryCollector
def normalize(self, element):
"""
This function exposes no explicit elements, but always just returns
the current fill of the battery.
"""
# TODO: multi-battery support? needs support by psutil…
if not self.data:
return 0
return self.data.percent / 100.0
def caption(self, fmt):
"""
Exposed keys:
* ``power_plugged``: Boolean, whether the AC cable is connected.
* ``percent``: current fill of the battery in percent.
* ``secsleft``: seconds left till battery is completely drained.
* ``state``: Current state of the battery, one of ``'full'``, ``'charging'`` or ``'draining'``.
"""
if not self.data:
return fmt
data = self.data._asdict()
data['percent'] = helpers.pretty_si(data['percent'])
if data['secsleft'] == psutil.POWER_TIME_UNLIMITED:
data['secsleft'] = ''
elif data['secsleft'] == psutil.POWER_TIME_UNKNOWN:
data['secsleft'] = 'unknown'
else:
data['secsleft'] = helpers.pretty_time(data['secsleft'])
if not data['power_plugged']:
data['state'] = 'draining'
elif data['percent'] == 100:
data['state'] = 'full'
else:
data['state'] = 'charging'
return fmt.format(**data)
class DiskMonitor(Monitor):
"""
Monitors disk I/O and partitions.
"""
collector_type = collectors.DiskCollector
def __init__(self, *args, **kwargs):
super(DiskMonitor, self).__init__(*args, **kwargs)
self.normalization_values = {}
io = psutil.disk_io_counters(perdisk=True)
for disk, info in io.items():
#self.normalization_values[disk] = info._asdict()
for key, value in info._asdict().items():
element = '.'.join(['io', disk, key])
if key.endswith('_bytes'):
self.normalization_values[element] = 100 * 1024 ** 2 # assume baseline ability of 100 MByte/s
elif key.endswith('_count'):
self.normalization_values[element] = 0 # FIXME: I have 0 clue what a reasonable baseline here is
elif key.endswith('_time'):
self.normalization_values[element] = 1000 # one second of data, collector reports data in milliseconds/s
else:
self.normalization_values[element] = 0
def normalize(self, element):
"""
Elements exposed:
* ``io``
* Valid subelements are disk device file names as found in
``/dev``. Examples: ``ada0``, ``sda``.
Valid subsubelements are as follows:
* ``read_bytes``
* ``write_bytes``
* ``read_time``
* ``write_time``
* ``busy_time``
* ``partitions``
* Valid subelements are partition device file names as
found in ``/dev``, with dots (``.``) being replaced
with dashes (``-``). Examples: ``root-eli``, ``sda1``.
"""
parts = element.split('.')
if parts[0] == 'io':
disk, key = parts[1:]
value = self.data['io'][disk][key]
if self.normalization_values[element] < value:
self.normalization_values[element] = value
return self.data['io'][disk][key] / self.normalization_values[element]
elif parts[0] == 'partitions':
name = parts[1]
info = self.data['partitions'][name]['usage']
return info['used'] / info['total']
return 0
def caption(self, fmt):
"""
Exposed keys are the same as for :func:`DiskMonitor.normalize`.
"""
data = helpers.DotDict()
if 'partitions' in self.data and 'io' in self.data:
data['io'] = helpers.DotDict()
for name, diskinfo in self.data['io'].items():
data['io'][name] = helpers.DotDict()
for key, value in diskinfo.items():
if key.endswith('_bytes'):
value = helpers.pretty_bytes(value)
data['io'][name][key] = value
data['partitions'] = helpers.DotDict()
for name, partition in self.data['partitions'].items():
part_data = helpers.DotDict()
for key in ['name', 'device', 'mountpoint', 'fstype', 'opts']:
part_data[key] = partition[key]
part_data['usage'] = helpers.DotDict(partition['usage'])
data['partitions'][name] = part_data
return fmt.format(**data)
return fmt
class NetdataMonitor(Monitor):
"""
Monitor that interfaces with (remote) netdata instances.
"""
collector_type = collectors.NetdataCollector
def __init__(self, app, component, host, port):
self.collector_type = functools.partial(self.collector_type, host=host, port=port)
super(NetdataMonitor, self).__init__(app, component)
self.charts = set()
self.normalization_values = {} # keep a table of known maximums because netdata doesn't supply absolute normalization values
#self.info_last_try = time.time()
#try:
# self.netdata_info = self.collector.client.charts()
#except netdata.NetdataException as e:
# print(f"Couldn't get chart overview from netdata host {host}!")
# self.netdata_info = None
# self.defective = True
self.defective = True
self.info_last_try = 0
self.netdata_info = False
self.almost_fixed = False
def __repr__(self):
return f"<{self.__class__.__name__} host={self.collector.client.host} port={self.collector.client.port}>"
def register_elements(self, elements):
for element in elements:
parts = element.split('.')
chart = '.'.join(parts[:2])
if not chart in self.charts:
self.normalization_values[chart] = 0
if self.netdata_info:
if not chart in self.netdata_info['charts']:
raise ValueError(f"Invalid chart: {chart} on netdata instance {self.host}:{self.port}!")
chart_info = self.netdata_info['charts'][chart]
if chart_info['units'] == 'percentage':
self.normalization_values[chart] = 100
else:
self.normalization_values[chart] = 0
self.charts.add(chart)
def run(self):
#while self.collector.is_alive():
while not self.seppuku:
try:
(chart, data) = self.queue_data.get(timeout=1/self.app.config['FPS'])
self.data[chart] = data
l = len(data['data'][0])
if(l < 2):
print(f"Missing data, marking {self} as defective.")
self.defective = True
else:
values = data['data'][0][1:]
if self.defective:
pass
elif values[0] is None: # ignore "dead" datapoints
print(f"Got dead datapoint, marking {self} as defective.")
self.defective = True
elif self.netdata_info['charts'][chart]['units'] == 'percentage':
self.normalization_values[chart] = 100 # in case self was defective when register_elements was called
else:
cumulative_value = sum(data['data'][0][1:])
if self.normalization_values[chart] < cumulative_value:
self.normalization_values[chart] = cumulative_value
except queue.Empty:
continue # try again
self.commit_seppuku()
def tick(self):
if self.defective:
t = time.time()
if t >= self.info_last_try + self.app.config['NETDATA_RETRY']:
if self.almost_fixed:
print(f"{self.__class__.__name__} instance almost fixed, ignoring first chart overview because of netdata weirdness.")
else:
print(f"{self.__class__.__name__} instance currently defective, trying to get netdata overview from {self.collector.client.host}.")
self.info_last_try = t
try:
self.netdata_info = self.collector.client.charts()
if self.almost_fixed:
self.defective = False
self.almost_fixed = False
self.tick() # do the actual tick (i.e. the else clause)
if not self.defective: # in case defective was re-set in because of dead datapoints
print("Success!")
else:
self.almost_fixed = True
except netdata.NetdataException as e:
print(f"Failed, will retry in {self.app.config['NETDATA_RETRY']} seconds.")
else:
if not self.queue_update.full():
#if not self.seppuku: # don't request more updates to collector when we're trying to die
for chart in self.charts:
self.queue_update.put(f"UPDATE {chart}", block=True)
def normalize(self, element):
"""
Exposed elements correspond to *chart names* and their datapoint
*dimension*\s. For a list of valid chart and dimensions names, consult
``/api/v1/charts`` of the netdata instance in question.
Examples:
* ``system.cpu.nice``
* ``disk.ada0.writes``
"""
if self.defective:
return 0
else:
parts = element.split('.')
chart = '.'.join(parts[:2])
#if chart not in self.charts or not self.data[chart]:
if not chart in self.data:
#print(f"No data for {chart}")
return 0 #
#timestamp = self.data[chart]['data'][0][0] # first element of a netdata datapoint is always time
#if timestamp > self.last_updates[chart]:
subelem = parts[2]
subidx = self.data[chart]['labels'].index(subelem)
value = self.data[chart]['data'][0][subidx]
if value >= self.normalization_values[chart]:
self.normalization_values[chart] = value
if self.normalization_values[chart] == 0:
return 0
r = value / self.normalization_values[chart]
return r
def caption(self, fmt):
"""
Exposed keys are the same as for :func:`NetdataMonitor.normalize`.
"""
if not self.data or self.defective:
return fmt
data = helpers.DotDict()
for chart_name, chart_data in self.data.items():
chart_keys = chart_name.split('.')
unit = self.netdata_info['charts'][chart_name]['units'] # called "units" but actually only ever one. it's a string.
if not chart_keys[0] in data:
data[chart_keys[0]] = helpers.DotDict()
d = helpers.DotDict()
for idx, label in enumerate(chart_data['labels']):
value = chart_data['data'][0][idx]
if value == None:
value = 0
elif unit == 'bytes':
value = helpers.pretty_bytes(value)
elif unit.startswith('kilobytes'):
postfix = unit[9:]
value = helpers.pretty_bytes(value * 1024) + postfix
elif unit.startswith('kilobits'):
postfix = unit[8:]
value = helpers.pretty_bits(value * 1024) + postfix
else:
value = f"{value} {unit}"
d[label] = value
data[chart_keys[0]][chart_keys[1]] = d
return fmt.format(**data)

72
gulik/palettes.py Normal file
View File

@ -0,0 +1,72 @@
# -*- coding: utf-8 -*-
def hue(base, count, distance=180):
"""
Creates a hue-rotation palette.
Parameters:
base (:class:`Color`): Color on which the palette will be based (i.e. the starting point of the hue-rotation).
count (int): number of colors the palette should hold.
distance (int or float): angular distance on a 360° hue circle thingamabob.
Returns:
list: A list of length **count** of :class:`Color` objects.
"""
if count == 1:
return [base]
palette = []
for i in range(0, count):
color = base.clone()
color.hue += i/(count - 1) * distance
palette.append(color)
return palette
def value(base, count, min=None, max=None):
"""
Creates a value-stepped palette
Parameters:
base (:class:`Color`): Color on which the palette will be based (i.e. source of hue and saturation)
count (int): number of colors the palette should hold
min (float >= 0 and <= 1): minimum value (the v in hsv)
max (float >= 0 and <= 1): maximum value
Returns:
list: A list of length **count** of :class:`Color` objects.
"""
if count == 1:
return [base]
if min is None:
if 0.2 > base.value:
min = base.value
else:
min = 0.2
if max is None:
if 0.6 < base.value:
max = base.value
else:
max = 0.6
span = max - min
step = span / (count - 1)
palette = []
for i in range(0, count):
color = base.clone()
color.value = max - i * step
palette.append(color)
return palette

29
gulik/patterns.py Normal file
View File

@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
import cairo
def stripe45(color):
"""
A tilable pattern with 45° stripes.
"""
surface = cairo.ImageSurface(cairo.Format.ARGB32, 10, 10)
context = cairo.Context(surface)
context.set_source_rgba(*color.tuple_rgba())
context.move_to(5, 5)
context.line_to(10, 0)
context.line_to(10, 5)
context.line_to(5, 10)
context.line_to(0, 10)
context.line_to(5, 5)
context.close_path()
context.fill()
context.move_to(0, 0)
context.line_to(5, 0)
context.line_to(0, 5)
context.close_path()
context.fill()
return surface

1236
gulik/visualizers.py Normal file

File diff suppressed because it is too large Load Diff