Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
JosephMeghanath committed Dec 18, 2018
2 parents efe868f + 332b0fe commit def0ba3
Show file tree
Hide file tree
Showing 6 changed files with 324 additions and 62 deletions.
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
## Installation

```
pip install apptuit
pip install apptuit --upgrade
```

## Dependencies
Expand All @@ -19,10 +19,30 @@ Supported Python versions: 2.7.x, 3.4, 3.5, 3.6, 3.7
## Usage

### Contents
- [Configuration](#configuration)
- [Sending Data](#sending-data)
* [Sending Data using ApptuitReporter](#sending-the-data-using-apptuitreporter)
* [Sending Data using `send()` API](#sending-data-using-send-api)
- [Querying for Data](#querying-for-data)

### Configuration

Apptuit supports two environmental variables `APPTUIT_PY_TOKEN` and `APPTUIT_PY_TAGS`:

* `APPTUIT_PY_TOKEN`: This can be used to set the Apptuit API token. If set, then we don't
need to pass the token as a parameter when working with the apptuit client or the apptuit reporter.
* `APPTUIT_PY_TAGS`: This can be used to set the global tags for apptuit. The apptuit client and reporter
will add these tags with each datapoint they are sending to Apptuit. These tags will work in combination
with any tags set with the reporter as well as set with individual metrics and datapoints. If any metric
shares a tag key in common with the global tags, the value of the tag from the metric takes preference.
The format of the value of this variable is as follows:
```sh
export APPTUIT_PY_TAGS="tag_key1: tag_val1, tag_key2: tag_val2, tag_key3: tag_val3"
```
The spaces after the comma and colon are optional.

**Note**: Support for these variable has been added in the development version of apptuit-py and is not available
in any of the released versions.

### Sending data

Expand Down
2 changes: 1 addition & 1 deletion apptuit/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from .apptuit_client import Apptuit, DataPoint, ApptuitException
from apptuit import pyformance, timeseries

__version__ = '0.3.0'
__version__ = '0.3.1'

__all__ = ['Apptuit', 'DataPoint', 'ApptuitException', 'pyformance', 'timeseries']
117 changes: 78 additions & 39 deletions apptuit/apptuit_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Client module for Apptuit APIs
"""

import os
from collections import defaultdict
import json
from string import ascii_letters, digits
Expand All @@ -15,25 +16,14 @@
import requests
import pandas as pd

APPTUIT_PY_TOKEN = "APPTUIT_PY_TOKEN"
APPTUIT_PY_TAGS = "APPTUIT_PY_TAGS"
VALID_CHARSET = set(ascii_letters + digits + "-_./")
INVALID_CHARSET = frozenset(map(chr, range(128))) - VALID_CHARSET

def _contains_valid_chars(string):
return INVALID_CHARSET.isdisjoint(string)


def _create_payload(datapoints):
data = []
for dp in datapoints:
row = {}
row["metric"] = dp.metric
row["timestamp"] = dp.timestamp
row["value"] = dp.value
row["tags"] = dp.tags
data.append(row)
return data


def _generate_query_string(query_string, start, end):
ret = "?start=" + str(start)
if end:
Expand Down Expand Up @@ -69,27 +59,89 @@ def _parse_response(resp, start, end=None):
qresult[output_id].series.append(series)
return qresult

def _get_tags_from_environment():
tags_str = os.environ.get(APPTUIT_PY_TAGS)
if not tags_str:
return {}
tags = {}
tags_str = tags_str.strip(", ")
tags_split = tags_str.split(',')
for tag in tags_split:
tag = tag.strip()
if not tag:
continue
try:
key, val = tag.split(":")
tags[key.strip()] = val.strip()
except ValueError:
raise ValueError("Invalid format for "
+ APPTUIT_PY_TAGS +
", failed to parse tag key-value pair '"
+ tag + "', " + APPTUIT_PY_TAGS + " should be in the format - "
"'tag_key1:tag_val1,tag_key2:tag_val2,...,tag_keyN:tag_valN'")
_validate_tags(tags)
return tags

def _validate_tags(tags):
for tagk, tagv in tags.items():
if not _contains_valid_chars(tagk):
raise ValueError("Tag key %s contains an invalid character, "
"allowed characters are a-z, A-Z, 0-9, -, _, ., and /" % tagk)
if not _contains_valid_chars(str(tagv)):
raise ValueError("Tag value %s contains an invalid character, "
"allowed characters are a-z, A-Z, 0-9, -, _, ., and /" % tagv)

class Apptuit(object):
"""
Apptuit is the client object, encapsulating the functionalities provided by Apptuit APIs
"""

def __init__(self, token, api_endpoint="https://api.apptuit.ai/", debug=False):
def __init__(self, token=None, api_endpoint="https://api.apptuit.ai", debug=False):
"""
Creates an apptuit client object
Params:
token: Token of the tenant to which we wish to connect
api_endpoint: Apptuit API End point (including the protocol and port)
port: Port on which the service is running
"""
if not token:
raise ValueError("Invalid token")
self.token = token
if not self.token:
self.token = os.environ.get(APPTUIT_PY_TOKEN)
if not self.token:
raise ValueError("Missing Apptuit API token, "
"either pass it as a parameter or "
"set as value of the environment variable '"
+ APPTUIT_PY_TOKEN + "'.")
self.endpoint = api_endpoint
if self.endpoint[-1] == '/':
self.endpoint = self.endpoint[:-1]
self.debug = debug
self._environ_tags = _get_tags_from_environment()

def _create_payload(self, datapoints):
data = []
for dp in datapoints:
if dp.tags and self._environ_tags:
tags = self._environ_tags.copy()
tags.update(dp.tags)
elif dp.tags:
tags = dp.tags
else:
tags = self._environ_tags
if not tags:
raise ValueError("Missing tags for the metric "
+ dp.metric +
". Either pass it as value of the tags"
" parameter to DataPoint or"
" set environment variable '"
+ APPTUIT_PY_TAGS +
"' for global tags")
row = {}
row["metric"] = dp.metric
row["timestamp"] = dp.timestamp
row["value"] = dp.value
row["tags"] = tags
data.append(row)
return data

def send(self, datapoints):
"""
Expand All @@ -98,8 +150,10 @@ def send(self, datapoints):
datapoints: A list of DataPoint objects
It raises an ApptuitException in case the backend API responds with an error
"""
if not datapoints:
return
url = self.endpoint + "/api/put?sync&sync=60000"
data = _create_payload(datapoints)
data = self._create_payload(datapoints)
body = json.dumps(data)
body = zlib.compress(body.encode("utf-8"))
headers = {}
Expand Down Expand Up @@ -203,18 +257,9 @@ def tags(self):
def tags(self, tags):
if not isinstance(tags, dict):
raise ValueError("tags parameter is expected to be a dict type")
for tagk, tagv in tags.items():
if not _contains_valid_chars(tagk):
raise ValueError("tag key %s contains a character which is not allowed, "
"only characters [a-z], [A-Z], [0-9] and [-_./] are allowed"
% (tagk))
if not _contains_valid_chars(str(tagv)):
raise ValueError("tag value %s contains a character which is not allowed, "
"only characters [a-z], [A-Z], [0-9] and [-_./] are allowed"
% (tagv))
_validate_tags(tags)
self._tags = tags


def __repr__(self):
repr_str = '%s{' % self.metric
for tagk in sorted(self.tags):
Expand Down Expand Up @@ -300,8 +345,6 @@ def __init__(self, metric, tags, timestamp, value):
timestamp: Number of seconds since Unix epoch
value: value of the metric at this timestamp (int or float)
"""
if tags is None or tags == {}:
raise ValueError("Ivalid tags: Metric: "+metric+" need minimum one tag.")
self.metric = metric
self.tags = tags
self.timestamp = timestamp
Expand All @@ -324,15 +367,12 @@ def tags(self):

@tags.setter
def tags(self, tags):
self._tags = None
if tags is None:
return
if not isinstance(tags, dict):
raise ValueError("Expected a value of type dict for tags")
for tagk, tagv in tags.items():
if not _contains_valid_chars(tagk):
raise ValueError("Tag key %s contains an invalid character, "
"allowed characters are a-z, A-Z, 0-9, -, _, ., and /" % tagk)
if not _contains_valid_chars(str(tagv)):
raise ValueError("Tag value %s contains an invalid character, "
"allowed characters are a-z, A-Z, 0-9, -, _, ., and /" % tagv)
_validate_tags(tags)
self._tags = tags

@property
Expand All @@ -343,7 +383,6 @@ def value(self):
def value(self, value):
if isinstance(value, (int, float)):
self._value = value

elif isinstance(value, str):
try:
self._value = float(value)
Expand Down
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

setup(
name="apptuit",
packages=['apptuit'],
version="0.3.0",
packages=['apptuit', 'apptuit.pyformance'],
version="0.3.1",
description="Apptuit Python Client",
url="https://github.com/ApptuitAI/apptuit-py",
author="Abhinav Upadhyay",
Expand Down
25 changes: 6 additions & 19 deletions tests/test_send.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,19 @@ def test_send_positive(mock_post):
metric_name = "node.load_avg.1m"
tags = {"host": "localhost", "region": "us-east-1", "service": "web-server"}
dps = []
client.send(dps)
points_sent = 0
while True:
ts = int(time.time())
dps.append(DataPoint(metric_name, tags, ts, random.random()))
if len(dps) == 100:
try:
client.send(dps)
except ApptuitException:
ok_(False)
client.send(dps)
dps = []
points_sent += 100
if points_sent > 500:
break
if dps:
try:
client.send(dps)
except ApptuitException:
ok_(False)
client.send(dps)

@patch('apptuit.apptuit_client.requests.post')
def test_send_server_error(mock_post):
Expand Down Expand Up @@ -83,18 +78,10 @@ def test_invalid_chars_in_tag_keys():
ts = int(time.time())
with assert_raises(ValueError) as ex:
DataPoint(metric_name, tags, ts, random.random())

def test_no_tag():
"""
Test for no tag keys
"""
metric_name = "node.load_avg.1m"
ts = int(time.time())
with assert_raises(ValueError) as ex:
DataPoint(metric_name, None, ts, random.random())
with assert_raises(ValueError) as ex:
DataPoint(metric_name, {}, ts, random.random())

DataPoint(metric_name, "error", ts, random.random())
dp = DataPoint(metric_name, None, ts, random.random())
assert_equals(dp.tags,None)

def test_invalid_chars_in_tag_values():
"""
Expand Down
Loading

0 comments on commit def0ba3

Please sign in to comment.