diff options
Diffstat (limited to 'bin/cfn-push-stats')
-rwxr-xr-x | bin/cfn-push-stats | 259 |
1 files changed, 259 insertions, 0 deletions
diff --git a/bin/cfn-push-stats b/bin/cfn-push-stats new file mode 100755 index 0000000..09fe72c --- /dev/null +++ b/bin/cfn-push-stats @@ -0,0 +1,259 @@ +#!/usr/bin/env python +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Implements cfn-push-stats CloudFormation functionality +""" +import argparse +import logging +import os +import subprocess +import random +import sys + +# Override BOTO_CONFIG, which makes boto look only at the specified +# config file, instead of the default locations +os.environ['BOTO_CONFIG'] = '/var/lib/cloud/data/cfn-boto-cfg' +from boto.ec2.cloudwatch import CloudWatchConnection + + +log_format = '%(levelname)s [%(asctime)s] %(message)s' +log_file_name = "/var/log/cfn-push-stats.log" +logging.basicConfig(filename=log_file_name, + format=log_format, + level=logging.DEBUG) +LOG = logging.getLogger('cfntools') + +try: + import psutil +except ImportError: + LOG.warn("psutil not available. If you want process and memory " + "statistics, you need to install it.") + +if os.path.exists('/opt/aws/bin'): + sys.path.insert(0, '/opt/aws/bin') + from cfn_helper import * +else: + from heat_jeos.cfntools.cfn_helper import * + +KILO = 1024 +MEGA = 1048576 +GIGA = 1073741824 +unit_map = {'bytes': 1, + 'kilobytes': KILO, + 'megabytes': MEGA, + 'gigabytes': GIGA} + +description = " " +parser = argparse.ArgumentParser(description=description) +parser.add_argument('-v', '--verbose', action="store_true", + help="Verbose logging", required=False) +parser.add_argument('--credential-file', dest="credential_file", + help="credential-file", required=False, + default='/etc/cfn/cfn-credentials') +parser.add_argument('--service-failure', required=False, action="store_true", + help='Reports a service falure.') +parser.add_argument('--mem-util', required=False, action="store_true", + help='Reports memory utilization in percentages.') +parser.add_argument('--mem-used', required=False, action="store_true", + help='Reports memory used (excluding cache and buffers) in megabytes.') +parser.add_argument('--mem-avail', required=False, action="store_true", + help='Reports available memory (including cache and buffers) in megabytes.') +parser.add_argument('--swap-util', required=False, action="store_true", + help='Reports swap utilization in percentages.') +parser.add_argument('--swap-used', required=False, action="store_true", + help='Reports allocated swap space in megabytes.') +parser.add_argument('--disk-space-util', required=False, action="store_true", + help='Reports disk space utilization in percentages.') +parser.add_argument('--disk-space-used', required=False, action="store_true", + help='Reports allocated disk space in gigabytes.') +parser.add_argument('--disk-space-avail',required=False, action="store_true", + help='Reports available disk space in gigabytes.') +parser.add_argument('--memory-units', required=False, default='megabytes', + help='Specifies units for memory metrics.') +parser.add_argument('--disk-units', required=False, default='megabytes', + help='Specifies units for disk metrics.') +parser.add_argument('--disk-path', required=False, default='/', + help='Selects the disk by the path on which to report.') +parser.add_argument('--cpu-util', required=False, action="store_true", + help='Reports cpu utilization in percentages.') +parser.add_argument('--haproxy', required=False, action='store_true', + help='Reports HAProxy loadbalancer usage.') +parser.add_argument('--heartbeat', required=False, action='store_true', + help='Sends a Heartbeat.') +parser.add_argument('--watch', required=True, + help='the name of the watch to post to.') +args = parser.parse_args() + +LOG.debug('cfn-push-stats called %s ' % (str(args))) + +credentials = parse_creds_file(args.credential_file) + +namespace = 'system/linux' +data = {} + +# service failure +# =============== +if args.service_failure: + data['ServiceFailure'] = { + 'Value': 1, + 'Units': 'Counter'} + +# heatbeat +# ======== +if args.heartbeat: + data['Heartbeat'] = { + 'Value': 1, + 'Units': 'Counter'} + +# memory space +# ============ +if args.mem_util or args.mem_used or args.mem_avail: + mem = psutil.phymem_usage() +if args.mem_util: + data['MemoryUtilization'] = { + 'Value': mem.percent, + 'Units': 'Percent'} +if args.mem_used: + data['MemoryUsed'] = { + 'Value': mem.used / unit_map[args.memory_units], + 'Units': args.memory_units} +if args.mem_avail: + data['MemoryAvailable'] = { + 'Value': mem.free / unit_map[args.memory_units], + 'Units': args.memory_units} + +# swap space +# ========== +if args.swap_util or args.swap_used: + swap = psutil.virtmem_usage() +if args.swap_util: + data['SwapUtilization'] = { + 'Value': swap.percent, + 'Units': 'Percent'} +if args.swap_used: + data['SwapUsed'] = { + 'Value': swap.used / unit_map[args.memory_units], + 'Units': args.memory_units} + +# disk space +# ========== +if args.disk_space_util or args.disk_space_used or args.disk_space_avail: + disk = psutil.disk_usage(args.disk_path) +if args.disk_space_util: + data['DiskSpaceUtilization'] = { + 'Value': disk.percent, + 'Units': 'Percent'} +if args.disk_space_used: + data['DiskSpaceUsed'] = { + 'Value': disk.used / unit_map[args.disk_units], + 'Units': args.disk_units} +if args.disk_space_avail: + data['DiskSpaceAvailable'] = { + 'Value': disk.free / unit_map[args.disk_units], + 'Units': args.disk_units} + +# cpu utilization +# =============== +if args.cpu_util: + # blocks for 1 second. + cpu_percent = psutil.cpu_percent(interval=1) + data['CPUUtilization'] = { + 'Value': cpu_percent, + 'Units': 'Percent'} + +# HAProxy +# ======= +def parse_haproxy_unix_socket(res): + # http://docs.amazonwebservices.com/ElasticLoadBalancing/latest + # /DeveloperGuide/US_MonitoringLoadBalancerWithCW.html + + type_map = {'FRONTEND': '0', 'BACKEND': '1', 'SERVER': '2', 'SOCKET': '3'} + num_map = {'status': 17, 'svname': 1, 'check_duration': 38, 'type': 32, + 'req_tot': 48, 'hrsp_2xx': 40, 'hrsp_3xx': 41, 'hrsp_4xx': 42, + 'hrsp_5xx': 43} + + def add_stat(key, value, unit='Counter'): + res[key] = {'Value': value, + 'Units': unit} + + echo = subprocess.Popen(['echo', 'show stat'], + stdout=subprocess.PIPE) + socat = subprocess.Popen(['socat', 'stdio', '/tmp/.haproxy-stats'], + stdin=echo.stdout, + stdout=subprocess.PIPE) + end_pipe = socat.stdout + raw = [l.strip('\n').split(',') for l in end_pipe + if l[0] != '#' and len(l) > 2] + latency = 0 + up_count = 0 + down_count = 0 + for f in raw: + if f[num_map['type']] == type_map['FRONTEND']: + add_stat('RequestCount', f[num_map['req_tot']]) + add_stat('HTTPCode_ELB_4XX', f[num_map['hrsp_4xx']]) + add_stat('HTTPCode_ELB_5XX', f[num_map['hrsp_5xx']]) + elif f[num_map['type']] == type_map['BACKEND']: + add_stat('HTTPCode_Backend_2XX', f[num_map['hrsp_2xx']]) + add_stat('HTTPCode_Backend_3XX', f[num_map['hrsp_3xx']]) + add_stat('HTTPCode_Backend_4XX', f[num_map['hrsp_4xx']]) + add_stat('HTTPCode_Backend_5XX', f[num_map['hrsp_5xx']]) + else: + if f[num_map['status']] == 'UP': + up_count = up_count + 1 + else: + down_count = down_count + 1 + if f[num_map['check_duration']] != '': + latency = max(float(f[num_map['check_duration']]), latency) + + # note: haproxy's check_duration is in ms, but Latency is in seconds + add_stat('Latency', str(latency / 1000), unit='Seconds') + add_stat('HealthyHostCount', str(up_count)) + add_stat('UnHealthyHostCount', str(down_count)) + + +def send_stats(info): + + # Create boto connection, need the hard-coded port/path as boto + # can't read these from config values in BOTO_CONFIG + # FIXME : currently only http due to is_secure=False + client = CloudWatchConnection( + aws_access_key_id=credentials['AWSAccessKeyId'], + aws_secret_access_key=credentials['AWSSecretKey'], + is_secure=False, port=8003, path="/v1", debug=0) + + # Then we send the metric datapoints passed in "info", note this could + # contain multiple keys as the options parsed above are noe exclusive + # The alarm name is passed as a dimension so the metric datapoint can + # be associated with the alarm/watch in the engine + metric_dims = [{'AlarmName': args.watch}] + for key in info: + LOG.info("Sending watch %s metric %s, Units %s, Value %s" % + (args.watch, key, info[key]['Units'], info[key]['Value'])) + client.put_metric_data(namespace=namespace, + name=key, + value=info[key]['Value'], + timestamp=None, # means use "now" in the engine + unit=info[key]['Units'], + dimensions=metric_dims, + statistics=None) + + +if args.haproxy: + namespace = 'AWS/ELB' + lb_data = {} + parse_haproxy_unix_socket(lb_data) + send_stats(lb_data) +else: + send_stats(data) |