blob: 9c9d52c5a066dc4f8aa1672040c829ffeadbbff5 [file] [log] [blame]
xixuanebdb0a82017-04-28 11:25:02 -07001#!/usr/bin/env python2
Paul Hobbsef4e0702016-06-27 17:01:42 -07002
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-27 17:01:42 -070015import re
16import sys
17
xixuanebdb0a82017-04-28 11:25:02 -070018# TODO(ayatane): Fix cros lint pylint to work with virtualenv imports
19# pylint: disable=import-error
20from devserver_lib.devserver import MakeLogHandler
Paul Hobbsef4e0702016-06-27 17:01:42 -070021
xixuanebdb0a82017-04-28 11:25:02 -070022# only import setup_chromite before chromite import.
23import setup_chromite # pylint: disable=unused-import
Paul Hobbsef4e0702016-06-27 17:01:42 -070024from chromite.lib import ts_mon_config
25from chromite.lib import metrics
Paul Hobbs338baee2016-07-13 13:42:34 -070026from chromite.lib import cros_logging as logging
Paul Hobbsef4e0702016-06-27 17:01:42 -070027from infra_libs import ts_mon
28
29
30STATIC_GET_MATCHER = re.compile(
31 r'^(?P<ip_addr>\d+\.\d+\.\d+\.\d+) '
Paul Hobbsfa915682016-07-19 15:11:29 -070032 r'.*GET /static/(?P<endpoint>\S*)[^"]*" '
Paul Hobbsef4e0702016-06-27 17:01:42 -070033 r'200 (?P<size>\S+) .*')
34
35STATIC_GET_METRIC_NAME = 'chromeos/devserver/apache/static_response_size'
36
37
38LAB_SUBNETS = (
39 ("172.17.40.0", 22),
40 ("100.107.160.0", 19),
41 ("100.115.128.0", 17),
42 ("100.115.254.126", 25),
43 ("100.107.141.128", 25),
44 ("172.27.212.0", 22),
45 ("100.107.156.192", 26),
46 ("172.22.29.0", 25),
47 ("172.22.38.0", 23),
48 ("100.107.224.0", 23),
49 ("100.107.226.0", 25),
50 ("100.107.126.0", 25),
51)
52
53def IPToNum(ip):
Paul Hobbsfa915682016-07-19 15:11:29 -070054 """Returns the integer represented by an IPv4 string.
55
56 Args:
57 ip: An IPv4-formatted string.
58 """
Paul Hobbs487e3812016-07-22 15:45:33 -070059 return reduce(lambda seed, x: seed * 2**8 + int(x),
Paul Hobbsfa915682016-07-19 15:11:29 -070060 ip.split('.'),
61 0)
Paul Hobbsef4e0702016-06-27 17:01:42 -070062
63
64def MatchesSubnet(ip, base, mask):
Paul Hobbsfa915682016-07-19 15:11:29 -070065 """Whether the ip string |ip| matches the subnet |base|, |mask|.
66
67 Args:
68 ip: An IPv4 string.
69 base: An IPv4 string which is the lowest value in the subnet.
70 mask: The number of bits which are not wildcards in the subnet.
71 """
Paul Hobbsef4e0702016-06-27 17:01:42 -070072 ip_value = IPToNum(ip)
73 base_value = IPToNum(base)
74 mask = (2**mask - 1) << (32 - mask)
75 return (ip_value & mask) == (base_value & mask)
76
77
78def InLab(ip):
Paul Hobbsfa915682016-07-19 15:11:29 -070079 """Whether |ip| is an IPv4 address which is in the ChromeOS Lab.
80
81 Args:
82 ip: An IPv4 address to be tested.
83 """
Paul Hobbsef4e0702016-06-27 17:01:42 -070084 return any(MatchesSubnet(ip, base, mask)
85 for (base, mask) in LAB_SUBNETS)
86
87
Paul Hobbs5c56c832016-07-22 17:21:57 -070088MILESTONE_PATTERN = re.compile(r'R\d+')
89
90FILENAME_CONSTANTS = [
91 'stateful.tgz',
92 'client-autotest.tar.bz2',
93 'chromiumos_test_image.bin',
94 'autotest_server_package.tar.bz2',
95]
96
97FILENAME_PATTERNS = [(re.compile(s), s) for s in FILENAME_CONSTANTS] + [
98 (re.compile(r'dep-.*\.bz2'), 'dep-*.bz2'),
99 (re.compile(r'chromeos_.*_delta_test\.bin-.*'),
100 'chromeos_*_delta_test.bin-*'),
101 (re.compile(r'chromeos_.*_full_test\.bin-.*'),
102 'chromeos_*_full_test.bin-*'),
103 (re.compile(r'test-.*\.bz2'), 'test-*.bz2'),
104 (re.compile(r'dep-.*\.bz2'), 'dep-*.bz2'),
105]
106
107
108def MatchAny(needle, patterns, default=''):
109 for pattern, value in patterns:
110 if pattern.match(needle):
111 return value
112 return default
113
114
Paul Hobbsfa915682016-07-19 15:11:29 -0700115def ParseStaticEndpoint(endpoint):
116 """Parses a /static/.* URL path into build_config, milestone, and filename.
117
118 Static endpoints are expected to be of the form
119 /static/$BUILD_CONFIG/$MILESTONE-$VERSION/$FILENAME
120
121 This function expects the '/static/' prefix to already be stripped off.
122
123 Args:
124 endpoint: A string which is the matched URL path after /static/
125 """
126 build_config, milestone, filename = [''] * 3
Paul Hobbsef4e0702016-06-27 17:01:42 -0700127 try:
Paul Hobbsfa915682016-07-19 15:11:29 -0700128 parts = endpoint.split('/')
129 build_config = parts[0]
130 if len(parts) >= 2:
131 version = parts[1]
132 milestone = version[:version.index('-')]
Paul Hobbs5c56c832016-07-22 17:21:57 -0700133 if not MILESTONE_PATTERN.match(milestone):
134 milestone = ''
Paul Hobbsfa915682016-07-19 15:11:29 -0700135 if len(parts) >= 3:
Paul Hobbs5c56c832016-07-22 17:21:57 -0700136 filename = MatchAny(parts[-1], FILENAME_PATTERNS)
137
Paul Hobbsfa915682016-07-19 15:11:29 -0700138 except IndexError as e:
139 logging.debug('%s failed to parse. Caught %s' % (endpoint, str(e)))
140
141 return build_config, milestone, filename
142
143
144def EmitStaticRequestMetric(m):
145 """Emits a Counter metric for sucessful GETs to /static endpoints.
146
147 Args:
148 m: A regex match object
149 """
150 build_config, milestone, filename = ParseStaticEndpoint(m.group('endpoint'))
151
152 try:
153 size = int(m.group('size'))
Paul Hobbsef4e0702016-06-27 17:01:42 -0700154 except ValueError: # Zero is represented by "-"
155 size = 0
156
157 metrics.Counter(STATIC_GET_METRIC_NAME).increment_by(
158 size, fields={
Paul Hobbsfa915682016-07-19 15:11:29 -0700159 'build_config': build_config,
160 'milestone': milestone,
Paul Hobbs487e3812016-07-22 15:45:33 -0700161 'in_lab': InLab(m.group('ip_addr')),
Paul Hobbsfa915682016-07-19 15:11:29 -0700162 'endpoint': filename})
Paul Hobbsef4e0702016-06-27 17:01:42 -0700163
164
165def RunMatchers(stream, matchers):
Paul Hobbsfa915682016-07-19 15:11:29 -0700166 """Parses lines of |stream| using patterns and emitters from |matchers|
167
168 Args:
169 stream: A file object to read from.
170 matchers: A list of pairs of (matcher, emitter), where matcher is a regex
171 and emitter is a function called when the regex matches.
172 """
Paul Hobbs338baee2016-07-13 13:42:34 -0700173 for line in iter(stream.readline, ''):
Paul Hobbsef4e0702016-06-27 17:01:42 -0700174 for matcher, emitter in matchers:
Paul Hobbs338baee2016-07-13 13:42:34 -0700175 logging.debug('Emitting %s for input "%s"',
176 emitter.__name__, line.strip())
Paul Hobbsef4e0702016-06-27 17:01:42 -0700177 m = matcher.match(line)
178 if m:
179 emitter(m)
180 # The input might terminate if the log gets rotated. Make sure that Monarch
181 # flushes any pending metrics before quitting.
182 ts_mon.close()
183
184
185# TODO(phobbs) add a matcher for all requests, not just static files.
186MATCHERS = [
187 (STATIC_GET_MATCHER, EmitStaticRequestMetric),
188]
189
190
191def ParseArgs():
192 """Parses command line arguments."""
193 p = argparse.ArgumentParser(
194 description='Parses apache logs and emits metrics to Monarch')
xixuanebdb0a82017-04-28 11:25:02 -0700195 p.add_argument('--logfile', required=True)
Paul Hobbsef4e0702016-06-27 17:01:42 -0700196 return p.parse_args()
197
198
199def main():
200 """Sets up logging and runs matchers against stdin"""
201 args = ParseArgs()
202 root = logging.getLogger()
xixuanebdb0a82017-04-28 11:25:02 -0700203
Paul Hobbsef4e0702016-06-27 17:01:42 -0700204 root.addHandler(MakeLogHandler(args.logfile))
205 root.setLevel(logging.DEBUG)
206 ts_mon_config.SetupTsMonGlobalState('devserver_apache_log_metrics')
207 RunMatchers(sys.stdin, MATCHERS)
208
209
210if __name__ == '__main__':
211 main()