blob: 073a123a3c5c68f80546f73b5baf3e7b405970a5 [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
Paul Hobbsef4e0702016-06-27 17:01:42 -070020
xixuanebdb0a82017-04-28 11:25:02 -070021# only import setup_chromite before chromite import.
22import setup_chromite # pylint: disable=unused-import
Paul Hobbsef4e0702016-06-27 17:01:42 -070023from chromite.lib import ts_mon_config
24from chromite.lib import metrics
Paul Hobbs338baee2016-07-13 13:42:34 -070025from chromite.lib import cros_logging as logging
Paul Hobbsfe0b1c62017-08-18 12:56:14 -070026
27
28# Log rotation parameters. Keep about two weeks of old logs.
29#
30# For more, see the documentation in standard python library for
31# logging.handlers.TimedRotatingFileHandler
32_LOG_ROTATION_TIME = 'H'
33_LOG_ROTATION_INTERVAL = 24 # hours
34_LOG_ROTATION_BACKUP = 14 # backup counts
Paul Hobbsef4e0702016-06-27 17:01:42 -070035
36
37STATIC_GET_MATCHER = re.compile(
38 r'^(?P<ip_addr>\d+\.\d+\.\d+\.\d+) '
Paul Hobbsfa915682016-07-19 15:11:29 -070039 r'.*GET /static/(?P<endpoint>\S*)[^"]*" '
Paul Hobbsef4e0702016-06-27 17:01:42 -070040 r'200 (?P<size>\S+) .*')
41
42STATIC_GET_METRIC_NAME = 'chromeos/devserver/apache/static_response_size'
43
44
45LAB_SUBNETS = (
46 ("172.17.40.0", 22),
47 ("100.107.160.0", 19),
48 ("100.115.128.0", 17),
49 ("100.115.254.126", 25),
50 ("100.107.141.128", 25),
51 ("172.27.212.0", 22),
52 ("100.107.156.192", 26),
53 ("172.22.29.0", 25),
54 ("172.22.38.0", 23),
55 ("100.107.224.0", 23),
56 ("100.107.226.0", 25),
57 ("100.107.126.0", 25),
58)
59
60def IPToNum(ip):
Paul Hobbsfa915682016-07-19 15:11:29 -070061 """Returns the integer represented by an IPv4 string.
62
63 Args:
64 ip: An IPv4-formatted string.
65 """
Paul Hobbs487e3812016-07-22 15:45:33 -070066 return reduce(lambda seed, x: seed * 2**8 + int(x),
Paul Hobbsfa915682016-07-19 15:11:29 -070067 ip.split('.'),
68 0)
Paul Hobbsef4e0702016-06-27 17:01:42 -070069
70
71def MatchesSubnet(ip, base, mask):
Paul Hobbsfa915682016-07-19 15:11:29 -070072 """Whether the ip string |ip| matches the subnet |base|, |mask|.
73
74 Args:
75 ip: An IPv4 string.
76 base: An IPv4 string which is the lowest value in the subnet.
77 mask: The number of bits which are not wildcards in the subnet.
78 """
Paul Hobbsef4e0702016-06-27 17:01:42 -070079 ip_value = IPToNum(ip)
80 base_value = IPToNum(base)
81 mask = (2**mask - 1) << (32 - mask)
82 return (ip_value & mask) == (base_value & mask)
83
84
85def InLab(ip):
Paul Hobbsfa915682016-07-19 15:11:29 -070086 """Whether |ip| is an IPv4 address which is in the ChromeOS Lab.
87
88 Args:
89 ip: An IPv4 address to be tested.
90 """
Paul Hobbsef4e0702016-06-27 17:01:42 -070091 return any(MatchesSubnet(ip, base, mask)
92 for (base, mask) in LAB_SUBNETS)
93
94
Paul Hobbs5c56c832016-07-22 17:21:57 -070095MILESTONE_PATTERN = re.compile(r'R\d+')
96
97FILENAME_CONSTANTS = [
98 'stateful.tgz',
99 'client-autotest.tar.bz2',
100 'chromiumos_test_image.bin',
101 'autotest_server_package.tar.bz2',
102]
103
104FILENAME_PATTERNS = [(re.compile(s), s) for s in FILENAME_CONSTANTS] + [
105 (re.compile(r'dep-.*\.bz2'), 'dep-*.bz2'),
106 (re.compile(r'chromeos_.*_delta_test\.bin-.*'),
107 'chromeos_*_delta_test.bin-*'),
108 (re.compile(r'chromeos_.*_full_test\.bin-.*'),
109 'chromeos_*_full_test.bin-*'),
110 (re.compile(r'test-.*\.bz2'), 'test-*.bz2'),
111 (re.compile(r'dep-.*\.bz2'), 'dep-*.bz2'),
112]
113
114
115def MatchAny(needle, patterns, default=''):
116 for pattern, value in patterns:
117 if pattern.match(needle):
118 return value
119 return default
120
121
Paul Hobbsfa915682016-07-19 15:11:29 -0700122def ParseStaticEndpoint(endpoint):
123 """Parses a /static/.* URL path into build_config, milestone, and filename.
124
125 Static endpoints are expected to be of the form
126 /static/$BUILD_CONFIG/$MILESTONE-$VERSION/$FILENAME
127
128 This function expects the '/static/' prefix to already be stripped off.
129
130 Args:
131 endpoint: A string which is the matched URL path after /static/
132 """
133 build_config, milestone, filename = [''] * 3
Paul Hobbsef4e0702016-06-27 17:01:42 -0700134 try:
Paul Hobbsfa915682016-07-19 15:11:29 -0700135 parts = endpoint.split('/')
136 build_config = parts[0]
137 if len(parts) >= 2:
138 version = parts[1]
139 milestone = version[:version.index('-')]
Paul Hobbs5c56c832016-07-22 17:21:57 -0700140 if not MILESTONE_PATTERN.match(milestone):
141 milestone = ''
Paul Hobbsfa915682016-07-19 15:11:29 -0700142 if len(parts) >= 3:
Paul Hobbs5c56c832016-07-22 17:21:57 -0700143 filename = MatchAny(parts[-1], FILENAME_PATTERNS)
144
Paul Hobbsfa915682016-07-19 15:11:29 -0700145 except IndexError as e:
146 logging.debug('%s failed to parse. Caught %s' % (endpoint, str(e)))
147
148 return build_config, milestone, filename
149
150
151def EmitStaticRequestMetric(m):
152 """Emits a Counter metric for sucessful GETs to /static endpoints.
153
154 Args:
155 m: A regex match object
156 """
157 build_config, milestone, filename = ParseStaticEndpoint(m.group('endpoint'))
158
159 try:
160 size = int(m.group('size'))
Paul Hobbsef4e0702016-06-27 17:01:42 -0700161 except ValueError: # Zero is represented by "-"
162 size = 0
163
164 metrics.Counter(STATIC_GET_METRIC_NAME).increment_by(
165 size, fields={
Paul Hobbsfa915682016-07-19 15:11:29 -0700166 'build_config': build_config,
167 'milestone': milestone,
Paul Hobbs487e3812016-07-22 15:45:33 -0700168 'in_lab': InLab(m.group('ip_addr')),
Paul Hobbsfa915682016-07-19 15:11:29 -0700169 'endpoint': filename})
Paul Hobbsef4e0702016-06-27 17:01:42 -0700170
171
172def RunMatchers(stream, matchers):
Paul Hobbsfa915682016-07-19 15:11:29 -0700173 """Parses lines of |stream| using patterns and emitters from |matchers|
174
175 Args:
176 stream: A file object to read from.
177 matchers: A list of pairs of (matcher, emitter), where matcher is a regex
178 and emitter is a function called when the regex matches.
179 """
Paul Hobbs338baee2016-07-13 13:42:34 -0700180 for line in iter(stream.readline, ''):
Paul Hobbsef4e0702016-06-27 17:01:42 -0700181 for matcher, emitter in matchers:
Paul Hobbs338baee2016-07-13 13:42:34 -0700182 logging.debug('Emitting %s for input "%s"',
183 emitter.__name__, line.strip())
Paul Hobbsef4e0702016-06-27 17:01:42 -0700184 m = matcher.match(line)
185 if m:
186 emitter(m)
Paul Hobbsef4e0702016-06-27 17:01:42 -0700187
188
189# TODO(phobbs) add a matcher for all requests, not just static files.
190MATCHERS = [
191 (STATIC_GET_MATCHER, EmitStaticRequestMetric),
192]
193
194
195def ParseArgs():
196 """Parses command line arguments."""
197 p = argparse.ArgumentParser(
198 description='Parses apache logs and emits metrics to Monarch')
xixuanebdb0a82017-04-28 11:25:02 -0700199 p.add_argument('--logfile', required=True)
Paul Hobbsef4e0702016-06-27 17:01:42 -0700200 return p.parse_args()
201
202
203def main():
204 """Sets up logging and runs matchers against stdin"""
205 args = ParseArgs()
206 root = logging.getLogger()
xixuanebdb0a82017-04-28 11:25:02 -0700207
Paul Hobbsfe0b1c62017-08-18 12:56:14 -0700208 root.addHandler(logging.handlers.TimedRotatingFileHandler(
209 args.logfile, when=_LOG_ROTATION_TIME,
210 interval=_LOG_ROTATION_INTERVAL,
211 backupCount=_LOG_ROTATION_BACKUP))
Paul Hobbsef4e0702016-06-27 17:01:42 -0700212 root.setLevel(logging.DEBUG)
Paul Hobbsfe0b1c62017-08-18 12:56:14 -0700213 with ts_mon_config.SetupTsMonGlobalState('devserver_apache_log_metrics',
214 indirect=True):
215 RunMatchers(sys.stdin, MATCHERS)
Paul Hobbsef4e0702016-06-27 17:01:42 -0700216
217
218if __name__ == '__main__':
219 main()