Compare commits
3 Commits
Author | SHA1 | Date |
---|---|---|
phryk | d097434c86 | |
phryk | a2144984b9 | |
phryk | 3e7deb94ce |
416
doc/man/gulik.1
416
doc/man/gulik.1
|
@ -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.
|
||||
.
|
||||
|
|
|
@ -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 |
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
==================
|
||||
|
||||
|
|
2956
gulik/__init__.py
2956
gulik/__init__.py
File diff suppressed because it is too large
Load Diff
|
@ -1 +1 @@
|
|||
version='0.0.0.1'
|
||||
version='0.0.0.2'
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
@ -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__}"
|
|
@ -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)
|
||||
|
||||
|
|
@ -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
|
|
@ -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
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue