blob: 6007e7470879c2a9c2ee6b26ec8d2f214d8e7dcb [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
Congbin Guoc2b908c2019-09-09 16:24:38 -070015import functools
Paul Hobbs84acd9d2017-09-20 15:10:53 -070016from logging import handlers
Paul Hobbsef4e0702016-06-27 17:01:42 -070017import re
18import sys
19
xixuanebdb0a82017-04-28 11:25:02 -070020# TODO(ayatane): Fix cros lint pylint to work with virtualenv imports
21# pylint: disable=import-error
Paul Hobbsef4e0702016-06-27 17:01:42 -070022
xixuanebdb0a82017-04-28 11:25:02 -070023# only import setup_chromite before chromite import.
24import setup_chromite # pylint: disable=unused-import
Paul Hobbsef4e0702016-06-27 17:01:42 -070025from chromite.lib import ts_mon_config
26from chromite.lib import metrics
Paul Hobbs338baee2016-07-13 13:42:34 -070027from chromite.lib import cros_logging as logging
Paul Hobbsfe0b1c62017-08-18 12:56:14 -070028
29
30# Log rotation parameters. Keep about two weeks of old logs.
31#
32# For more, see the documentation in standard python library for
33# logging.handlers.TimedRotatingFileHandler
34_LOG_ROTATION_TIME = 'H'
35_LOG_ROTATION_INTERVAL = 24 # hours
36_LOG_ROTATION_BACKUP = 14 # backup counts
Paul Hobbsef4e0702016-06-27 17:01:42 -070037
38
39STATIC_GET_MATCHER = re.compile(
40 r'^(?P<ip_addr>\d+\.\d+\.\d+\.\d+) '
Paul Hobbsfa915682016-07-19 15:11:29 -070041 r'.*GET /static/(?P<endpoint>\S*)[^"]*" '
Paul Hobbsef4e0702016-06-27 17:01:42 -070042 r'200 (?P<size>\S+) .*')
43
44STATIC_GET_METRIC_NAME = 'chromeos/devserver/apache/static_response_size'
45
46
47LAB_SUBNETS = (
48 ("172.17.40.0", 22),
49 ("100.107.160.0", 19),
50 ("100.115.128.0", 17),
51 ("100.115.254.126", 25),
52 ("100.107.141.128", 25),
53 ("172.27.212.0", 22),
54 ("100.107.156.192", 26),
55 ("172.22.29.0", 25),
56 ("172.22.38.0", 23),
57 ("100.107.224.0", 23),
58 ("100.107.226.0", 25),
59 ("100.107.126.0", 25),
60)
61
62def IPToNum(ip):
Paul Hobbsfa915682016-07-19 15:11:29 -070063 """Returns the integer represented by an IPv4 string.
64
65 Args:
66 ip: An IPv4-formatted string.
67 """
Congbin Guoc2b908c2019-09-09 16:24:38 -070068 return functools.reduce(lambda seed, x: seed * 2**8 + int(x),
69 ip.split('.'),
70 0)
Paul Hobbsef4e0702016-06-27 17:01:42 -070071
72
73def MatchesSubnet(ip, base, mask):
Paul Hobbsfa915682016-07-19 15:11:29 -070074 """Whether the ip string |ip| matches the subnet |base|, |mask|.
75
76 Args:
77 ip: An IPv4 string.
78 base: An IPv4 string which is the lowest value in the subnet.
79 mask: The number of bits which are not wildcards in the subnet.
80 """
Paul Hobbsef4e0702016-06-27 17:01:42 -070081 ip_value = IPToNum(ip)
82 base_value = IPToNum(base)
83 mask = (2**mask - 1) << (32 - mask)
84 return (ip_value & mask) == (base_value & mask)
85
86
87def InLab(ip):
Paul Hobbsfa915682016-07-19 15:11:29 -070088 """Whether |ip| is an IPv4 address which is in the ChromeOS Lab.
89
90 Args:
91 ip: An IPv4 address to be tested.
92 """
Paul Hobbsef4e0702016-06-27 17:01:42 -070093 return any(MatchesSubnet(ip, base, mask)
94 for (base, mask) in LAB_SUBNETS)
95
96
Paul Hobbs5c56c832016-07-22 17:21:57 -070097MILESTONE_PATTERN = re.compile(r'R\d+')
98
99FILENAME_CONSTANTS = [
100 'stateful.tgz',
101 'client-autotest.tar.bz2',
102 'chromiumos_test_image.bin',
103 'autotest_server_package.tar.bz2',
104]
105
106FILENAME_PATTERNS = [(re.compile(s), s) for s in FILENAME_CONSTANTS] + [
107 (re.compile(r'dep-.*\.bz2'), 'dep-*.bz2'),
108 (re.compile(r'chromeos_.*_delta_test\.bin-.*'),
109 'chromeos_*_delta_test.bin-*'),
110 (re.compile(r'chromeos_.*_full_test\.bin-.*'),
111 'chromeos_*_full_test.bin-*'),
112 (re.compile(r'test-.*\.bz2'), 'test-*.bz2'),
113 (re.compile(r'dep-.*\.bz2'), 'dep-*.bz2'),
114]
115
116
117def MatchAny(needle, patterns, default=''):
118 for pattern, value in patterns:
119 if pattern.match(needle):
120 return value
121 return default
122
123
Paul Hobbsfa915682016-07-19 15:11:29 -0700124def ParseStaticEndpoint(endpoint):
125 """Parses a /static/.* URL path into build_config, milestone, and filename.
126
127 Static endpoints are expected to be of the form
128 /static/$BUILD_CONFIG/$MILESTONE-$VERSION/$FILENAME
129
130 This function expects the '/static/' prefix to already be stripped off.
131
132 Args:
133 endpoint: A string which is the matched URL path after /static/
134 """
135 build_config, milestone, filename = [''] * 3
Paul Hobbsef4e0702016-06-27 17:01:42 -0700136 try:
Paul Hobbsfa915682016-07-19 15:11:29 -0700137 parts = endpoint.split('/')
138 build_config = parts[0]
139 if len(parts) >= 2:
140 version = parts[1]
141 milestone = version[:version.index('-')]
Paul Hobbs5c56c832016-07-22 17:21:57 -0700142 if not MILESTONE_PATTERN.match(milestone):
143 milestone = ''
Paul Hobbsfa915682016-07-19 15:11:29 -0700144 if len(parts) >= 3:
Paul Hobbs5c56c832016-07-22 17:21:57 -0700145 filename = MatchAny(parts[-1], FILENAME_PATTERNS)
146
Paul Hobbsfa915682016-07-19 15:11:29 -0700147 except IndexError as e:
Congbin Guoc2b908c2019-09-09 16:24:38 -0700148 logging.debug('%s failed to parse. Caught %s', endpoint, str(e))
Paul Hobbsfa915682016-07-19 15:11:29 -0700149
150 return build_config, milestone, filename
151
152
153def EmitStaticRequestMetric(m):
154 """Emits a Counter metric for sucessful GETs to /static endpoints.
155
156 Args:
157 m: A regex match object
158 """
159 build_config, milestone, filename = ParseStaticEndpoint(m.group('endpoint'))
160
161 try:
162 size = int(m.group('size'))
Paul Hobbsef4e0702016-06-27 17:01:42 -0700163 except ValueError: # Zero is represented by "-"
164 size = 0
165
166 metrics.Counter(STATIC_GET_METRIC_NAME).increment_by(
167 size, fields={
Paul Hobbsfa915682016-07-19 15:11:29 -0700168 'build_config': build_config,
169 'milestone': milestone,
Paul Hobbs487e3812016-07-22 15:45:33 -0700170 'in_lab': InLab(m.group('ip_addr')),
Paul Hobbsfa915682016-07-19 15:11:29 -0700171 'endpoint': filename})
Paul Hobbsef4e0702016-06-27 17:01:42 -0700172
173
174def RunMatchers(stream, matchers):
Paul Hobbsfa915682016-07-19 15:11:29 -0700175 """Parses lines of |stream| using patterns and emitters from |matchers|
176
177 Args:
178 stream: A file object to read from.
179 matchers: A list of pairs of (matcher, emitter), where matcher is a regex
180 and emitter is a function called when the regex matches.
181 """
Paul Hobbs338baee2016-07-13 13:42:34 -0700182 for line in iter(stream.readline, ''):
Paul Hobbsef4e0702016-06-27 17:01:42 -0700183 for matcher, emitter in matchers:
Paul Hobbs338baee2016-07-13 13:42:34 -0700184 logging.debug('Emitting %s for input "%s"',
185 emitter.__name__, line.strip())
Paul Hobbsef4e0702016-06-27 17:01:42 -0700186 m = matcher.match(line)
187 if m:
188 emitter(m)
Paul Hobbsef4e0702016-06-27 17:01:42 -0700189
190
191# TODO(phobbs) add a matcher for all requests, not just static files.
192MATCHERS = [
193 (STATIC_GET_MATCHER, EmitStaticRequestMetric),
194]
195
196
197def ParseArgs():
198 """Parses command line arguments."""
199 p = argparse.ArgumentParser(
200 description='Parses apache logs and emits metrics to Monarch')
xixuanebdb0a82017-04-28 11:25:02 -0700201 p.add_argument('--logfile', required=True)
Paul Hobbsef4e0702016-06-27 17:01:42 -0700202 return p.parse_args()
203
204
205def main():
206 """Sets up logging and runs matchers against stdin"""
207 args = ParseArgs()
208 root = logging.getLogger()
xixuanebdb0a82017-04-28 11:25:02 -0700209
Paul Hobbs84acd9d2017-09-20 15:10:53 -0700210 root.addHandler(handlers.TimedRotatingFileHandler(
Paul Hobbsfe0b1c62017-08-18 12:56:14 -0700211 args.logfile, when=_LOG_ROTATION_TIME,
212 interval=_LOG_ROTATION_INTERVAL,
213 backupCount=_LOG_ROTATION_BACKUP))
Paul Hobbsef4e0702016-06-27 17:01:42 -0700214 root.setLevel(logging.DEBUG)
Paul Hobbsfe0b1c62017-08-18 12:56:14 -0700215 with ts_mon_config.SetupTsMonGlobalState('devserver_apache_log_metrics',
216 indirect=True):
217 RunMatchers(sys.stdin, MATCHERS)
Paul Hobbsef4e0702016-06-27 17:01:42 -0700218
219
220if __name__ == '__main__':
221 main()