blob: 0bd1a4d02a92c18dcacfd7428369805bfb72b324 [file] [log] [blame]
Paul Hobbsef4e0702016-06-28 00:01:421#!/usr/bin/python2
2
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 Hobbsef4e0702016-06-28 00:01:4215import re
16import sys
17
18from devserver import MakeLogHandler
19
20from chromite.lib import ts_mon_config
21from chromite.lib import metrics
Paul Hobbs338baee2016-07-13 20:42:3422from chromite.lib import cros_logging as logging
Paul Hobbsef4e0702016-06-28 00:01:4223from infra_libs import ts_mon
24
25
26STATIC_GET_MATCHER = re.compile(
27 r'^(?P<ip_addr>\d+\.\d+\.\d+\.\d+) '
Paul Hobbsfa915682016-07-19 22:11:2928 r'.*GET /static/(?P<endpoint>\S*)[^"]*" '
Paul Hobbsef4e0702016-06-28 00:01:4229 r'200 (?P<size>\S+) .*')
30
31STATIC_GET_METRIC_NAME = 'chromeos/devserver/apache/static_response_size'
32
33
34LAB_SUBNETS = (
35 ("172.17.40.0", 22),
36 ("100.107.160.0", 19),
37 ("100.115.128.0", 17),
38 ("100.115.254.126", 25),
39 ("100.107.141.128", 25),
40 ("172.27.212.0", 22),
41 ("100.107.156.192", 26),
42 ("172.22.29.0", 25),
43 ("172.22.38.0", 23),
44 ("100.107.224.0", 23),
45 ("100.107.226.0", 25),
46 ("100.107.126.0", 25),
47)
48
49def IPToNum(ip):
Paul Hobbsfa915682016-07-19 22:11:2950 """Returns the integer represented by an IPv4 string.
51
52 Args:
53 ip: An IPv4-formatted string.
54 """
Paul Hobbs487e3812016-07-22 22:45:3355 return reduce(lambda seed, x: seed * 2**8 + int(x),
Paul Hobbsfa915682016-07-19 22:11:2956 ip.split('.'),
57 0)
Paul Hobbsef4e0702016-06-28 00:01:4258
59
60def MatchesSubnet(ip, base, mask):
Paul Hobbsfa915682016-07-19 22:11:2961 """Whether the ip string |ip| matches the subnet |base|, |mask|.
62
63 Args:
64 ip: An IPv4 string.
65 base: An IPv4 string which is the lowest value in the subnet.
66 mask: The number of bits which are not wildcards in the subnet.
67 """
Paul Hobbsef4e0702016-06-28 00:01:4268 ip_value = IPToNum(ip)
69 base_value = IPToNum(base)
70 mask = (2**mask - 1) << (32 - mask)
71 return (ip_value & mask) == (base_value & mask)
72
73
74def InLab(ip):
Paul Hobbsfa915682016-07-19 22:11:2975 """Whether |ip| is an IPv4 address which is in the ChromeOS Lab.
76
77 Args:
78 ip: An IPv4 address to be tested.
79 """
Paul Hobbsef4e0702016-06-28 00:01:4280 return any(MatchesSubnet(ip, base, mask)
81 for (base, mask) in LAB_SUBNETS)
82
83
Paul Hobbs5c56c832016-07-23 00:21:5784MILESTONE_PATTERN = re.compile(r'R\d+')
85
86FILENAME_CONSTANTS = [
87 'stateful.tgz',
88 'client-autotest.tar.bz2',
89 'chromiumos_test_image.bin',
90 'autotest_server_package.tar.bz2',
91]
92
93FILENAME_PATTERNS = [(re.compile(s), s) for s in FILENAME_CONSTANTS] + [
94 (re.compile(r'dep-.*\.bz2'), 'dep-*.bz2'),
95 (re.compile(r'chromeos_.*_delta_test\.bin-.*'),
96 'chromeos_*_delta_test.bin-*'),
97 (re.compile(r'chromeos_.*_full_test\.bin-.*'),
98 'chromeos_*_full_test.bin-*'),
99 (re.compile(r'test-.*\.bz2'), 'test-*.bz2'),
100 (re.compile(r'dep-.*\.bz2'), 'dep-*.bz2'),
101]
102
103
104def MatchAny(needle, patterns, default=''):
105 for pattern, value in patterns:
106 if pattern.match(needle):
107 return value
108 return default
109
110
Paul Hobbsfa915682016-07-19 22:11:29111def ParseStaticEndpoint(endpoint):
112 """Parses a /static/.* URL path into build_config, milestone, and filename.
113
114 Static endpoints are expected to be of the form
115 /static/$BUILD_CONFIG/$MILESTONE-$VERSION/$FILENAME
116
117 This function expects the '/static/' prefix to already be stripped off.
118
119 Args:
120 endpoint: A string which is the matched URL path after /static/
121 """
122 build_config, milestone, filename = [''] * 3
Paul Hobbsef4e0702016-06-28 00:01:42123 try:
Paul Hobbsfa915682016-07-19 22:11:29124 parts = endpoint.split('/')
125 build_config = parts[0]
126 if len(parts) >= 2:
127 version = parts[1]
128 milestone = version[:version.index('-')]
Paul Hobbs5c56c832016-07-23 00:21:57129 if not MILESTONE_PATTERN.match(milestone):
130 milestone = ''
Paul Hobbsfa915682016-07-19 22:11:29131 if len(parts) >= 3:
Paul Hobbs5c56c832016-07-23 00:21:57132 filename = MatchAny(parts[-1], FILENAME_PATTERNS)
133
Paul Hobbsfa915682016-07-19 22:11:29134 except IndexError as e:
135 logging.debug('%s failed to parse. Caught %s' % (endpoint, str(e)))
136
137 return build_config, milestone, filename
138
139
140def EmitStaticRequestMetric(m):
141 """Emits a Counter metric for sucessful GETs to /static endpoints.
142
143 Args:
144 m: A regex match object
145 """
146 build_config, milestone, filename = ParseStaticEndpoint(m.group('endpoint'))
147
148 try:
149 size = int(m.group('size'))
Paul Hobbsef4e0702016-06-28 00:01:42150 except ValueError: # Zero is represented by "-"
151 size = 0
152
153 metrics.Counter(STATIC_GET_METRIC_NAME).increment_by(
154 size, fields={
Paul Hobbsfa915682016-07-19 22:11:29155 'build_config': build_config,
156 'milestone': milestone,
Paul Hobbs487e3812016-07-22 22:45:33157 'in_lab': InLab(m.group('ip_addr')),
Paul Hobbsfa915682016-07-19 22:11:29158 'endpoint': filename})
Paul Hobbsef4e0702016-06-28 00:01:42159
160
161def RunMatchers(stream, matchers):
Paul Hobbsfa915682016-07-19 22:11:29162 """Parses lines of |stream| using patterns and emitters from |matchers|
163
164 Args:
165 stream: A file object to read from.
166 matchers: A list of pairs of (matcher, emitter), where matcher is a regex
167 and emitter is a function called when the regex matches.
168 """
Paul Hobbs338baee2016-07-13 20:42:34169 for line in iter(stream.readline, ''):
Paul Hobbsef4e0702016-06-28 00:01:42170 for matcher, emitter in matchers:
Paul Hobbs338baee2016-07-13 20:42:34171 logging.debug('Emitting %s for input "%s"',
172 emitter.__name__, line.strip())
Paul Hobbsef4e0702016-06-28 00:01:42173 m = matcher.match(line)
174 if m:
175 emitter(m)
176 # The input might terminate if the log gets rotated. Make sure that Monarch
177 # flushes any pending metrics before quitting.
178 ts_mon.close()
179
180
181# TODO(phobbs) add a matcher for all requests, not just static files.
182MATCHERS = [
183 (STATIC_GET_MATCHER, EmitStaticRequestMetric),
184]
185
186
187def ParseArgs():
188 """Parses command line arguments."""
189 p = argparse.ArgumentParser(
190 description='Parses apache logs and emits metrics to Monarch')
191 p.add_argument('--logfile')
192 return p.parse_args()
193
194
195def main():
196 """Sets up logging and runs matchers against stdin"""
197 args = ParseArgs()
198 root = logging.getLogger()
199 root.addHandler(MakeLogHandler(args.logfile))
200 root.setLevel(logging.DEBUG)
201 ts_mon_config.SetupTsMonGlobalState('devserver_apache_log_metrics')
202 RunMatchers(sys.stdin, MATCHERS)
203
204
205if __name__ == '__main__':
206 main()