Skip to content

Commit 129bdc8

Browse files
author
Davide Schiera
authored
Fix dashboard functionalities (#84)
1 parent 8057300 commit 129bdc8

File tree

4 files changed

+157
-35
lines changed

4 files changed

+157
-35
lines changed

examples/create_dashboard.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def usage():
2929
usage()
3030

3131
# Name for the dashboard to create
32-
dashboardName = "API test - cassandra in prod"
32+
dashboardName = "Overview by Process"
3333
for opt, arg in opts:
3434
if opt in ("-d", "--dashboard"):
3535
dashboardName = arg
@@ -55,8 +55,7 @@ def usage():
5555
# in Sysdig Cloud Explore page.
5656
# You can also refer to AWS tags by using "cloudProvider.tag.*" metadata or
5757
# agent tags by using "agent.tag.*" metadata
58-
dashboardFilter = "kubernetes.namespace.name = prod and proc.name = cassandra"
59-
58+
dashboardFilter = "kubernetes.namespace.name = prod"
6059
print('Creating dashboard from view')
6160
ok, res = sdclient.create_dashboard_from_view(dashboardName, viewName, dashboardFilter)
6261
#
@@ -74,9 +73,9 @@ def usage():
7473
#
7574

7675
# Name of the dashboard to copy
77-
dashboardCopy = "Copy Of {}".format(dashboardName)
76+
dashboardCopy = "Copy of {}".format(dashboardName)
7877
# Filter to apply to the new dashboard. Same as above.
79-
dashboardFilter = "kubernetes.namespace.name = dev and proc.name = cassandra"
78+
dashboardFilter = "kubernetes.namespace.name != prod"
8079

8180
print('Creating dashboard from dashboard')
8281
ok, res = sdclient.create_dashboard_from_dashboard(dashboardCopy, dashboardName, dashboardFilter)

examples/dashboard_scope.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
#!/usr/bin/env python
2+
#
3+
# This example shows some examples of scope you can use for dashboards.
4+
#
5+
6+
import getopt
7+
import os
8+
import sys
9+
sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])), '..'))
10+
from sdcclient import SdcClient
11+
12+
13+
#
14+
# Scopes can be passed to most of dashboard-related functions, e.g. create_dashboard_from_file.
15+
#
16+
# NOTE: convert_scope_string_to_expression should never be used in a user script
17+
# We're going to use it here just to demonstrate some scope options and some constraints
18+
#
19+
def evaluate(scope, expected):
20+
parsed_scope = SdcClient.convert_scope_string_to_expression(scope)
21+
print '{} is valid: {}'.format(scope, parsed_scope[0] == True)
22+
23+
if parsed_scope[0] != expected:
24+
print('Unexpected parsing result!')
25+
sys.exit(1)
26+
27+
28+
# simple example: tag = value
29+
evaluate('proc.name = "cassandra"', True)
30+
31+
# NOTE: For now you can still leave values without quotes.
32+
# The API will be more strict, so please make sure you adopt the new format!
33+
evaluate('proc.name = cassandra', True)
34+
35+
# other operators
36+
evaluate('proc.name != "cassandra"', True)
37+
evaluate('proc.name starts with "cassandra"', True)
38+
evaluate('proc.name contains "cassandra"', True)
39+
40+
# list operators
41+
evaluate('proc.name in ("cassandra", "mysql")', True)
42+
43+
# not-ed expressions
44+
evaluate('not proc.name starts with "cassandra"', True)
45+
evaluate('not proc.name contains "cassandra"', True)
46+
evaluate('not proc.name in ("cassandra", "mysql")', True)
47+
48+
# you can combine multiple expressions; note that only AND'd scopes are currently supported
49+
evaluate('kubernetes.service.name = "database" and proc.name = "cassandra"', True)
50+
51+
# the scope can obviously be omitted in the dashboard configuration
52+
evaluate('', True)
53+
evaluate(None, True)
54+
55+
# invalid scopes will cause errors
56+
evaluate('proc.name == "cassandra"', False) # invalid operator
57+
58+
# currently, one space is required around operands and operators -- improvements will come soon
59+
evaluate('proc.name="cassandra"', False)
60+
61+
#
62+
# The current grammer is unable to validate all errors -- in these cases, the API will fail!
63+
# Improvements will come soon!
64+
#
65+
# Here some errors that will not be detected by the Python library, but the API will
66+
#
67+
evaluate('proc.name = "cassandra" or proc.name = "mysql"', True) # not AND'd expressions are supported
68+
evaluate('proc.name in ("cassandra\', \'mysql")', True) # mismatching quotes
69+
evaluate('proc.name in ("cassandra", "mysql"', True) # missing parenthesis

