blob: 199588a638e4be86bafba3b0d5c3f850b90c6ab3 [file] [log] [blame]
xixuanebdb0a82017-04-28 18:25:021#!/usr/bin/env python2
Paul Hobbsef4e0702016-06-28 00:01:422
3# Copyright 2016 The Chromium OS Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7"""Script to upload metrics from apache logs to Monarch.
8
9We are interested in static file bandwidth, so it parses out GET requests to
10/static and uploads the sizes to a cumulative metric.
11"""
12from __future__ import print_function
13
14import argparse
Paul Hobbs84acd9d2017-09-20 22:10:5315from logging import handlers
Paul Hobbsef4e0702016-06-28 00:01:4216import re
17import sys
18
xixuanebdb0a82017-04-28 18:25:0219# TODO(ayatane): Fix cros lint pylint to work with virtualenv imports
20# pylint: disable=import-error
Paul Hobbsef4e0702016-06-28 00:01:4221
xixuanebdb0a82017-04-28 18:25:0222# only import setup_chromite before chromite import.
23import setup_chromite # pylint: disable=unused-import
Paul Hobbsef4e0702016-06-28 00:01:4224from chromite.lib import ts_mon_config
25from chromite.lib import metrics
Paul Hobbs338baee2016-07-13 20:42:3426from chromite.lib import cros_logging as logging
Paul Hobbsfe0b1c62017-08-18 19:56:1427
28
29# Log rotation parameters. Keep about two weeks of old logs.
30#
31# For more, see the documentation in standard python library for
32# logging.handlers.TimedRotatingFileHandler
33_LOG_ROTATION_TIME = 'H'
34_LOG_ROTATION_INTERVAL = 24 # hours
35_LOG_ROTATION_BACKUP = 14 # backup counts
Paul Hobbsef4e0702016-06-28 00:01:4236
37
38STATIC_GET_MATCHER = re.compile(
39 r'^(?P<ip_addr>\d+\.\d+\.\d+\.\d+) '
Paul Hobbsfa915682016-07-19 22:11:2940 r'.*GET /static/(?P<endpoint>\S*)[^"]*" '
Paul Hobbsef4e0702016-06-28 00:01:4241 r'200 (?P<size>\S+) .*')
42
43STATIC_GET_METRIC_NAME = 'chromeos/devserver/apache/static_response_size'
44
45
46LAB_SUBNETS = (
47 ("172.17.40.0", 22),
48 ("100.107.160.0", 19),
49 ("100.115.128.0", 17),
50 ("100.115.254.126", 25),
51 ("100.107.141.128", 25),
52 ("172.27.212.0", 22),
53 ("100.107.156.192", 26),
54 ("172.22.29.0", 25),
55 ("172.22.38.0", 23),
56 ("100.107.224.0", 23),
57 ("100.107.226.0", 25),
58 ("100.107.126.0", 25),
59)
60
61def IPToNum(ip):
Paul Hobbsfa915682016-07-19 22:11:2962 """Returns the integer represented by an IPv4 string.
63
64 Args:
65 ip: An IPv4-formatted string.
66 """
Paul Hobbs487e3812016-07-22 22:45:3367 return reduce(lambda seed, x: seed * 2**8 + int(x),
Paul Hobbsfa915682016-07-19 22:11:2968 ip.split('.'),
69 0)
Paul Hobbsef4e0702016-06-28 00:01:4270
71
72def MatchesSubnet(ip, base, mask):
Paul Hobbsfa915682016-07-19 22:11:2973 """Whether the ip string |ip| matches the subnet |base|, |mask|.
74
75 Args:
76 ip: An IPv4 string.
77 base: An IPv4 string which is the lowest value in the subnet.
78 mask: The number of bits which are not wildcards in the subnet.
79 """
Paul Hobbsef4e0702016-06-28 00:01:4280 ip_value = IPToNum(ip)
81 base_value = IPToNum(base)
82 mask = (2**mask - 1) << (32 - mask)
83 return (ip_value & mask) == (base_value & mask)
84
85
86def InLab(ip):
Paul Hobbsfa915682016-07-19 22:11:2987 """Whether |ip| is an IPv4 address which is in the ChromeOS Lab.
88
89 Args:
90 ip: An IPv4 address to be tested.
91 """
Paul Hobbsef4e0702016-06-28 00:01:4292 return any(MatchesSubnet(ip, base, mask)
93 for (base, mask) in LAB_SUBNETS)
94
95
Paul Hobbs5c56c832016-07-23 00:21:5796MILESTONE_PATTERN = re.compile(r'R\d+')
97
98FILENAME_CONSTANTS = [
99 'stateful.tgz',
100 'client-autotest.tar.bz2',
101 'chromiumos_test_image.bin',
102 'autotest_server_package.tar.bz2',
103]
104
105FILENAME_PATTERNS = [(re.compile(s), s) for s in FILENAME_CONSTANTS] + [
106 (re.compile(r'dep-.*\.bz2'), 'dep-*.bz2'),
107 (re.compile(r'chromeos_.*_delta_test\.bin-.*'),
108 'chromeos_*_delta_test.bin-*'),
109 (re.compile(r'chromeos_.*_full_test\.bin-.*'),
110 'chromeos_*_full_test.bin-*'),
111 (re.compile(r'test-.*\.bz2'), 'test-*.bz2'),
112 (re.compile(r'dep-.*\.bz2'), 'dep-*.bz2'),
113]
114
115
116def MatchAny(needle, patterns, default=''):
117 for pattern, value in patterns:
118 if pattern.match(needle):
119 return value
120 return default
121
122
Paul Hobbsfa915682016-07-19 22:11:29123def ParseStaticEndpoint(endpoint):
124 """Parses a /static/.* URL path into build_config, milestone, and filename.
125
126 Static endpoints are expected to be of the form
127 /static/$BUILD_CONFIG/$MILESTONE-$VERSION/$FILENAME
128
129 This function expects the '/static/' prefix to already be stripped off.
130
131 Args:
132 endpoint: A string which is the matched URL path after /static/
133 """
134 build_config, milestone, filename = [''] * 3
Paul Hobbsef4e0702016-06-28 00:01:42135 try:
Paul Hobbsfa915682016-07-19 22:11:29136 parts = endpoint.split('/')
137 build_config = parts[0]
138 if len(parts) >= 2:
139 version = parts[1]
140 milestone = version[:version.index('-')]
Paul Hobbs5c56c832016-07-23 00:21:57141 if not MILESTONE_PATTERN.match(milestone):
142 milestone = ''
Paul Hobbsfa915682016-07-19 22:11:29143 if len(parts) >= 3:
Paul Hobbs5c56c832016-07-23 00:21:57144 filename = MatchAny(parts[-1], FILENAME_PATTERNS)
145
Paul Hobbsfa915682016-07-19 22:11:29146 except IndexError as e:
147 logging.debug('%s failed to parse. Caught %s' % (endpoint, str(e)))
148
149 return build_config, milestone, filename
150
151
152def EmitStaticRequestMetric(m):
153 """Emits a Counter metric for sucessful GETs to /static endpoints.
154
155 Args:
156 m: A regex match object
157 """
158 build_config, milestone, filename = ParseStaticEndpoint(m.group('endpoint'))
159
160 try:
161 size = int(m.group('size'))
Paul Hobbsef4e0702016-06-28 00:01:42162 except ValueError: # Zero is represented by "-"
163 size = 0
164
165 metrics.Counter(STATIC_GET_METRIC_NAME).increment_by(
166 size, fields={
Paul Hobbsfa915682016-07-19 22:11:29167 'build_config': build_config,
168 'milestone': milestone,
Paul Hobbs487e3812016-07-22 22:45:33169 'in_lab': InLab(m.group('ip_addr')),
Paul Hobbsfa915682016-07-19 22:11:29170 'endpoint': filename})
Paul Hobbsef4e0702016-06-28 00:01:42171
172
173def RunMatchers(stream, matchers):
Paul Hobbsfa915682016-07-19 22:11:29174 """Parses lines of |stream| using patterns and emitters from |matchers|
175
176 Args:
177 stream: A file object to read from.
178 matchers: A list of pairs of (matcher, emitter), where matcher is a regex
179 and emitter is a function called when the regex matches.
180 """
Paul Hobbs338baee2016-07-13 20:42:34181 for line in iter(stream.readline, ''):
Paul Hobbsef4e0702016-06-28 00:01:42182 for matcher, emitter in matchers:
Paul Hobbs338baee2016-07-13 20:42:34183 logging.debug('Emitting %s for input "%s"',
184 emitter.__name__, line.strip())
Paul Hobbsef4e0702016-06-28 00:01:42185 m = matcher.match(line)
186 if m:
187 emitter(m)
Paul Hobbsef4e0702016-06-28 00:01:42188
189
190# TODO(phobbs) add a matcher for all requests, not just static files.
191MATCHERS = [
192 (STATIC_GET_MATCHER, EmitStaticRequestMetric),
193]
194
195
196def ParseArgs():
197 """Parses command line arguments."""
198 p = argparse.ArgumentParser(
199 description='Parses apache logs and emits metrics to Monarch')
xixuanebdb0a82017-04-28 18:25:02200 p.add_argument('--logfile', required=True)
Paul Hobbsef4e0702016-06-28 00:01:42201 return p.parse_args()
202
203
204def main():
205 """Sets up logging and runs matchers against stdin"""
206 args = ParseArgs()
207 root = logging.getLogger()
xixuanebdb0a82017-04-28 18:25:02208
Paul Hobbs84acd9d2017-09-20 22:10:53209 root.addHandler(handlers.TimedRotatingFileHandler(
Paul Hobbsfe0b1c62017-08-18 19:56:14210 args.logfile, when=_LOG_ROTATION_TIME,
211 interval=_LOG_ROTATION_INTERVAL,
212 backupCount=_LOG_ROTATION_BACKUP))
Paul Hobbsef4e0702016-06-28 00:01:42213 root.setLevel(logging.DEBUG)
Paul Hobbsfe0b1c62017-08-18 19:56:14214 with ts_mon_config.SetupTsMonGlobalState('devserver_apache_log_metrics',
215 indirect=True):
216 RunMatchers(sys.stdin, MATCHERS)
Paul Hobbsef4e0702016-06-28 00:01:42217
218
219if __name__ == '__main__':
220 main()