blob: 0d35d095d42cf4a6e4a6fa4facce58a136988dfe [file] [log] [blame]
Paul Hobbsef4e0702016-06-27 17:01:42 -07001#!/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-27 17:01:42 -070015import 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 13:42:34 -070022from chromite.lib import cros_logging as logging
Paul Hobbsef4e0702016-06-27 17:01:42 -070023from 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 15:11:29 -070028 r'.*GET /static/(?P<endpoint>\S*)[^"]*" '
Paul Hobbsef4e0702016-06-27 17:01:42 -070029 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 15:11:29 -070050 """Returns the integer represented by an IPv4 string.
51
52 Args:
53 ip: An IPv4-formatted string.
54 """
55 return reduce(lambda (seed, x): seed * 2**8 + int(x),
56 ip.split('.'),
57 0)
Paul Hobbsef4e0702016-06-27 17:01:42 -070058
59
60def MatchesSubnet(ip, base, mask):
Paul Hobbsfa915682016-07-19 15:11:29 -070061 """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-27 17:01:42 -070068 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 15:11:29 -070075 """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-27 17:01:42 -070080 return any(MatchesSubnet(ip, base, mask)
81 for (base, mask) in LAB_SUBNETS)
82
83
Paul Hobbsfa915682016-07-19 15:11:29 -070084def ParseStaticEndpoint(endpoint):
85 """Parses a /static/.* URL path into build_config, milestone, and filename.
86
87 Static endpoints are expected to be of the form
88 /static/$BUILD_CONFIG/$MILESTONE-$VERSION/$FILENAME
89
90 This function expects the '/static/' prefix to already be stripped off.
91
92 Args:
93 endpoint: A string which is the matched URL path after /static/
94 """
95 build_config, milestone, filename = [''] * 3
Paul Hobbsef4e0702016-06-27 17:01:42 -070096 try:
Paul Hobbsfa915682016-07-19 15:11:29 -070097 parts = endpoint.split('/')
98 build_config = parts[0]
99 if len(parts) >= 2:
100 version = parts[1]
101 milestone = version[:version.index('-')]
102 if len(parts) >= 3:
103 filename = parts[-1]
104 except IndexError as e:
105 logging.debug('%s failed to parse. Caught %s' % (endpoint, str(e)))
106
107 return build_config, milestone, filename
108
109
110def EmitStaticRequestMetric(m):
111 """Emits a Counter metric for sucessful GETs to /static endpoints.
112
113 Args:
114 m: A regex match object
115 """
116 build_config, milestone, filename = ParseStaticEndpoint(m.group('endpoint'))
117
118 try:
119 size = int(m.group('size'))
Paul Hobbsef4e0702016-06-27 17:01:42 -0700120 except ValueError: # Zero is represented by "-"
121 size = 0
122
123 metrics.Counter(STATIC_GET_METRIC_NAME).increment_by(
124 size, fields={
Paul Hobbsfa915682016-07-19 15:11:29 -0700125 'build_config': build_config,
126 'milestone': milestone,
127 'in_lab': InLab(m.group('ipaddr')),
128 'endpoint': filename})
Paul Hobbsef4e0702016-06-27 17:01:42 -0700129
130
131def RunMatchers(stream, matchers):
Paul Hobbsfa915682016-07-19 15:11:29 -0700132 """Parses lines of |stream| using patterns and emitters from |matchers|
133
134 Args:
135 stream: A file object to read from.
136 matchers: A list of pairs of (matcher, emitter), where matcher is a regex
137 and emitter is a function called when the regex matches.
138 """
Paul Hobbs338baee2016-07-13 13:42:34 -0700139 for line in iter(stream.readline, ''):
Paul Hobbsef4e0702016-06-27 17:01:42 -0700140 for matcher, emitter in matchers:
Paul Hobbs338baee2016-07-13 13:42:34 -0700141 logging.debug('Emitting %s for input "%s"',
142 emitter.__name__, line.strip())
Paul Hobbsef4e0702016-06-27 17:01:42 -0700143 m = matcher.match(line)
144 if m:
145 emitter(m)
146 # The input might terminate if the log gets rotated. Make sure that Monarch
147 # flushes any pending metrics before quitting.
148 ts_mon.close()
149
150
151# TODO(phobbs) add a matcher for all requests, not just static files.
152MATCHERS = [
153 (STATIC_GET_MATCHER, EmitStaticRequestMetric),
154]
155
156
157def ParseArgs():
158 """Parses command line arguments."""
159 p = argparse.ArgumentParser(
160 description='Parses apache logs and emits metrics to Monarch')
161 p.add_argument('--logfile')
162 return p.parse_args()
163
164
165def main():
166 """Sets up logging and runs matchers against stdin"""
167 args = ParseArgs()
168 root = logging.getLogger()
169 root.addHandler(MakeLogHandler(args.logfile))
170 root.setLevel(logging.DEBUG)
171 ts_mon_config.SetupTsMonGlobalState('devserver_apache_log_metrics')
172 RunMatchers(sys.stdin, MATCHERS)
173
174
175if __name__ == '__main__':
176 main()