sdcclient/_monitor.py

Lines changed: 83 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import json
22
import copy
33
import requests
4+
import re
45

56
from sdcclient._common import _SdcCommon
67

@@ -430,31 +431,10 @@ def add_dashboard_panel(self, dashboard, name, panel_type, metrics, scope=None,
430431
'groupAggregation': metric['aggregations']['group'] if 'aggregations' in metric else None,
431432
'propertyName': property_name + str(i)
432433
})
433-
#
434-
# Convert scope to format used by Sysdig Monitor
435-
#
436-
if scope != None:
437-
filter_expressions = scope.strip(' \t\n\r?!.').split(" and ")
438-
filters = []
439-
440-
for filter_expression in filter_expressions:
441-
values = filter_expression.strip(' \t\n\r?!.').split("=")
442-
if len(values) != 2:
443-
return [False, "invalid scope format"]
444-
filters.append({
445-
'metric': values[0].strip(' \t\n\r?!.'),
446-
'op': '=',
447-
'value': values[1].strip(' \t\n\r"?!.'),
448-
'filters': None
449-
})
450-
451-
if len(filters) > 0:
452-
panel_configuration['filter'] = {
453-
'filters': {
454-
'logic': 'and',
455-
'filters': filters
456-
}
457-
}
434+
435+
panel_configuration['scope'] = scope
436+
# if chart scope is equal to dashboard scope, set it as non override
437+
panel_configuration['overrideFilter'] = ('scope' in dashboard and dashboard['scope'] != scope) or ('scope' not in dashboard and scope != None)
458438

