diff --git a/README.md b/README.md index 43f0ce5..6327528 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,5 @@ -import monascastatsd.monasca_statsdmonasca-statsd -================ - A Monasca-Statsd Python client. +================ Quick Start Guide ----------------- @@ -18,35 +16,60 @@ Then start instrumenting your code: ``` # Import the module. -import monascastatsd.monasca_statsd +import monascastatsd as mstatsd -mstatsd = monascastatsd.monasca_statsd.MonascaStatsd() +# Create the connection +conn = mstatsd.Connection(host='localhost', port=8125) -# Optionally, configure the host and port if you're running Statsd on a -# non-standard port. -mstatsd.connect('localhost', 8125) +# Create the client with optional dimensions +client = mstatsd.Client(connection=conn, dimensions={'env': 'test'}) -# Increment a counter. -mstatsd.increment('page.views') +NOTE: You can also create a client without specifying the connection and it will create the client with the default connection information for the monasca-agent statsd processor daemon which uses host='localhost' and port=8125. + +client = mstatsd.Client(dimensions={'env': 'test'}) + +# Increment and decrement a counter. +counter = client.get_counter(name='page.views') + +counter.increment() +counter += 3 + +counter.decrement() +counter -= 3 # Record a gauge 50% of the time. -mstatsd.gauge('users.online', 123, sample_rate=0.5) +gauge = client.get_gauge('gauge', dimensions={'env': 'test'}) + +gauge.send('metric', 123.4, sample_rate=0.5) # Sample a histogram. -mstatsd.histogram('file.upload.size', 1234) +histogram = client.get_histogram('histogram', dimensions={'test': 'True'}) + +histogram.send('metric', 123.4, dimensions={'color': 'red'}) # Time a function call. -@mstatsd.timed('page.render') +timer = client.get_timer() + +@timer.timed('page.render') def render_page(): # Render things ... + pass -# Add a dimension to a metric. -mstatsd.histogram('query.time', 10, dimensions = {'version': '1.0', 'environment': 'dev'}) +# Time a block of code. +timer = client.get_timer() + +with timer.time('t'): + # Do stuff + time.sleep(2) + +# Add dimensions to any metric. +histogram = client.get_histogram('my_hist') +histogram.send('query.time', 10, dimensions = {'version': '1.0', 'environment': 'dev'}) ``` -Documentation +Repository ------------- -Read the full API docs +The monasca-statsd code is located here: [here](https://github.com/stackforge/monasca-statsd). Feedback @@ -55,11 +78,6 @@ Feedback To suggest a feature, report a bug, or general discussion, head over [here](https://bugs.launchpad.net/monasca). -Change Log ----------- -- 1.0.0 - - Initial version of the code - License ------- diff --git a/monascastatsd/__init__.py b/monascastatsd/__init__.py index e69de29..d81c78e 100644 --- a/monascastatsd/__init__.py +++ b/monascastatsd/__init__.py @@ -0,0 +1,8 @@ +from client import Client +from connection import Connection +from counter import Counter +from gauge import Gauge +from histogram import Histogram +from metricbase import MetricBase +from set import Set +from timer import Timer diff --git a/monascastatsd/client.py b/monascastatsd/client.py new file mode 100644 index 0000000..ef66a8c --- /dev/null +++ b/monascastatsd/client.py @@ -0,0 +1,124 @@ +# Copyright (c) 2014 Hewlett-Packard Development Company, L.P. +# +# 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. + +"""Monasca-Statsd is a Python client for Statsd that adds dimensions. +""" +from monascastatsd.connection import Connection +from monascastatsd.counter import Counter +from monascastatsd.gauge import Gauge +from monascastatsd.histogram import Histogram +from monascastatsd.set import Set +from monascastatsd.timer import Timer + + +class Client(object): + + def __init__(self, name=None, connection=None, max_buffer_size=50, dimensions=None): + """Initialize a Client object. + + >>> monascastatsd = MonascaStatsd() + + :name: the name for this client. Everything sent by this client + will be prefixed by name + :param host: the host of the MonascaStatsd server. + :param port: the port of the MonascaStatsd server. + :param max_buffer_size: Maximum number of metric to buffer before + sending to the server if sending metrics in batch + """ + if connection is None: + self.connection = Connection(host='localhost', + port=8125, + max_buffer_size=50) + else: + self.connection = connection + self._dimensions = dimensions + self._name = name + + def get_counter(self, name, connection=None, dimensions=None): + """Gets a Counter object. + + """ + if connection is None: + connection = self.connection + return Counter(name=self._update_name(name), + connection=connection, + dimensions=self._update_dimensions(dimensions)) + + def get_gauge(self, name=None, connection=None, dimensions=None): + """Gets a Gauge object. + + """ + if connection is None: + connection = self.connection + return Gauge(name=self._update_name(name), + connection=connection, + dimensions=self._update_dimensions(dimensions)) + + def get_histogram(self, name=None, connection=None, dimensions=None): + """Gets a Histogram object. + + """ + if connection is None: + connection = self.connection + return Histogram(name=self._update_name(name), + connection=connection, + dimensions=self._update_dimensions(dimensions)) + + def get_set(self, name=None, connection=None, dimensions=None): + """Gets a Set object. + + """ + if connection is None: + connection = self.connection + return Set(name=self._update_name(name), + connection=connection, + dimensions=self._update_dimensions(dimensions)) + + def get_timer(self, name=None, connection=None, dimensions=None): + """Gets a Timer object. + + """ + if connection is None: + connection = self.connection + return Timer(name=self._update_name(name), + connection=connection, + dimensions=self._update_dimensions(dimensions)) + + def _update_name(self, name): + """Update the metric name with the client + + name that was passed in on instantiation. + """ + if self._name: + metric = self._name + if name: + metric = metric + "." + name + else: + metric = name + return metric + + def _update_dimensions(self, dimensions): + """Update the dimensions list with the default + + dimensions that were passed in on instantiation. + """ + if self._dimensions: + new_dimensions = self._dimensions.copy() + else: + new_dimensions = {} + if dimensions: + new_dimensions.update(dimensions) + + return new_dimensions diff --git a/monascastatsd/connection.py b/monascastatsd/connection.py new file mode 100644 index 0000000..d42bb40 --- /dev/null +++ b/monascastatsd/connection.py @@ -0,0 +1,97 @@ +import logging +import random +import socket + +try: + import itertools +except ImportError: + imap = map + +logging.basicConfig() +log = logging.getLogger(__name__) + + +class Connection(object): + + def __init__(self, host='localhost', port=8125, max_buffer_size=50): + """Initialize a Connection object. + + >>> monascastatsd = MonascaStatsd() + + :name: the name for this client. Everything sent by this client + will be prefixed by name + :param host: the host of the MonascaStatsd server. + :param port: the port of the MonascaStatsd server. + :param max_buffer_size: Maximum number of metric to buffer before + sending to the server if sending metrics in batch + """ + self._host = None + self._port = None + self.socket = None + self.max_buffer_size = max_buffer_size + self._send = self._send_to_server + self.connect(host, port) + self.encoding = 'utf-8' + + def __enter__(self): + self.open_buffer(self.max_buffer_size) + return self + + def __exit__(self, the_type, value, traceback): + self.close_buffer() + + def open_buffer(self, max_buffer_size=50): + """Open a buffer to send a batch of metrics in one packet. + + """ + self.max_buffer_size = max_buffer_size + self.buffer = [] + self._send = self._send_to_buffer + + def close_buffer(self): + """Flush the buffer and switch back to single metric packets. + + """ + self._send = self._send_to_server + self._flush_buffer() + + def connect(self, host, port): + """Connect to the monascastatsd server on the given host and port. + + """ + self._host = host + self._port = int(port) + self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.socket.connect((self._host, self._port)) + + def report(self, metric, metric_type, value, dimensions, sample_rate): + """Use this connection to report metrics. + + """ + if sample_rate != 1 and random.random() > sample_rate: + return + + payload = [metric, ":", value, "|", metric_type] + if sample_rate != 1: + payload.extend(["|@", sample_rate]) + if dimensions: + payload.extend(["|#"]) + payload.append(dimensions) + + encoded = "".join(itertools.imap(str, payload)) + self._send(encoded) + + def _send_to_server(self, packet): + try: + self.socket.send(packet.encode(self.encoding)) + except socket.error: + log.exception("Error submitting metric") + + def _send_to_buffer(self, packet): + self.buffer.append(packet) + if len(self.buffer) >= self.max_buffer_size: + self._flush_buffer() + + def _flush_buffer(self): + self._send_to_server("\n".join(self.buffer)) + self.buffer = [] diff --git a/monascastatsd/counter.py b/monascastatsd/counter.py new file mode 100644 index 0000000..7d617fa --- /dev/null +++ b/monascastatsd/counter.py @@ -0,0 +1,55 @@ +from metricbase import MetricBase + + +class Counter(MetricBase): + + def __init__(self, name, connection, dimensions=None): + super(self.__class__, self).__init__(name=name, + connection=connection, + dimensions=dimensions) + + def increment(self, value=1, dimensions=None, sample_rate=1): + """Increment a counter, optionally setting a value, dimensions + + and a sample rate. + + >>> monascastatsd.increment() + >>> monascastatsd.increment(12) + """ + self._connection.report(metric=self._name, + metric_type='c', + value=value, + dimensions=self.update_dimensions(dimensions), + sample_rate=sample_rate) + + def decrement(self, value=1, dimensions=None, sample_rate=1): + """Decrement a counter, optionally setting a value, dimensions and a + + sample rate. + + >>> monascastatsd.decrement() + >>> monascastatsd.decrement(2) + """ + self._connection.report(metric=self._name, + metric_type='c', + value=-value, + dimensions=self.update_dimensions(dimensions), + sample_rate=sample_rate) + + def __add__(self, value): + """Increment the counter with `value` + + :keyword value: The value to add to the counter + :type value: int + """ + self.increment(value=value) + return self + + def __sub__(self, value): + """Decrement the counter with `value` + + :keyword value: The value to remove from the counter + :type value: int + """ + self.decrement(value=value) + return self diff --git a/monascastatsd/gauge.py b/monascastatsd/gauge.py new file mode 100644 index 0000000..35ca0f9 --- /dev/null +++ b/monascastatsd/gauge.py @@ -0,0 +1,24 @@ +from metricbase import MetricBase + + +class Gauge(MetricBase): + + def __init__(self, connection, name=None, dimensions=None): + super(self.__class__, self).__init__(name=name, + connection=connection, + dimensions=dimensions) + + def send(self, name, value, dimensions=None, sample_rate=1): + """Record the value of a gauge, optionally setting a list of + + dimensions and a sample rate. + + >>> monascastatsd.gauge('users.online', 123) + >>> monascastatsd.gauge('active.connections', 1001, + >>> dimensions={"protocol": "http"}) + """ + self._connection.report(metric=self.update_name(name), + metric_type='g', + value=value, + dimensions=self.update_dimensions(dimensions), + sample_rate=sample_rate) diff --git a/monascastatsd/histogram.py b/monascastatsd/histogram.py new file mode 100644 index 0000000..18828ed --- /dev/null +++ b/monascastatsd/histogram.py @@ -0,0 +1,24 @@ +from metricbase import MetricBase + + +class Histogram(MetricBase): + + def __init__(self, connection, name=None, dimensions=None): + super(self.__class__, self).__init__(name=name, + connection=connection, + dimensions=dimensions) + + def send(self, name, value, dimensions=None, sample_rate=1): + """Sample a histogram value, optionally setting dimensions and a + + sample rate. + + >>> monascastatsd.histogram('uploaded.file.size', 1445) + >>> monascastatsd.histogram('album.photo.count', 26, + >>> dimensions={"gender": "female"}) + """ + self._connection.report(metric=self.update_name(name), + metric_type='h', + value=value, + dimensions=self.update_dimensions(dimensions), + sample_rate=sample_rate) diff --git a/monascastatsd/metricbase.py b/monascastatsd/metricbase.py new file mode 100644 index 0000000..5504c39 --- /dev/null +++ b/monascastatsd/metricbase.py @@ -0,0 +1,36 @@ +class MetricBase(object): + """Base class for all metric types. + + """ + + def __init__(self, name, connection, dimensions): + self._name = name + self._connection = connection + self._dimensions = dimensions + + def update_dimensions(self, dimensions): + """Update the dimensions list with the default + + dimensions that were passed in on instantiation. + """ + if self._dimensions: + new_dimensions = self._dimensions.copy() + else: + new_dimensions = {} + if dimensions: + new_dimensions.update(dimensions) + + return new_dimensions + + def update_name(self, name): + """Update the metric name with the metric + + base name that was passed in on instantiation. + """ + if self._name: + metric = self._name + if name: + metric = metric + "." + name + else: + metric = name + return metric diff --git a/monascastatsd/monasca_statsd.py b/monascastatsd/monasca_statsd.py deleted file mode 100644 index d095325..0000000 --- a/monascastatsd/monasca_statsd.py +++ /dev/null @@ -1,207 +0,0 @@ -# Copyright (c) 2014 Hewlett-Packard Development Company, L.P. -# -# 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. - -"""Monasca-Statsd is a Python client for Statsd that adds dimensions. -""" - -import functools -import logging -import random -import socket -import time - -try: - import itertools -except ImportError: - imap = map - - -log = logging.getLogger(__name__) - - -class MonascaStatsd(object): - - def __init__(self, host='localhost', port=8125, max_buffer_size=50): - """Initialize a MonascaStatsd object. - - >>> monascastatsd = MonascaStatsd() - - :param host: the host of the MonascaStatsd server. - :param port: the port of the MonascaStatsd server. - :param max_buffer_size: Maximum number of metric to buffer before - sending to the server if sending metrics in batch - """ - self._host = None - self._port = None - self.socket = None - self.max_buffer_size = max_buffer_size - self._send = self._send_to_server - self.connect(host, port) - self.encoding = 'utf-8' - - def __enter__(self): - self.open_buffer(self.max_buffer_size) - return self - - def __exit__(self, the_type, value, traceback): - self.close_buffer() - - def open_buffer(self, max_buffer_size=50): - '''Open a buffer to send a batch of metrics in one packet - - You can also use this as a context manager. - - >>> with DogStatsd() as batch: - >>> batch.gauge('users.online', 123) - >>> batch.gauge('active.connections', 1001) - - ''' - self.max_buffer_size = max_buffer_size - self.buffer = [] - self._send = self._send_to_buffer - - def close_buffer(self): - '''Flush the buffer and switch back to single metric packets.''' - self._send = self._send_to_server - self._flush_buffer() - - def connect(self, host, port): - """Connect to the monascastatsd server on the given host and port.""" - self._host = host - self._port = int(port) - self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - self.socket.connect((self._host, self._port)) - - def gauge(self, metric, value, dimensions=None, sample_rate=1): - """Record the value of a gauge, optionally setting a list of - - dimensions and a sample rate. - - >>> monascastatsd.gauge('users.online', 123) - >>> monascastatsd.gauge('active.connections', 1001, - >>> dimensions={"protocol": "http"}) - """ - return self._report(metric, 'g', value, dimensions, sample_rate) - - def increment(self, metric, value=1, dimensions=None, sample_rate=1): - """Increment a counter, optionally setting a value, dimensions - - and a sample rate. - - >>> monascastatsd.increment('page.views') - >>> monascastatsd.increment('files.transferred', 124) - """ - self._report(metric, 'c', value, dimensions, sample_rate) - - def decrement(self, metric, value=1, dimensions=None, sample_rate=1): - """Decrement a counter, optionally setting a value, dimensions and a - - sample rate. - - >>> monascastatsd.decrement('files.remaining') - >>> monascastatsd.decrement('active.connections', 2) - """ - self._report(metric, 'c', -value, dimensions, sample_rate) - - def histogram(self, metric, value, dimensions=None, sample_rate=1): - """Sample a histogram value, optionally setting dimensions and a - - sample rate. - - >>> monascastatsd.histogram('uploaded.file.size', 1445) - >>> monascastatsd.histogram('album.photo.count', 26, - >>> dimensions={"gender": "female"}) - """ - self._report(metric, 'h', value, dimensions, sample_rate) - - def timing(self, metric, value, dimensions=None, sample_rate=1): - """Record a timing, optionally setting dimensions and a sample rate. - - >>> monascastatsd.timing("query.response.time", 1234) - """ - self._report(metric, 'ms', value, dimensions, sample_rate) - - def timed(self, metric, dimensions=None, sample_rate=1): - """A decorator that will measure the distribution of a function's - - run time. Optionally specify a list of tag or a sample rate. - :: - - @monascastatsd.timed('user.query.time', sample_rate=0.5) - def get_user(user_id): - # Do what you need to ... - pass - - # Is equivalent to ... - start = time.time() - try: - get_user(user_id) - finally: - monascastatsd.timing('user.query.time', time.time() - start) - """ - def wrapper(func): - @functools.wraps(func) - def wrapped(*args, **kwargs): - start = time.time() - result = func(*args, **kwargs) - self.timing(metric, - time.time() - start, - dimensions=dimensions, - sample_rate=sample_rate) - return result - wrapped.__name__ = func.__name__ - wrapped.__doc__ = func.__doc__ - wrapped.__dict__.update(func.__dict__) - return wrapped - return wrapper - - def set(self, metric, value, dimensions=None, sample_rate=1): - """Sample a set value. - - >>> monascastatsd.set('visitors.uniques', 999) - """ - - self._report(metric, 's', value, dimensions, sample_rate) - - def _report(self, metric, metric_type, value, dimensions, sample_rate): - if sample_rate != 1 and random.random() > sample_rate: - return - - payload = [metric, ":", value, "|", metric_type] - if sample_rate != 1: - payload.extend(["|@", sample_rate]) - if dimensions: - payload.extend(["|#"]) - payload.append(dimensions) - - encoded = "".join(itertools.imap(str, payload)) - self._send(encoded) - - def _send_to_server(self, packet): - try: - self.socket.send(packet.encode(self.encoding)) - except socket.error: - log.exception("Error submitting metric") - - def _send_to_buffer(self, packet): - self.buffer.append(packet) - if len(self.buffer) >= self.max_buffer_size: - self._flush_buffer() - - def _flush_buffer(self): - self._send_to_server("\n".join(self.buffer)) - self.buffer = [] - -monascastatsd = MonascaStatsd() diff --git a/monascastatsd/set.py b/monascastatsd/set.py new file mode 100644 index 0000000..70c0801 --- /dev/null +++ b/monascastatsd/set.py @@ -0,0 +1,21 @@ +from metricbase import MetricBase + + +class Set(MetricBase): + + def __init__(self, connection, name=None, dimensions=None): + super(self.__class__, self).__init__(name=name, + connection=connection, + dimensions=dimensions) + + def send(self, name, value, dimensions=None, sample_rate=1): + """Sample a set value. + + >>> monascastatsd.set('visitors.uniques', 999) + """ + + self._connection.report(metric=self.update_name(name), + metric_type='s', + value=value, + dimensions=self.update_dimensions(dimensions), + sample_rate=sample_rate) diff --git a/monascastatsd/timer.py b/monascastatsd/timer.py new file mode 100644 index 0000000..62aa50e --- /dev/null +++ b/monascastatsd/timer.py @@ -0,0 +1,74 @@ +import contextlib +import functools +import time + +from metricbase import MetricBase + + +class Timer(MetricBase): + + def __init__(self, connection, name=None, dimensions=None): + super(self.__class__, self).__init__(name=name, + connection=connection, + dimensions=dimensions) + + def timing(self, name, value, dimensions=None, sample_rate=1): + """Record a timing, optionally setting dimensions and a sample rate. + + >>> monascastatsd.timing("query.response.time", 1234) + """ + self._connection.report(metric=self.update_name(name), + metric_type='ms', + value=value, + dimensions=self.update_dimensions(dimensions), + sample_rate=sample_rate) + + def timed(self, name, dimensions=None, sample_rate=1): + """A decorator that will measure the distribution of a function's + + run time. Optionally specify a list of tag or a sample rate. + :: + + @monascastatsd.timed('user.query.time', sample_rate=0.5) + def get_user(user_id): + # Do what you need to ... + pass + + # Is equivalent to ... + start = time.time() + try: + get_user(user_id) + finally: + monascastatsd.timing('user.query.time', time.time() - start) + """ + def wrapper(func): + @functools.wraps(func) + def wrapped(*args, **kwargs): + start = time.time() + result = func(*args, **kwargs) + self.timing(name, + time.time() - start, + dimensions=dimensions, + sample_rate=sample_rate) + return result + wrapped.__name__ = func.__name__ + wrapped.__doc__ = func.__doc__ + wrapped.__dict__.update(func.__dict__) + return wrapped + return wrapper + + @contextlib.contextmanager + def time(self, name, dimensions=None, sample_rate=1): + """Time a block of code, optionally setting dimensions and a sample rate. + + try: + with monascastatsd.time("query.response.time"): + Do something... + except Exception: + Log something... + """ + + start_time = time.time() + yield + end_time = time.time() + self.timing(name, end_time - start_time, dimensions, sample_rate) diff --git a/statsd-generator/generator.py b/statsd-generator/generator.py index 0f69e60..7c47412 100755 --- a/statsd-generator/generator.py +++ b/statsd-generator/generator.py @@ -18,7 +18,7 @@ import ConfigParser import random import time -import monascastatsd.monasca_statsd +import monascastatsd as mstatsd import statsd @@ -43,30 +43,42 @@ class MonascaStatsdGenerator(object): def send_messages(self): '''Main processing for sending messages.''' try: - mstatsd = monascastatsd.monasca_statsd.MonascaStatsd() - mstatsd.connect(self.host, self.port) + conn = mstatsd.Connection(host=self.host, port=self.port) + self.client = mstatsd.Client(name='statsd-generator', connection=conn) for index in range(1, self.num_of_iterations + 1): print("Starting iteration " + str(index) + " of " + str(self.num_of_iterations)) - mstatsd.increment('Teraflops', 5) - mstatsd.gauge('NumOfTeraflops', - random.uniform(1.0, 10.0), - dimensions={'Origin': 'Dev', - 'Environment': 'Test'}) - mstatsd.histogram('file.upload.size', - random.randrange(1, 100), - dimensions={'Version': '1.0'}) + counter = self.client.get_counter('teraflops') + counter.increment(5) + gauge = self.client.get_gauge() + gauge.send('num_of_teraflops', + random.uniform(1.0, 10.0), + dimensions={'origin': 'dev', + 'environment': 'test'}) + histogram = self.client.get_histogram('hist') + histogram.send('file.upload.size', + random.randrange(1, 100), + dimensions={'version': '1.0'}) + set = self.client.get_set('hist') + set.send('load_time', + random.randrange(1, 100), + dimensions={'page_name': 'mypage.html'}) - @mstatsd.timed('config_db_time', - dimensions={'db_name': 'mydb'}) + timer = self.client.get_timer('timer') + + @timer.timed('config_db_time', + dimensions={'db_name': 'mydb'}) def time_db(): - time.sleep(0.5) + time.sleep(0.2) time_db() + with timer.time('time_block'): + time.sleep(0.3) + # Send some regular statsd messages - counter = statsd.Counter('statsd_generator') + counter = statsd.Counter('statsd_counter') counter += 1 - gauge = statsd.Gauge('statsd_generator') + gauge = statsd.Gauge('statsd_gauge') gauge.send('cpu_percent', random.uniform(1.0, 100.0)) print("Completed iteration " + str(index) + diff --git a/tests/test_monascastatsd.py b/tests/test_monascastatsd.py index 1c9e20d..3d4ca26 100644 --- a/tests/test_monascastatsd.py +++ b/tests/test_monascastatsd.py @@ -21,7 +21,7 @@ import socket import time import unittest -import monascastatsd.monasca_statsd +import monascastatsd as mstatsd class FakeSocket(object): @@ -50,105 +50,147 @@ class BrokenSocket(FakeSocket): raise socket.error("Socket error") -class TestMonStatsd(unittest.TestCase): +class TestMonascaStatsd(unittest.TestCase): def setUp(self): - self.monascastatsd = monascastatsd.monasca_statsd.MonascaStatsd() - self.monascastatsd.socket = FakeSocket() + conn = mstatsd.Connection() + conn.socket = FakeSocket() + self.client = mstatsd.Client(connection=conn, dimensions={'env': 'test'}) - def recv(self): - return self.monascastatsd.socket.recv() - - def test_set(self): - self.monascastatsd.set('set', 123) - assert self.recv() == 'set:123|s' - - def test_gauge(self): - self.monascastatsd.gauge('gauge', 123.4) - assert self.recv() == 'gauge:123.4|g' + def recv(self, metric_obj): + return metric_obj._connection.socket.recv() def test_counter(self): - self.monascastatsd.increment('page.views') - self.assertEqual('page.views:1|c', self.recv()) + counter = self.client.get_counter(name='page.views') - self.monascastatsd.increment('page.views', 11) - self.assertEqual('page.views:11|c', self.recv()) + counter.increment() + self.assertEqual("page.views:1|c|#{'env': 'test'}", + self.recv(counter)) - self.monascastatsd.decrement('page.views') - self.assertEqual('page.views:-1|c', self.recv()) + counter += 1 + self.assertEqual("page.views:1|c|#{'env': 'test'}", + self.recv(counter)) - self.monascastatsd.decrement('page.views', 12) - self.assertEqual('page.views:-12|c', self.recv()) + counter.increment(11) + self.assertEqual("page.views:11|c|#{'env': 'test'}", + self.recv(counter)) - def test_histogram(self): - self.monascastatsd.histogram('histo', 123.4) - self.assertEqual('histo:123.4|h', self.recv()) + counter += 11 + self.assertEqual("page.views:11|c|#{'env': 'test'}", + self.recv(counter)) - def test_gauge_with_dimensions(self): - self.monascastatsd.gauge('gt', 123.4, - dimensions={'country': 'china', - 'age': 45, - 'color': 'blue'}) - self.assertEqual("gt:123.4|g|#{" + - "'color': 'blue', " + - "'country': 'china', " + - "'age': 45}", - self.recv()) + counter.decrement() + self.assertEqual("page.views:-1|c|#{'env': 'test'}", + self.recv(counter)) + + counter -= 1 + self.assertEqual("page.views:-1|c|#{'env': 'test'}", + self.recv(counter)) + + counter.decrement(12) + self.assertEqual("page.views:-12|c|#{'env': 'test'}", + self.recv(counter)) + + counter -= 12 + self.assertEqual("page.views:-12|c|#{'env': 'test'}", + self.recv(counter)) def test_counter_with_dimensions(self): - self.monascastatsd.increment('ct', - dimensions={'country': 'canada', - 'color': 'red'}) - self.assertEqual("ct:1|c|#{'color': 'red', 'country': 'canada'}", - self.recv()) + counter = self.client.get_counter('counter_with_dims', + dimensions={'date': '10/24', 'time': '23:00'}) + + counter.increment(dimensions={'country': 'canada', 'color': 'red'}) + self.assertEqual("counter_with_dims:1|c|#{'date': '10/24', 'color': 'red', " + + "'country': 'canada', 'env': 'test', 'time': '23:00'}", + self.recv(counter)) + + counter += 1 + self.assertEqual("counter_with_dims:1|c|#{'date': '10/24', 'env': 'test', 'time': '23:00'}", + self.recv(counter)) + + def test_set(self): + set = self.client.get_set('set') + set.send('metric', 123) + assert self.recv(set) == "set.metric:123|s|#{'env': 'test'}" + + def test_gauge(self): + gauge = self.client.get_gauge('gauge') + gauge.send('metric', 123.4) + assert self.recv(gauge) == "gauge.metric:123.4|g|#{'env': 'test'}" + + def test_histogram(self): + histogram = self.client.get_histogram('histogram') + + histogram.send('metric', 123.4) + self.assertEqual("histogram.metric:123.4|h|#{'env': 'test'}", self.recv(histogram)) + + def test_gauge_with_dimensions(self): + gauge = self.client.get_gauge('gauge') + gauge.send('gt', 123.4, + dimensions={'country': 'china', + 'age': 45, + 'color': 'blue'}) + self.assertEqual("gauge.gt:123.4|g|#{" + + "'color': 'blue', " + + "'country': 'china', " + + "'age': 45, " + + "'env': 'test'}", + self.recv(gauge)) def test_histogram_with_dimensions(self): - self.monascastatsd.histogram('h', 1, dimensions={'color': 'red'}) - self.assertEqual("h:1|h|#{'color': 'red'}", self.recv()) + histogram = self.client.get_histogram('my_hist') + histogram.send('h', 1, dimensions={'color': 'red'}) + self.assertEqual("my_hist.h:1|h|#{'color': 'red', 'env': 'test'}", self.recv(histogram)) def test_sample_rate(self): - self.monascastatsd.increment('c', sample_rate=0) - assert not self.recv() + counter = self.client.get_counter('sampled_counter') + counter.increment(sample_rate=0) + assert not self.recv(counter) for _ in range(10000): - self.monascastatsd.increment('sampled_counter', sample_rate=0.3) + counter.increment(sample_rate=0.3) self.assert_almost_equal(3000, - len(self.monascastatsd.socket.payloads), + len(self.client.connection.socket.payloads), 150) - self.assertEqual('sampled_counter:1|c|@0.3', self.recv()) + self.assertEqual("sampled_counter:1|c|@0.3|#{'env': 'test'}", self.recv(counter)) def test_samples_with_dimensions(self): + gauge = self.client.get_gauge() for _ in range(100): - self.monascastatsd.gauge('gst', - 23, - dimensions={'status': 'sampled'}, - sample_rate=0.9) + gauge.send('gst', + 23, + dimensions={'status': 'sampled'}, + sample_rate=0.9) def test_samples_with_dimensions(self): for _ in range(100): - self.monascastatsd.gauge('gst', - 23, - dimensions={'status': 'sampled'}, - sample_rate=0.9) + gauge.send('gst', + 23, + dimensions={'status': 'sampled'}, + sample_rate=0.9) self.assertEqual('gst:23|g|@0.9|#status:sampled') def test_timing(self): - self.monascastatsd.timing('t', 123) - self.assertEqual('t:123|ms', self.recv()) + timer = self.client.get_timer() + timer.timing('t', 123) + self.assertEqual("t:123|ms|#{'env': 'test'}", self.recv(timer)) - @staticmethod - def assert_almost_equal(a, b, delta): - assert 0 <= abs(a - b) <= delta, "%s - %s not within %s" % (a, - b, - delta) + def test_time(self): + timer = self.client.get_timer() + with timer.time('t'): + time.sleep(2) + packet = self.recv(timer) + name_value, type_, dimensions = packet.split('|') + name, value = name_value.split(':') - def test_socket_error(self): - self.monascastatsd.socket = BrokenSocket() - self.monascastatsd.gauge('no error', 1) - assert True, 'success' + self.assertEqual('ms', type_) + self.assertEqual('t', name) + self.assert_almost_equal(2.0, float(value), 0.1) + self.assertEqual("{'env': 'test'}", dimensions.lstrip('#')) def test_timed(self): + timer = self.client.get_timer() - @self.monascastatsd.timed('timed.test') + @timer.timed('timed.test') def func(a, b, c=1, d=1): """docstring.""" time.sleep(0.5) @@ -161,42 +203,61 @@ class TestMonStatsd(unittest.TestCase): # Assert it handles args and kwargs correctly. self.assertEqual(result, (1, 2, 1, 3)) - packet = self.recv() - name_value, type_ = packet.split('|') + packet = self.recv(timer) + name_value, type_, dimensions = packet.split('|') name, value = name_value.split(':') self.assertEqual('ms', type_) self.assertEqual('timed.test', name) self.assert_almost_equal(0.5, float(value), 0.1) + self.assertEqual("{'env': 'test'}", dimensions.lstrip('#')) + + def test_socket_error(self): + self.client.connection.socket = BrokenSocket() + self.client.get_gauge().send('no error', 1) + assert True, 'success' + self.client.connection.socket = FakeSocket() def test_batched(self): - self.monascastatsd.open_buffer() - self.monascastatsd.gauge('page.views', 123) - self.monascastatsd.timing('timer', 123) - self.monascastatsd.close_buffer() + self.client.connection.open_buffer() + gauge = self.client.get_gauge('site') + gauge.send('views', 123) + timer = self.client.get_timer('site') + timer.timing('timer', 123) + self.client.connection.close_buffer() - self.assertEqual('page.views:123|g\ntimer:123|ms', self.recv()) + self.assertEqual("site.views:123|g|#{'env': 'test'}\nsite.timer:123|ms|#{'env': 'test'}", + self.recv(gauge)) def test_context_manager(self): fake_socket = FakeSocket() - with monascastatsd.monasca_statsd.MonascaStatsd() as statsd: - statsd.socket = fake_socket - statsd.gauge('page.views', 123) - statsd.timing('timer', 123) + with mstatsd.Connection() as conn: + conn.socket = fake_socket + client = mstatsd.Client(name='ContextTester', connection=conn) + client.get_gauge('page').send('views', 123) + client.get_timer('page').timing('timer', 12) - self.assertEqual('page.views:123|g\ntimer:123|ms', fake_socket.recv()) + self.assertEqual('ContextTester.page.views:123|g\nContextTester.page.timer:12|ms', + fake_socket.recv()) def test_batched_buffer_autoflush(self): fake_socket = FakeSocket() - with monascastatsd.monasca_statsd.MonascaStatsd() as statsd: - statsd.socket = fake_socket + with mstatsd.Connection() as conn: + conn.socket = fake_socket + client = mstatsd.Client(name='BufferedTester', connection=conn) + counter = client.get_counter('mycounter') for _ in range(51): - statsd.increment('mycounter') - self.assertEqual('\n'.join(['mycounter:1|c' for _ in range(50)]), + counter.increment() + self.assertEqual('\n'.join(['BufferedTester.mycounter:1|c' for _ in range(50)]), fake_socket.recv()) - self.assertEqual('mycounter:1|c', fake_socket.recv()) + self.assertEqual('BufferedTester.mycounter:1|c', fake_socket.recv()) + @staticmethod + def assert_almost_equal(a, b, delta): + assert 0 <= abs(a - b) <= delta, "%s - %s not within %s" % (a, + b, + delta) if __name__ == '__main__': unittest.main() diff --git a/tox.ini b/tox.ini index 92a84af..8635a57 100644 --- a/tox.ini +++ b/tox.ini @@ -22,3 +22,7 @@ commands = {posargs} [flake8] show-source = True exclude=.venv,.git,.tox,dist,*egg,build +max-line-length = 120 +# F401 module imported but unused +# H302 import only modules +ignore = F401, H302,