41f4a0b7c6296a081f6aa5ae8aaf9639090f150c
[integration/test.git] / csit / scripts / push_to_elk.py
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
3
4 # @License EPL-1.0 <http://spdx.org/licenses/EPL-1.0>
5 ##############################################################################
6 # Copyright (c) 2017 Raghuram Vadapalli, Jaspreet Singh and others.
7 #
8 # All rights reserved. This program and the accompanying materials
9 # are made available under the terms of the Eclipse Public License v1.0
10 # which accompanies this distribution, and is available at
11 # http://www.eclipse.org/legal/epl-v10.html
12 ##############################################################################
13
14 """
15 This script is used to parse logs, construct JSON BODY and push
16 it to ELK DB.
17
18 Usage: python construct_json.py host:port
19
20 JSON body similar to following is constructed from robot files, jenkins environment
21 and plot files available in workspace available post-build.
22 {
23     "project": "opendaylight", <- fix string for ODL project
24     "subject": "test", <- fix string for ODL test
25     "test-type": "performance", <- if there are csv files, otherwise "functional"
26     "jenkins-silo": "releng" <- from Jenkins $SILO
27     "test-name": "openflowplugin-csit-1node-periodic-bulkomatic-perf-daily-only-carbon", <- from Jenkins $JOB_NAME
28     "test-run": 289, <- from Jenkins $BUILD_NUMBER
29     "start-time": "20170612 16:50:04 GMT-07:00",  <- from robot log
30     "duration": "00:01:05.942", <- from robot log
31     "pass-tests": 9, <- from robot log
32     "fail-tests": 0, <- from robot log
33     "plots": {
34         "rate": { <- csv filename
35             "Config DS": 5816.99726601, <- from csv file
36             "OVS Switch": 5757.05238918, <- from csv file
37             "Operational DS": 2654.49139945 <- from csv file
38         },
39         "time": { <- csv filename
40             "Config DS": 17.191, <- from csv file
41             "OVS Switch": 17.37, <- from csv file
42             "Operational DS": 37.672 <- from csv file
43         }
44     }
45 }
46 """
47
48 # stdlib
49 from datetime import datetime
50 import glob
51 import json
52 import os
53 import requests
54 import sys
55 import time
56 import xml.etree.ElementTree as ET
57
58 # 3rd party lib
59 import yaml
60
61 # ELK DB host and port to be passed as ':' separated argument
62
63 if len(sys.argv) > 1:
64     if ':' in sys.argv[1]:
65         ELK_DB_HOST = sys.argv[1].split(':')[0]
66         ELK_DB_PORT = sys.argv[1].split(':')[1]
67 else:
68     print('Usage: python push_to_elk.py host:port')
69     print('Unable to publish data to ELK. Exiting.')
70     sys.exit()
71
72 # Construct json body
73
74 BODY = {}
75
76 ts = time.time()
77 formatted_ts = \
78     datetime.fromtimestamp(ts).strftime('%Y-%m-%dT%H:%M:%S.%fZ')
79 BODY['@timestamp'] = formatted_ts
80
81 # Plots are obtained from csv files (present in archives directory in $WORKSPACE).
82
83 csv_files = glob.glob('archives/*.csv')
84 BODY['project'] = 'opendaylight'
85 BODY['subject'] = 'test'
86
87 # If there are no csv files, then it is a functional test.
88 # Parse csv files and fill perfomance parameter values
89
90 if len(csv_files) == 0:
91     BODY['test-type'] = 'functional'
92 else:
93     BODY['test-type'] = 'performance'
94     BODY['plots'] = {}
95     for f in csv_files:
96         key = (f.split('/')[-1])[:-4]
97         BODY['plots'][key] = {}
98         lines = open(f).readlines()
99         props = lines[0].strip().split(',')
100         vals = lines[1].strip().split(',')
101         BODY['plots'][key][props[0]] = float(vals[0])
102         BODY['plots'][key][props[1]] = float(vals[1])
103         BODY['plots'][key][props[2]] = float(vals[2])
104
105 # Fill the required parameters whose values are obtained from environment.
106
107 BODY['jenkins-silo'] = os.environ['SILO']
108 BODY['test-name'] = os.environ['JOB_NAME']
109 BODY['test-run'] = os.environ['BUILD_NUMBER']
110
111 # Parsing robot log for statistics on no of start-time, pass/fail tests and duration.
112
113 robot_log = os.environ['WORKSPACE'] + '/output.xml'
114 tree = ET.parse(robot_log)
115 BODY['id'] = '{}-{}'.format(os.environ['JOB_NAME'],
116                             os.environ['BUILD_NUMBER'])
117 BODY['start-time'] = tree.getroot().attrib['generated']
118 BODY['pass-tests'] = tree.getroot().find('statistics')[0][1].get('pass')
119 BODY['fail-tests'] = tree.getroot().find('statistics')[0][1].get('fail')
120 endtime = tree.getroot().find('suite').find('status').get('endtime')
121 starttime = tree.getroot().find('suite').find('status').get('starttime')
122 elap_time = datetime.strptime(endtime, '%Y%m%d %H:%M:%S.%f') \
123     - datetime.strptime(starttime, '%Y%m%d %H:%M:%S.%f')
124 BODY['duration'] = str(elap_time)
125
126 # Parse JSON BODY to construct PUT_URL
127
128 PUT_URL_INDEX = '/{}-{}'.format(BODY['project'], BODY['subject'])
129 PUT_URL_TYPE = '/{}'.format(BODY['test-type'])
130 PUT_URL_ID = '/{}-{}'.format(BODY['test-name'], BODY['test-run'])
131 PUT_URL = 'https://{}:{}{}{}{}'.format(ELK_DB_HOST, ELK_DB_PORT, PUT_URL_INDEX, PUT_URL_TYPE, PUT_URL_ID)
132 print(PUT_URL)
133
134 print(json.dumps(BODY, indent=4))
135
136 # Try to send request to ELK DB.
137
138 try:
139     r = requests.put(PUT_URL, json=BODY)
140     print(r.status_code)
141     print(json.dumps(json.loads(r.content), indent=4))
142 except:
143     print('Unable to push data to ElasticSearch')
144
145
146 # Function to convert JSON object to string.
147 # Python puts 'true' as 'True' etc. which need handling.
148
149 def JSONToString(jobj):
150     retval = str(jobj)
151     retval = retval.replace('\'', '"')
152     retval = retval.replace(': ', ':')
153     retval = retval.replace(', ', ',')
154     retval = retval.replace('True', 'true')
155     retval = retval.replace('False', 'false')
156     retval = retval.replace('None', 'null')
157     return retval
158
159
160 # This function takes
161 # testname (string, eg: 'openflowplugin-csit-1node-periodic-bulkomatic-perf-daily-only-carbon'),
162 # fieldlist (list of fields, eg: ['pass-tests', 'failed-tests']),
163 # plotkey (string, eg: 'rate')
164 # as parameters and constructs a visualization object in JSON format
165
166 def getVisualization(testname, fieldlist, plotkey=''):
167     vis = {}
168     vis['title'] = testname
169     if plotkey != '':
170         vis['title'] += '-' + plotkey
171     vis['description'] = 'visualization of ' + plotkey \
172         + ' trends for testplan ' + testname
173     vis['version'] = 1
174     vis['kibanaSavedObjectMeta'] = {'searchSourceJSON': ''}
175     searchSourceJSON = {
176         'index': 'opendaylight-test',
177         'query': {
178             'query_string': {
179                 'analyze_wildcard': True,
180                 'query': '*'
181             }
182         },
183         'filter': [{
184             'meta': {
185                 'index': 'opendaylight-test',
186                 'negate': False,
187                 'disabled': False,
188                 'alias': None,
189                 'type': 'phrase',
190                 'key': 'test-name',
191                 'value': testname,
192             },
193             'query': {
194                 'match': {
195                     'test-name': {
196                         'query': testname,
197                         'type': 'phrase'
198                     }
199                 }
200             },
201             '$state': {
202                 'store': 'appState'
203             }
204         }]
205     }
206
207     vis['kibanaSavedObjectMeta']['searchSourceJSON'] = \
208         JSONToString(searchSourceJSON)
209     vis['uiStateJSON'] = '{"vis":{"legendOpen":true, "colors":{"pass-tests":"#7EB26D","failed-tests":"#E24D42"}}}'
210     visState = {
211         'title': vis['title'],
212         'type': 'area',
213         'params': {
214             'addLegend': True,
215             'addTimeMarker': False,
216             'addTooltip': True,
217             'times': [],
218             'grid': {
219                 'categoryLines': False,
220                 'style': {
221                     'color': '#eee'
222                 }
223             },
224             'legendPosition': 'right',
225             'seriesParams': [],
226             'categoryAxes': [{
227                 'id': 'CategoryAxis-1',
228                 'labels': {'show': True, 'truncate': 100},
229                 'position': 'bottom',
230                 'scale': {'type': 'linear'},
231                 'show': True,
232                 'style': {},
233                 'title': {'text': 'Test run number'},
234                 'type': 'category',
235             }],
236             'valueAxes': [{
237                 'id': 'ValueAxis-1',
238                 'labels': {
239                     'filter': False,
240                     'rotate': 0,
241                     'show': True,
242                     'truncate': 100,
243                 },
244                 'name': 'LeftAxis-1',
245                 'position': 'left',
246                 'scale': {'mode': 'normal', 'type': 'linear'},
247                 'show': True,
248                 'style': {},
249                 'title': {'text': ''},
250                 'type': 'value',
251             }],
252         },
253         'aggs': [{
254             'id': '2',
255             'enabled': True,
256             'type': 'histogram',
257             'schema': 'segment',
258             'params': {
259                 'field': 'test-run',
260                 'interval': 1,
261                 'extended_bounds': {},
262                 'customLabel': 'Test run number',
263             },
264         }],
265         'listeners': {},
266     }
267     if plotkey != '':  # Performance plot
268         visState['type'] = 'line'
269     for field in fieldlist:
270         seriesParam = {
271             'show': True,
272             'mode': 'normal',
273             'type': 'area',
274             'drawLinesBetweenPoints': True,
275             'showCircles': True,
276             'interpolate': 'linear',
277             'lineWidth': 2,
278             'data': {
279                 'id': str(len(visState['params']['seriesParams']) + 1) + '-' + vis['title'],
280                 'label': field.split('.')[-1]
281             },
282             'valueAxis': 'ValueAxis-1',
283         }
284         if plotkey != '':  # Performance plot
285             seriesParam['type'] = 'line'
286         agg = {
287             'id': str(len(visState['params']['seriesParams']) + 1) + '-' + vis['title'],
288             'enabled': True,
289             'type': 'sum',
290             'schema': 'metric',
291             'params': {
292                 'field': field,
293                 'customLabel': field.split('.')[-1]
294             },
295         }
296
297         visState['params']['seriesParams'].append(seriesParam)
298         visState['aggs'].append(agg)
299
300     vis['visState'] = JSONToString(visState)
301     return vis
302
303
304 vis_ids = []
305 if BODY['test-type'] == 'performance':
306
307     # Create visualizations for performance tests
308     # One visualization for one plot
309
310     for key in BODY['plots']:
311         fieldlist = []
312         for subkey in BODY['plots'][key]:
313             fieldlist.append('plots.' + key + '.' + subkey)
314         vis = getVisualization(BODY['test-name'], fieldlist, key)
315         vis_ids.append(BODY['test-name'] + '-' + key)
316         PUT_URL = \
317             'https://{}:{}/.kibana/visualization/{}-{}'.format(ELK_DB_HOST, ELK_DB_PORT, BODY['test-name'], key)
318         print(PUT_URL)
319         print(json.dumps(vis, indent=4))
320         try:
321             r = requests.put(PUT_URL, json=vis)
322             print(r.status_code)
323             print(json.dumps(json.loads(r.content), indent=4))
324         except:
325             print('Unable to push visualization to Kibana')
326
327 vis = getVisualization(BODY['test-name'],
328                        ['pass-tests', 'failed-tests'])
329 vis_ids.append(BODY['test-name'])
330 PUT_URL = 'https://{}:{}/.kibana/visualization/{}'.format(ELK_DB_HOST, ELK_DB_PORT, BODY['test-name'])
331 print(PUT_URL)
332 print(json.dumps(vis, indent=4))
333 try:
334     r = requests.put(PUT_URL, json=vis)
335     print(r.status_code)
336     print(json.dumps(json.loads(r.content), indent=4))
337 except:
338     print('Unable to push dashboard to Kibana')
339
340 # Create dashboard and add above created visualizations to it
341
342 DASHBOARD_NAME = BODY['test-name'].split('-')[0]
343 dashboard = {}
344 dashboard['title'] = DASHBOARD_NAME
345 dashboard['description'] = 'Dashboard for visualizing ' \
346     + DASHBOARD_NAME
347 dashboard['uiStateJSON'] = '{}'
348 dashboard['optionsJSON'] = '{"darkTheme":false}'
349 dashboard['version'] = 1
350 dashboard['timeRestore'] = False
351 dashboard['kibanaSavedObjectMeta'] = {
352     'searchSourceJSON': '{"filter":[{"query":{"query_string":{"query":"*","analyze_wildcard":true}}}],'
353     '"highlightAll":true,"version":true}'
354 }
355
356 # Check if visualizations already present in dashboard. If present, don't add, else, add at end
357
358 GET_URL = 'https://{}:{}/.kibana/dashboard/{}'.format(ELK_DB_HOST, ELK_DB_PORT, DASHBOARD_NAME)
359 r = requests.get(GET_URL)
360 response = json.loads(r.content)
361 dashboard_found = response['found']
362 vis_ids_present = set()
363 panelsJSON = []
364
365 print json.dumps(response, indent=4)
366 if dashboard_found:
367     panelsJSON = yaml.safe_load(response['_source']['panelsJSON'])
368     for vis in panelsJSON:
369         vis_ids_present.add(vis['id'])
370
371 size_x = 6
372 size_y = 3
373 xpos = (len(vis_ids_present) % 2) * 6 + 1
374 ypos = (len(vis_ids_present) / 2) * 3 + 1
375 for (i, vis_id) in enumerate(vis_ids):
376     if (not dashboard_found or vis_id not in vis_ids_present):
377         panelJSON = {
378             'size_x': size_x,
379             'size_y': size_y,
380             'panelIndex': len(vis_ids_present) + i,
381             'type': 'visualization',
382             'id': vis_id,
383             'col': xpos,
384             'row': ypos,
385         }
386         xpos += size_x
387         if xpos > 12:
388             xpos = 1
389             ypos += size_y
390         panelsJSON.append(panelJSON)
391     else:
392         print('visualization ' + vis_id + ' already present in dashboard')
393
394 dashboard['panelsJSON'] = JSONToString(panelsJSON)
395 PUT_URL = 'https://{}:{}/.kibana/dashboard/{}'.format(ELK_DB_HOST, ELK_DB_PORT, DASHBOARD_NAME)
396 print(PUT_URL)
397 print(json.dumps(dashboard, indent=4))
398 r = requests.put(PUT_URL, json=dashboard)
399 print(r.status_code)
400 print(json.dumps(json.loads(r.content), indent=4))