459439
#
460440
# Configure panel type
@@ -580,17 +560,32 @@ def create_dashboard_from_template(self, dashboard_name, template, scope, shared
580560
template['isPublic'] = public
581561
template['publicToken'] = None
582562

583-
#
584563
# set dashboard scope to the specific parameter
585-
# NOTE: Individual panels might override the dashboard scope, the override will NOT be reset
586-
#
564+
scopeExpression = self.convert_scope_string_to_expression(scope)
565+
if scopeExpression[0] == False:
566+
return scopeExpression
587567
template['filterExpression'] = scope
568+
template['scopeExpressionList'] = map(lambda ex: {'operand':ex['operand'], 'operator':ex['operator'],'value':ex['value'],'displayName':'', 'isVariable':False}, scopeExpression[1])
569+
570+
if 'widgets' in template and template['widgets'] is not None:
571+
# Default dashboards (aka Explore views) specify panels with the property `widgets`,
572+
# while custom dashboards use `items`
573+
template['items'] = list(template['widgets'])
574+
del template['widgets']
588575

589-
if 'items' in template:
576+
# NOTE: Individual panels might override the dashboard scope, the override will NOT be reset
577+
if 'items' in template and template['items'] is not None:
590578
for chart in template['items']:
591-
if 'overrideFilter' in chart and chart['overrideFilter'] == False:
579+
if 'overrideFilter' not in chart:
580+
chart['overrideFilter'] = False
581+
582+
if chart['overrideFilter'] == False:
592583
# patch frontend bug to hide scope override warning even when it's not really overridden
593584
chart['scope'] = scope
585+
586+
# if chart scope is equal to dashboard scope, set it as non override
587+
chart_scope = chart['scope'] if 'scope' in chart else None
588+
chart['overrideFilter'] = chart_scope != scope
594589

595590
if 'annotations' in template:
596591
template['annotations'].update(annotations)
@@ -754,6 +749,64 @@ def get_metrics(self):
754749
res = requests.get(self.url + '/api/data/metrics', headers=self.hdrs, verify=self.ssl_verify)
755750
return self._request_result(res)
756751

752+
@staticmethod
753+
def convert_scope_string_to_expression(scope):
754+
'''**Description**
755+
Internal function to convert a filter string to a filter object to be used with dashboards.
756+
'''
757+
#
758+
# NOTE: The supported grammar is not perfectly aligned with the grammar supported by the Sysdig backend.
759+
# Proper grammar implementation will happen soon.
760+
# For practical purposes, the parsing will have equivalent results.
761+
#
762+
763+
if scope is None or not scope:
764+
return [True, []]
765+
766+
expressions = []
767+
string_expressions = scope.strip(' \t\n\r').split(' and ')
768+
expression_re = re.compile('^(?P<not>not )?(?P<operand>[^ ]+) (?P<operator>=|!=|in|contains|starts with) (?P<value>(:?"[^"]+"|\'[^\']+\'|\(.+\)|.+))$')
769+
770+
for string_expression in string_expressions:
771+
matches = expression_re.match(string_expression)
772+
773+
if matches is None:
774+
return [False, 'invalid scope format']
775+
776+
is_not_operator = matches.group('not') is not None
777+
778+
if matches.group('operator') == 'in':
779+
list_value = matches.group('value').strip(' ()')
780+
value_matches = re.findall('(:?\'[^\',]+\')|(:?"[^",]+")|(:?[,]+)', list_value)
781+
782+
if len(value_matches) == 0:
783+
return [False, 'invalid scope value list format']
784+
785+
value_matches = map(lambda v: v[0] if v[0] else v[1], value_matches)
786+
values = map(lambda v: v.strip(' "\''), value_matches)
787+
else:
788+
values = [matches.group('value').strip('"\'')]
789+
790+
operator_parse_dict = {
791+
'in': 'in' if not is_not_operator else 'notIn',
792+
'=': 'equals' if not is_not_operator else 'notEquals',
793+
'!=': 'notEquals' if not is_not_operator else 'equals',
794+
'contains': 'contains' if not is_not_operator else 'notContains',
795+
'starts with': 'startsWith'
796+
}
797+
798+
operator = operator_parse_dict.get(matches.group('operator'), None)
799+
if operator is None:
800+
return [False, 'invalid scope operator']
801+
802+
expressions.append({
803+
'operand': matches.group('operand'),
804+
'operator': operator,
805+
'value': values
806+
})
807+
808+
return [True, expressions]
809+
757810

758811
# For backwards compatibility
759812
SdcClient = SdMonitorClient

test/test_monitor_apis.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ date; $SCRIPTDIR/../examples/create_alert.py -a $ALERT_NAME $PYTHON_SDC_TEST_MON
2121
date; $SCRIPTDIR/../examples/update_alert.py -a $ALERT_NAME $PYTHON_SDC_TEST_MONITOR_API_TOKEN
2222
date; $SCRIPTDIR/../examples/delete_alert.py -a $ALERT_NAME $PYTHON_SDC_TEST_MONITOR_API_TOKEN
2323
date; $SCRIPTDIR/../examples/dashboard.py -d $DASHBOARD_1_NAME $PYTHON_SDC_TEST_MONITOR_API_TOKEN
24+
date; $SCRIPTDIR/../examples/dashboard_scope.py
2425
date; $SCRIPTDIR/../examples/create_dashboard.py -d $DASHBOARD_2_NAME $PYTHON_SDC_TEST_MONITOR_API_TOKEN
2526
date; $SCRIPTDIR/../examples/delete_dashboard.py -p $SESSION_UUID $PYTHON_SDC_TEST_MONITOR_API_TOKEN
2627
date; $SCRIPTDIR/../examples/get_data_advanced.py $PYTHON_SDC_TEST_MONITOR_API_TOKEN $AGENT_HOSTNAME

0 commit comments

Comments
 (0)