Commit 94f04ff3 authored by Julius Metz's avatar Julius Metz

custom configs and doku

parent 53da944e
......@@ -8,6 +8,7 @@ import time
import datetime
import multiprocessing as mp
import shutil
import importlib
import click
import plotly
......@@ -16,8 +17,26 @@ from yattag import Doc
import filter_func
import value_merger
from Collectl2plotly_Config import *
from Collectl2plotly_Plots_Config import *
import Collectl2plotly_Plots_Config as default_plot_conf
### Filter Configs
# registered filter functions
# filter functions must be in filter_func.py
# key : Display name
# value: function name
FILTER_FUNCTIONS = {'hardvalue': 'filter_hardvalue', 'average': 'filter_average'}
# Default filter function if no explicit is given
# must be a display name of a function
DEFAULT_FILTER = 'average'
### validation Config
# required Config values
CONFIG_VARIABLES = ['NEEDED_VALUES_DEFAULTS', 'NEEDED_VALUES', 'PLOTS_CONFIG',
'NAME_SPEZIAL_PARAMETER', 'COMAND_BLACKLIST', 'PLOTLY_COLORS', 'PLOTLY_STATIC_COLORS']
......@@ -37,7 +56,7 @@ def datestr2date(datestr):
)
def get_cmdname(cmd, coarsest=False):
def get_cmdname(cmd, spezial_parameters_of_all, coarsest=False):
"""search in complete commandstring the name of the skript or the command that is used
Arguments:
......@@ -54,8 +73,8 @@ def get_cmdname(cmd, coarsest=False):
bash_function = cmd_splited[0].split('/')[-1]
bash_function = re.search(r'[^\W\n]+', bash_function).group(0)
# check if bash_function is known, if not, return bash_function
spezial_parameter = NAME_SPEZIAL_PARAMETER_CONFIG.get(bash_function, None)
if coarsest or spezial_parameter is None:
spezial_parameters = spezial_parameters_of_all.get(bash_function, None)
if coarsest or spezial_parameters is None:
return bash_function
skip = False
# search script/program name within the parameters and only return this without path
......@@ -63,7 +82,7 @@ def get_cmdname(cmd, coarsest=False):
if skip:
skip = False
continue
if parameter in spezial_parameter:
if parameter in spezial_parameters:
skip = True
continue
if bash_function == 'bash' or bash_function == 'sh' and parameter == '-c':
......@@ -75,7 +94,7 @@ def get_cmdname(cmd, coarsest=False):
return cmd
def parse_file(path, collectl, shorten_cmds, coarsest):
def parse_file(path, collectl, shorten_cmds, coarsest, config):
"""start subproccess collectl than parse the output and merge.
After that build usefull dict from parsed data
......@@ -84,9 +103,13 @@ def parse_file(path, collectl, shorten_cmds, coarsest):
collectl {str} -- collectl command
shorten_cmds {bool} -- if True cmd will be shorted by get_cmdname
coarsest {bool} -- parameter of get_cmdname
config {dict} -- plots and merge Config
Returns:
(dict, dict) -- 1.: parsed_data 2.: data for filter function
(dict, dict) -- 1. plot_data = {comands : {metrics: [values, ...], ...}, ...}
2. data for filter function :
{'number_of_values': X, 'commands':{ cmd:{'number_of_values': X, metrics:SUM, ...}}, metrics:SUM, ...}
"""
collectl_starttime = time.time()
# run collectl in playback mode and read output into list
......@@ -114,14 +137,16 @@ def parse_file(path, collectl, shorten_cmds, coarsest):
empty_dict_with_value_titles = {
value_title: copy.deepcopy(
value_title_settings.get(
'default', NEEDED_VALUES_DEFAULTS['default_value']
'base_value', config['NEEDED_VALUES_DEFAULTS']['default_base_value']
)
) for value_title, value_title_settings in NEEDED_VALUES.items()
) for value_title, value_title_settings in config['NEEDED_VALUES'].items()
}
# parse all output lines
entrys_data = {}
cmd_cmdshort_dict = {}
merger_lookup_dict = {}
for entry in output:
# split by ' ' (exclude command from splitting)
splited_entry = entry.split(' ', len(head_indexes_dict)-1)
......@@ -131,10 +156,10 @@ def parse_file(path, collectl, shorten_cmds, coarsest):
if cmd in cmd_cmdshort_dict:
cmd = cmd_cmdshort_dict[cmd]
else:
short_cmd = get_cmdname(cmd, coarsest=coarsest)
short_cmd = get_cmdname(cmd, config['NAME_SPEZIAL_PARAMETER'], coarsest=coarsest)
cmd_cmdshort_dict[cmd]= short_cmd
cmd = short_cmd
if cmd in COMAND_BLACKLIST:
if cmd in config['COMAND_BLACKLIST']:
continue
# create dict for each command
if not cmd in entrys_data:
......@@ -150,13 +175,14 @@ def parse_file(path, collectl, shorten_cmds, coarsest):
empty_dict_with_value_titles)
# get values from data as given in NEEDED_VALUES and run specified merger function
# to merge multiple values with same timestamp or rescale (e.g. to GB)
for value_title, value_title_settings in NEEDED_VALUES.items():
entrys_data[cmd][tmp_datetime][value_title] = getattr(
value_merger,
value_title_settings.get('merger', NEEDED_VALUES_DEFAULTS['default_merger']),
)(
for value_title, value_title_settings in config['NEEDED_VALUES'].items():
merger, merger_kwargs = value_title_settings.get('merger', config['NEEDED_VALUES_DEFAULTS']['default_merger'])
if not merger in merger_lookup_dict:
merger_lookup_dict[merger] = getattr(value_merger, merger)
entrys_data[cmd][tmp_datetime][value_title] = merger_lookup_dict[merger](
entrys_data[cmd][tmp_datetime][value_title],
*[splited_entry[head_indexes_dict[key]] for key in value_title_settings['keys']],
**merger_kwargs
)
print('parsing/merge took {:.1f}s'.format(time.time() - parsing_starttime))
dictbuild_starttime = time.time()
......@@ -171,8 +197,7 @@ def parse_file(path, collectl, shorten_cmds, coarsest):
plot_filter_data['commands'][cmd] = copy.deepcopy(
empty_dict_with_value_titles)
plot_filter_data['commands'][cmd]['number_of_values'] = 0
entry_data_plotfriendly[cmd] = {key: []
for key in NEEDED_VALUES}
entry_data_plotfriendly[cmd] = {key: [] for key in config['NEEDED_VALUES']}
entry_data_plotfriendly[cmd]['datetime'] = []
for cmd_data_time, cmd_data_values in cmd_data.items():
entry_data_plotfriendly[cmd]['datetime'].append(cmd_data_time)
......@@ -226,17 +251,34 @@ def make_plot(cmds_data, filter_info, cmd_color, plot_settings, plotly_format_va
'layout': copy.deepcopy(plot_settings['layout']),
}
if 'title' in plot['layout']:
plot['layout']['title'] = plot['layout']['title'].format(**plotly_format_vars)
if type(plot['layout']['title']) == str:
plot['layout']['title'] = plot['layout']['title'].format(
**plotly_format_vars,
)
else:
plot['layout']['title']['text'] = plot['layout']['title']['text'].format(
**plotly_format_vars,
)
if 'yaxis' in plot['layout'] and 'title' in plot['layout']['yaxis']:
plot['layout']['yaxis']['title'] = plot['layout']['yaxis']['title'].format(
**plotly_format_vars,
)
if type(plot['layout']['yaxis']['title']) == str:
plot['layout']['yaxis']['title'] = plot['layout']['yaxis']['title'].format(
**plotly_format_vars,
)
else:
plot['layout']['yaxis']['title']['text'] = plot['layout']['yaxis']['title']['text'].format(
**plotly_format_vars,
)
if 'xaxis' in plot['layout'] and 'title' in plot['layout']['xaxis']:
plot['layout']['xaxis']['title'] = plot['layout']['xaxis']['title'].format(
**plotly_format_vars,
)
if type(plot['layout']['xaxis']['title']) == str:
plot['layout']['xaxis']['title'] = plot['layout']['xaxis']['title'].format(
**plotly_format_vars,
)
else:
plot['layout']['xaxis']['title']['text'] = plot['layout']['xaxis']['title']['text'].format(
**plotly_format_vars,
)
return plotly.offline.plot(plot, include_plotlyjs=False, output_type='div')
......@@ -301,10 +343,7 @@ def get_sources(sources):
for source in sources:
source_path = Path(source)
if source_path.is_dir():
source_paths.extend(
[Path(source_path, sourcefile) for sourcefile in os.listdir(
source_path) if sourcefile.endswith('.raw.gz')]
)
source_paths.extend(source_path.glob('*.raw.gz'))
elif source_path.is_file() and source.endswith('.raw.gz'):
source_paths.append(source_path)
return source_paths
......@@ -322,28 +361,32 @@ def data_from_file(arguments):
filtercmds {bool} -- if True cmds will be filter else not
filtervalue {int} -- para for filter
filtertype {str} -- which filter will be called
config {dict} -- plots and merge Config
Returns:
[type] -- [description]
(str, dict, dict) -- 1. hostname
2. plot_data = {comands : {metrics: [values, ...], ...}, ...}
3. filter_infos = {metrics: [cmds, ...]
"""
path, collectl, shorten_cmds, coarsest, filtercmds, filtervalue, filtertype = arguments
path, collectl, shorten_cmds, coarsest, filtercmds, filtervalue, filtertype, config = arguments
zcat = subprocess.Popen(('zcat', str(path)), stdout=subprocess.PIPE)
host = subprocess.check_output(
('awk', '/^# Host:/{print $3;exit}'), stdin=zcat.stdout,
).decode().strip()
zcat.terminate()
data, filter_data = parse_file(path, collectl, shorten_cmds, coarsest)
data, filter_data = parse_file(path, collectl, shorten_cmds, coarsest, config)
if filtercmds:
filter_infos = getattr(filter_func, filtertype)(filter_data, filtervalue)
else:
filter_infos = {key: list(data.keys()) for key in NEEDED_VALUES}
filter_infos = {key: list(data.keys()) for key in config['NEEDED_VALUES']}
return host, data, filter_infos
@click.command(help='Generate htmlfiles with Plotlyplots with data from collectlfiles(".raw.gz")')
@click.option('--source', '-s', multiple=True, default=['.'], show_default=True, type=click.Path(exists=True), help='source for the plots. (.raw.gz file or directory with .raw.gz) multiple useable')
@click.option('--collectl', '-c', default='collectl', show_default=True, help='collectl command')
@click.option('--plotlypath', '-p', default='./plotly-latest.min.js', type=click.Path(exists=True), show_default=True, help='path to plotly.js')
@click.option('--plotlypath', '-p', type=click.Path(exists=True), help='path to plotly.js')
@click.option('--destination', '-d', default='.', type=click.Path(exists=True), show_default=True, help='path to directory where directory with plots will be created')
@click.option('--configpath', default=None, type=click.Path(exists=True), help='python file with plot and merge infos see doku for detail')
@click.option('--shorten/--notshorten', default=True, help='commands will be shorted only to name')
@click.option('--coarsest/--notcoarsest', default=False, help='commands will be shorted only to type (bash, perl, ...)')
@click.option('--filtercmds/--notfiltercmds', default=True, help='filtering or not')
......@@ -353,8 +396,9 @@ def data_from_file(arguments):
default=DEFAULT_FILTER, show_default=True,
help='Filter which is to be used.',
)
def main(source, collectl, plotlypath, destination,
shorten, coarsest, filtercmds, filtervalue, filtertype):
@click.option('--force', is_flag=True, help='override existing plot directory if exist')
def main(source, collectl, plotlypath, destination, configpath,
shorten, coarsest, filtercmds, filtervalue, filtertype, force):
source_paths = get_sources(source)
if not source_paths:
......@@ -367,8 +411,29 @@ def main(source, collectl, plotlypath, destination,
plots_dir = Path(destination, 'collectlplots')
if plots_dir.is_dir():
print('in destination "collectlplots" already exist')
exit(1)
if force:
shutil.rmtree(plots_dir)
else:
print('in destination "collectlplots" already exist')
exit(1)
config_module = default_plot_conf
if configpath:
if configpath.endswith('.py'):
config_module = importlib.import_module(configpath[:-3])
else:
print('given config isn`t a ".py" Python file')
exit(1)
# validate config and make dict from values
config = {}
for config_var_name in CONFIG_VARIABLES:
try:
config[config_var_name] = getattr(config_module, config_var_name)
except AttributeError:
print('{} missing in config'.format(config_var_name))
exit(1)
data_colllect_functions = []
......@@ -380,6 +445,7 @@ def main(source, collectl, plotlypath, destination,
filtercmds,
filtervalue,
FILTER_FUNCTIONS[filtertype],
config,
))
# use multiprocessing to parse all collectl output files independently from each other in parallel
......@@ -387,19 +453,19 @@ def main(source, collectl, plotlypath, destination,
results = pool.map(data_from_file, data_colllect_functions)
pool.close()
hosts_data = {}
cmd_all = []
cmd_all = set()
for host, data, filter_infos in results:
hosts_data[host] = {'data': data, 'filter_infos': filter_infos}
cmd_all.extend([cmd for cmds in filter_infos.values() for cmd in cmds])
cmd_colors = {cmd: PLOTLY_COLORS[i % len(PLOTLY_COLORS)] for i, cmd in enumerate(set(cmd_all))}
cmd_all.update([cmd for cmds in filter_infos.values() for cmd in cmds])
cmd_colors = {cmd: config['PLOTLY_COLORS'][i % len(config['PLOTLY_COLORS'])] for i, cmd in enumerate(sorted(list(cmd_all)))}
cmd_colors.update(config['PLOTLY_STATIC_COLORS'])
# for each host and each plot (as given in config) call make_plot to create html div with plotly
start_plots_build = time.time()
plots_dict = {}
for host, host_data in hosts_data.items():
plots_dict[host] = {config['name']: [] for config in PLOT_CONFIG}
for plot_config in PLOT_CONFIG:
plots_dict[host] = {plot_config['name']: [] for plot_config in config['PLOTS_CONFIG']}
for plot_config in config['PLOTS_CONFIG']:
plots_dict[host][plot_config['name']].append( make_plot(
host_data['data'],
host_data['filter_infos'],
......@@ -413,6 +479,8 @@ def main(source, collectl, plotlypath, destination,
# create output directory and copy plotly.js
plots_dir.mkdir()
if plotlypath is None:
plotlypath = Path(__file__).with_name('plotly-latest.min.js')
try:
shutil.copy(plotlypath, str(Path(plots_dir, 'plotly.js')))
except shutil.SameFileError:
......
"""Config of collectl2plotly
"""
### Filter Configs
# registered filter functions
# filter functions must be in filter_func.py
# key : Display name
# value: function name
FILTER_FUNCTIONS = {'hardvalue': 'filter_hardvalue', 'average': 'filter_average'}
# Default filter function if no explicit is given
# must be a display name of a function
DEFAULT_FILTER = 'average'
......@@ -2,25 +2,25 @@
# hint: merger function must be in value_merger.py
# Defaults merge infos
# default_value is the startvalue for merging if no is given in NEEDED_VALUES
# default_base_value is the startvalue for merging if no is given in NEEDED_VALUES
# default_merger is the merger function that is called if no is given in NEEDED_VALUES
NEEDED_VALUES_DEFAULTS = {
'default_value': 0,
'default_merger': 'addition_int',
'default_base_value': 0,
'default_merger': ('x2oneaddition_int', {}),
}
# Information which values needed for plots
# key : name of value needed for plotconfig
# value: is dict with following configuration options
# 'keys' : must be a list with the table heads from collectl that is to be merge
# 'merger' : function that merge the values from the given keys to the new for the plots
# 'default': startvalue for merging
# 'merger' : function that merge the values from the given keys to the new for the plots, parameter of merger
# 'base_value': startvalue for merging
NEEDED_VALUES = {
'PCT': {'keys':['PCT'], 'merger': 'x2oneaddition_float_by_100'},
'VmRSS': {'keys':['VmRSS'], 'merger': 'x2oneaddition_float_sizedown2'},
'RKBC': {'keys':['RKBC'], 'merger': 'x2oneaddition_float_sizedown1'},
'WKBC': {'keys':['WKBC'], 'merger': 'x2oneaddition_float_sizedown1'},
'syscalls': {'keys':['RSYS', 'WSYS'], 'merger': 'x2oneaddtion_int'},
'PCT': {'keys':['PCT'], 'merger': ('x2oneaddition_float', {'operator': '/', 'operator_value': 100})},
'VmRSS': {'keys':['VmRSS'], 'merger': ('x2oneaddition_float', {'operator': '/', 'operator_value': 1048576})},
'RKBC': {'keys':['RKBC'], 'merger': ('x2oneaddition_float', {'operator': '/', 'operator_value': 1024})},
'WKBC': {'keys':['WKBC'], 'merger': ('x2oneaddition_float', {'operator': '/', 'operator_value': 1024})},
'syscalls': {'keys':['RSYS', 'WSYS'], 'merger': ('x2oneaddition_int', {})},
}
......@@ -36,7 +36,7 @@ NEEDED_VALUES = {
barchart_buttons = {
'annotations': [
{'text': 'time interval in Barchart:',
'x': 0.2, 'y': 1.13, 'yref':'paper', 'xref':'paper', 'showarrow':False}
'x': 0.2, 'y': 1.13, 'yref': 'paper', 'xref': 'paper', 'showarrow': False}
],
'updatemenus':[
{
......@@ -48,7 +48,7 @@ barchart_buttons = {
{'args':[{'type': 'scattergl'}, {}],
'label':'dots',
'method':'update'},
{'args':[{'type': 'histogram', 'histfunc': 'sum'}, {'barmode': 'stack'}],
{'args':[{'type': 'histogram', 'histfunc': 'avg'}, {'barmode': 'stack'}],
'label':'barchart',
'method':'update'},
],
......@@ -105,7 +105,7 @@ barchart_buttons = {
],
}
PLOT_CONFIG = [{
PLOTS_CONFIG = [{
'name': 'CPU load',
'needed_key': 'PCT',
'plotly_settings': {
......@@ -115,9 +115,10 @@ PLOT_CONFIG = [{
},
'layout': {
'height': 500,
'title': 'CPU load {host}',
'title': {'text': 'CPU load {host}', 'y': 1},
'xaxis': {'title': 'Date'},
'yaxis': {'title': 'CPU load'},
'showlegend': True,
**barchart_buttons
},
},
......@@ -132,9 +133,10 @@ PLOT_CONFIG = [{
},
'layout': {
'height': 500,
'title': 'Memory Usage {host}',
'title': {'text': 'Memory Usage {host}', 'y': 1},
'xaxis': {'title': 'Date'},
'yaxis': {'title': 'RAM usage GiB'},
'showlegend': True,
**barchart_buttons
},
},
......@@ -149,9 +151,10 @@ PLOT_CONFIG = [{
},
'layout': {
'height': 500,
'title': 'I/O read {host}',
'title': {'text': 'I/O read {host}', 'y': 1},
'xaxis': {'title': 'Date'},
'yaxis': {'title': 'I/O MiB/s', 'type': 'log'},
'showlegend': True,
**barchart_buttons
},
},
......@@ -166,9 +169,10 @@ PLOT_CONFIG = [{
},
'layout': {
'height': 500,
'title': 'I/O write {host}',
'title': {'text': 'I/O write {host}', 'y': 1},
'xaxis': {'title': 'Date'},
'yaxis': {'title': 'I/O MiB/s', 'type': 'log'},
'showlegend': True,
**barchart_buttons
},
},
......@@ -183,9 +187,10 @@ PLOT_CONFIG = [{
},
'layout': {
'height': 500,
'title': 'I/O syscalls {host}',
'title': {'text': 'I/O syscalls {host}', 'y': 1},
'xaxis': {'title': 'Date'},
'yaxis': {'title': 'I/O syscalls/s', 'type': 'log'},
'showlegend': True,
**barchart_buttons
},
},
......@@ -198,7 +203,7 @@ PLOT_CONFIG = [{
# dict of all commands where the name will be filtered out
# key : command
# value: list of parameter that take a value what is not the name
NAME_SPEZIAL_PARAMETER_CONFIG = {
NAME_SPEZIAL_PARAMETER = {
'java': ['-cp', '-classpath'],
'bash': [],
'sh': [],
......@@ -219,3 +224,5 @@ PLOTLY_COLORS=['rgb(31, 119, 180)', 'rgb(255, 127, 14)',
'rgb(148, 103, 189)', 'rgb(140, 86, 75)',
'rgb(227, 119, 194)', 'rgb(127, 127, 127)',
'rgb(188, 189, 34)', 'rgb(23, 190, 207)']
PLOTLY_STATIC_COLORS = {}
\ No newline at end of file
......@@ -18,14 +18,279 @@ yattag=<1.13.2
## Options
| Option | takes | Description | Default |
| Option | Takes | Description | Default |
| --------------- | ----- | ------------ | ------- |
| -s, --source | a path to a directory with .raw.gz or .raw.gz file directly. | It is multiple useable! It will be used as collectl sources to get the Plotdata. | If no sources is given as parameter it search in the current dir |
| -c, --collectl | how to call collectl | is the collectl that will be called to get the data from the sources | collectl without a path |
| -p, --plotlypath | a path to a plotly javascript libery | is needed for the plot in the html files | ./plotly-latest.min.js |
| -p, --plotlypath | a path to a plotly javascript libery | is needed for the plot in the html files | plotly.js next to skript |
| -d, --destination | a path to a directory | is where a directory with the html files will be created | current directory |
| --configpath | a path to config file | is a python file with the plot and merge settings see [here](config)| a default config |
| --shorten /<br> --notshorten | - | enable or disable shorten of commands with parameters/options only to file/command names. <br> examples:<br> python ~/scripts/script.py 1 --> script.py <br>ls -lisa --> ls | enabled |
| --coarsest /<br> --notcoarsest | - | enable or disable shorten of commands only to command names.<br> If enabled --shorten is ignored!<br> examples: <br> python ~/scripts/script.py 1 --> python <br> ls -lisa --> ls | disabled |
| --filtercmds / --notfiltercmds | - | enable or disable filtering | enabled |
| --filtertype | filtertype <br> see --help for detail | to select which filter to be used | see --help |
| --filtervalue | any value (string, int, ...) | is passed to the filter function as string| - |
| --filtervalue | any value (string, int, ...) | is passed to the filter function as string | - |
| --force | - | override existing plot directory if exist | - |
## Config
The config is python file with following varibles to create spezial plots or/and with different values instead of the default.
#### NEEDED_VALUES
Specifies which values are merged under which names in a dict.
As key is the name of the merged values and the dict value is config with:
**'keys'**:<br>
This are a list of collectl table headers of the values which are given into the merger function.
**'merger'**:<br>
Is a tuple with the merger function name and a dict with the parameter of the merger. See [merger](#merger) to know what merger and parameter exist.
**'base_value'**:<br>
Is the startvalue of merger.
If 'merger' or 'base_value' is not given it use the default from [NEEDED_VALUES_DEFAULTS](#needed_values_defaults).
Example:
```python
NEEDED_VALUES = {
'CPU': {
'keys':['PCT'],
'merger': ('x2oneaddition_float', {'operator': '/', 'operator_value': 100}),
'base_value': 0.0,
},
'syscalls': {
'keys':['RSYS', 'WSYS'],
'merger': ('x2oneaddition_int', {}),
},
}
```
#### NEEDED_VALUES_DEFAULTS
Defines the defaults of NEEDED_VALUES.
Example:
```python
NEEDED_VALUES_DEFAULTS = {
'default_base_value': 0,
'default_merger': ('x2oneaddition_int', {}),
}
```
#### NAME_SPEZIAL_PARAMETER_CONFIG
Specifies for which commands a script/program name is searched (dict key)and
which parameters the command has, that takes another value that is not the name (dict value). This is required to identify the name.
Important:<br>
Versions and path are ignored on the command example: python3 and /bin/python2 are both only python.
Bash -c and sh -c are an exception and are treated separately in the code.
Example:
```python
NAME_SPEZIAL_PARAMETER_CONFIG = {
'java': ['-cp', '-classpath'],
'bash': [],
'sh': [],
'perl': [],
'python': ['-m', '-W', '-X', '--check-hash-based-pycs', '-c'],
}
```
#### COMAND_BLACKLIST
Is a list of commands which are ignored. It use the shorted command to check it is in blacklist or not.
Example:
```python
COMAND_BLACKLIST = [
'collectl',
]
```
#### PLOTS_CONFIG
Is a list of plots settings of the plots to be displayed on the pages.
```
PLOTS_CONFIG = [
{Plot1},
{Plot2},
...
]
```
A plot settings is dict with following keys:
**'name'**:<br>
Is the name of the Plot this will be displayed in the index of the websites.
**'needed_key'**:<br>
Is a name from NEEDED_VALUES this values are used as y axis data in the plot.
**'plotly_settings'**:<br>
Is a settings dict in Plotly style, here it is possible to set everything about the appearance of the plots.<br>
Hints for plotly_settings:
+ Use [Plotly wiki](https://plot.ly/javascript/) javascript to search settings because the javascript objekt looks same like the python dict.
+ If you use same settings in multiple plots you can put in a dict variable first and included on the right possition like "**variable".
+ {host} will replaced with the hostname in title of plot and in y/xaxis.
Example:
```python
PLOTS_CONFIG = [
{
'name': 'CPU load',
'needed_key': 'CPU',
'plotly_settings': {
'data': {
'type': 'scattergl',
'mode': 'markers',
},
'layout': {
'height': 500,
'title': {'text': 'CPU load {host}', 'y': 1},
'xaxis': {'title': 'Date'},
'yaxis': {'title': 'CPU load'},
'showlegend': True,
**barchart_buttons
},
},
},
]
```
#### PLOTLY_COLORS
Is a list of colors to be used for the plots.
Repeats itself when there are not enough colors for the plot.
Example:
```python
PLOTLY_COLORS=[
'rgb(31, 119, 180)', 'rgb(255, 127, 14)',
'rgb(44, 160, 44)', 'rgb(214, 39, 40)',
'rgb(148, 103, 189)', 'rgb(140, 86, 75)',
'rgb(227, 119, 194)', 'rgb(127, 127, 127)',
'rgb(188, 189, 34)', 'rgb(23, 190, 207)',
]
```
#### PLOTLY_STATIC_COLORS
Is to give one comand a specific Color.
Key is the displayed name in plot.<br>
Value is color as string.
Example:
```python
PLOTLY_STATIC_COLORS = {
'example.py': 'rgb(254, 1, 154)',