-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcloudwatchd-worker
More file actions
executable file
·247 lines (212 loc) · 9.5 KB
/
cloudwatchd-worker
File metadata and controls
executable file
·247 lines (212 loc) · 9.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
#!/usr/bin/python
"""A daemon for running scripts and posting output to Amazon's Cloudwatch."""
"""
This file is part of cloudwatchd.
Cloudwatchd is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Cloudwatchd is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with cloudwatchd. If not, see <http://www.gnu.org/licenses/>.
"""
__author__ = 'Royce Remer (royce@sunrun.com)'
__version__ = "3.0"
import boto
import logging
import re
from logging import handlers as logging_handlers
from optparse import OptionParser
from os import listdir, popen, uname
from sys import argv, stdout
from time import sleep
def init_option_parser(args):
"""Setup command line argument parser.
Args:
args: List of strings, command-line arguments to be parsed.
Returns:
options: OptionParser options object.
"""
parser = OptionParser()
parser.add_option("-c", "--conf", "--config",
dest="conf_filename",
action="store",
type="string",
default="/etc/cloudwatchd/cloudwatchd.conf",
help="Cloudwatch configuration file to source; default: %default")
parser.add_option("-i", "--interval",
dest="interval",
action="store",
type="int",
default="30",
help="Frequency interval to run metric scripts at (in seconds); "
"default: %default")
parser.add_option("-l", "--log",
dest="log_filepath",
action="store",
type="string",
default="/var/log/cloudwatchd/cloudwatchd.log",
help="Log activities to this location, filepath or 'stdout'")
options = parser.parse_args(args)
return options
def parse_conf(filepath):
"""Take a path to a configuration file and parse its options into a dict.
Args:
filepath: String, full system path to a file to parse.
Returns:
options: Dict, option pairs contained in the config file provided.
"""
options = {}
with open(filepath,'r') as conf:
for line in conf:
if line[0] is not "#" and line.find("=") is not -1:
try:
key, value = line.split("=", 1)
value = value.strip()
options[key] = value
except ValueError:
pass # We found an "=" sign that is missing a key or a value
return options
def get_connection(credentials, logger):
"""Take Cloudwatch credentials and connect to local endpoint.
Args:
credentials: Dict of Amazon Cloudwatch read/write API credentials.
logger: Logging object.
Returns:
connection: boto.cloudwatch connection object.
"""
# First connection to collect metadata, can be foobar credentials...
connection = boto.connect_cloudwatch(
aws_access_key_id=aws_creds.get('AWSAccessKeyId'),
aws_secret_access_key=aws_creds.get('AWSSecretKey'))
instance_metadata = boto.utils.get_instance_metadata()
instance_region_str = ((instance_metadata.get('placement')
.get('availability-zone'))[:-1])
instance_region_reg = re.compile(instance_region_str)
# Get a list of region objects and find which one to connect to
cloudwatch_region_index = None
cloudwatch_region_list = boto.ec2.cloudwatch.regions()
for index, item in enumerate(cloudwatch_region_list):
if bool(re.search(instance_region_reg, repr(item))):
cloudwatch_region_index = index
if cloudwatch_region_index == None:
logger.info("Local region '%s' not one of listed endpoints %s" %(
instance_region_str, cloudwatch_region_list))
# Reconnect to the correct region endpoint
try:
region = cloudwatch_region_list[cloudwatch_region_index]
logger.info("Connecting to %s" %region)
connection = boto.connect_cloudwatch(
aws_access_key_id=aws_creds.get('AWSAccessKeyId'),
aws_secret_access_key=aws_creds.get('AWSSecretKey'),
region=region)
except boto.exception.BotoServerError as error:
logger.info("Error connecting to cloudwatch with credentials %s: %s" %(
credentials,error.error_message))
return connection
def put_metrics(cloudwatch_con, metrics_dir, options, logger):
"""Runs scripts out of a directory and sends the output to Cloudwatch.
Args:
credentials: Dict of Amazon Cloudwatch read/write API credentials.
metrics_dir: String, full path to a directory of executable scripts.
options: OptionParser options object.
logger: Logging object.
"""
instance_metadata = boto.utils.get_instance_metadata()
cloudwatch_metric_dict = (
{'dimensions':
{'instanceid':instance_metadata.get('instance-id'),
'hostname':uname()[1]}})
metrics_dir_list = listdir(metrics_dir)
logger.info("Metric scripts found: %s" %repr(metrics_dir_list))
# Continuously loop through the metrics dir (sleep at the end)
while True:
metrics_dir_updated_list = listdir(metrics_dir)
if metrics_dir_list != metrics_dir_updated_list:
metrics_dir_list = metrics_dir_updated_list
logger.info("Metrics dir contents have changed, found: %s"
%repr(metrics_dir_list))
for script in metrics_dir_list:
cloudwatch_metric_dict = {}
script_path = "%s/%s" % (metrics_dir, script)
if script.find(".") is not -1:
metric_name, script_extension = script.split(".", 1)
else:
metric_name = script
re_shebang = re.compile('#!')
with open(script_path, 'r') as script_file:
re_match = re.search(re_shebang, script_file.readline())
if re_match is None:
logger.warn("Skippng %s, line 1 must have #! with runpath"
%script_path)
else:
shebang = re_match.string.strip()
runner = re.sub(re_shebang, '', shebang)
cloudwatch_metric_dict['dimensions'] = (
{'instanceid':instance_metadata.get('instance-id'),
'hostname':uname()[1]})
cloudwatch_metric_dict['namespace'] = (parse_conf(script_path)
.get('NAMESPACE'))
if cloudwatch_metric_dict['namespace'] == None:
logger.info("%s had no NAMESPACE key set, " %script_path +
"putting in 'default'")
cloudwatch_metric_dict['namespace'] = "default"
cloudwatch_metric_dict['name'] = metric_name
cloudwatch_metric_dict['unit'] = (parse_conf(script_path)
.get('METRIC_UNIT'))
logger.info("Running %s %s" %(runner, script_path))
# Run the script found, assume stdout is a float
try:
cloudwatch_metric_dict['value'] = float(popen("%s %s"
%(runner, script_path)).readline().strip())
except ValueError:
cloudwatch_metric_dict['value'] = None
# Strip uneccesary precision for this metric
if str(cloudwatch_metric_dict.get('value'))[-2:] == '.0':
cloudwatch_metric_dict['value'] = (
int(cloudwatch_metric_dict.get('value')))
# If the output of the script was not usable, warn and continue
if isinstance(cloudwatch_metric_dict.get('value'),
(int,long,float)):
logger.info(("Sending %s to %s") %(
cloudwatch_metric_dict,cloudwatch_con.region))
cloudwatch_con.put_metric_data(**cloudwatch_metric_dict)
else:
logger.warn("""Last run of %s produced no value,
nothing to send to cloudwatch""" %script_path)
logger.info(("Sleeping %s seconds") %options.interval)
sleep(options.interval)
def init_logger(options):
"""Setup logging facility.
Args:
options: OptionParser options object.
Returns:
logger: Logging object.
"""
logger = logging.getLogger('logger')
logger.setLevel(logging.DEBUG)
if options.log_filepath == 'stdout':
handler = logging.StreamHandler(stdout)
else:
handler = logging_handlers.RotatingFileHandler(
options.log_filepath, mode='a', maxBytes=20000, backupCount=5)
handler.setLevel(logging.DEBUG)
logger.addHandler(handler)
return logger
if __name__ == "__main__":
(cli_options, cli_args) = init_option_parser(args=argv)
Logger = init_logger(cli_options)
Logger.info("Using %s as configuration file" %cli_options.conf_filename)
cloudwatch_conf = parse_conf(cli_options.conf_filename)
aws_creds = parse_conf(cloudwatch_conf.get('AWS_CREDENTIAL_FILE'))
Logger.info(("Using %s as credentials file") %(cloudwatch_conf.get(
'AWS_CREDENTIAL_FILE')))
CloudwatchCon = get_connection(credentials=aws_creds, logger=Logger)
put_metrics(
cloudwatch_con=CloudwatchCon,
metrics_dir=cloudwatch_conf.get('CLOUDWATCH_METRICS_DIR'),
options=cli_options,
logger=Logger)