Skip to content
This repository has been archived by the owner on May 19, 2021. It is now read-only.

Commit

Permalink
Merge branch 'master' into float_num_instability
Browse files Browse the repository at this point in the history
  • Loading branch information
ManuelBahr committed Jul 20, 2017
2 parents 07ce7ed + 325416f commit 6edf756
Show file tree
Hide file tree
Showing 6 changed files with 89 additions and 72 deletions.
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Change Log
All notable changes to this project are noted in this file. This project adheres to [Semantic
Versioning](http://semver.org/).

0.3.2
0.4.1
-----

- Fixed issue (https://github.com/blue-yonder/azure-cost-mon/issues/12)
Expand All @@ -13,6 +13,14 @@ Versioning](http://semver.org/).
resets within Prometheus than necessary, so that `increase` gave wrong
results!


0.4.0
-----

- Use the `X-Prometheus-Scrape-Timeout-Seconds` header sent by
prometheus to overwrite the internal request timeout default.


0.3.1
-----

Expand Down
16 changes: 0 additions & 16 deletions azure_costs_exporter/main.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,15 @@
from os import getcwd
from flask import Flask
from prometheus_client import REGISTRY
from azure_costs_exporter.prometheus_collector import AzureEABillingCollector


def create_app():
from azure_costs_exporter.views import bp

app = Flask(__name__)
app.config.from_pyfile(getcwd()+"/application.cfg")
app.register_blueprint(bp)

collector = AzureEABillingCollector(
app.config['PROMETHEUS_METRIC_NAME'],
app.config['ENROLLMENT_NUMBER'],
app.config['BILLING_API_ACCESS_KEY']
)
try:
REGISTRY.register(collector)
except ValueError:
#don't register multiple times
pass

return app



if __name__ == "__main__":
app = create_app()
app.run(debug=True, host='0.0.0.0')
10 changes: 6 additions & 4 deletions azure_costs_exporter/prometheus_collector.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import requests
import datetime
from pandas import DataFrame
from prometheus_client.core import CounterMetricFamily, Metric
from prometheus_client.core import CounterMetricFamily

base_columns = ['DepartmentName', 'AccountName', 'SubscriptionName', 'MeterCategory',
'MeterSubCategory', 'MeterName', 'ResourceGroup']
Expand Down Expand Up @@ -38,17 +38,19 @@ class AzureEABillingCollector(object):
in Prometheus compatible format.
"""

def __init__(self, metric_name, enrollment, token):
def __init__(self, metric_name, enrollment, token, timeout):
"""
Constructor.
:param metric_name: Name of the timeseries
:param enrollment: ID of the enterprise agreement (EA)
:param token: Access Key generated via the EA portal
:param timeout: Timeout to use for the request against the EA portal
"""
self._metric_name = metric_name
self._enrollment = enrollment
self._token = token
self._timeout = timeout

def _get_azure_data(self, month=None):
"""
Expand All @@ -65,7 +67,7 @@ def _get_azure_data(self, month=None):
url = "https://ea.azure.com/rest/{0}/usage-report?month={1}&type=detail&fmt=Json".format(self._enrollment,
month)

rsp = requests.get(url, headers=headers, timeout=10)
rsp = requests.get(url, headers=headers, timeout=self._timeout)
rsp.raise_for_status()

if rsp.text.startswith('"Usage Data Extract"'):
Expand Down Expand Up @@ -106,4 +108,4 @@ def collect(self):
for name, value in groups.iterrows():
c.add_metric(name, int(round(value.ExtendedCost)))

yield c
yield c
27 changes: 24 additions & 3 deletions azure_costs_exporter/views.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
from flask import Blueprint, Response, abort
from prometheus_client import generate_latest, CONTENT_TYPE_LATEST
from flask import Blueprint, Response, abort, current_app, request
from prometheus_client import CONTENT_TYPE_LATEST, CollectorRegistry, generate_latest

from .prometheus_collector import AzureEABillingCollector


bp = Blueprint('views', __name__)
DEFAULT_SCRAPE_TIMEOUT = 10


def _get_timeout():
try:
return float(request.headers.get('X-Prometheus-Scrape-Timeout-Seconds'))
except Exception:
return DEFAULT_SCRAPE_TIMEOUT


@bp.route("/health")
def health():
Expand All @@ -10,8 +22,17 @@ def health():

@bp.route("/metrics")
def metrics():
timeout = _get_timeout()
collector = AzureEABillingCollector(
current_app.config['PROMETHEUS_METRIC_NAME'],
current_app.config['ENROLLMENT_NUMBER'],
current_app.config['BILLING_API_ACCESS_KEY'],
timeout
)
registry = CollectorRegistry()
registry.register(collector)
try:
content = generate_latest()
content = generate_latest(registry)
return content, 200, {'Content-Type': CONTENT_TYPE_LATEST}
except Exception as e:
abort(Response("Scrape failed: {}".format(e), status=502))
56 changes: 29 additions & 27 deletions tests/test_app.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import datetime

import pytest
import responses
import datetime

from azure_costs_exporter.main import create_app
from azure_costs_exporter.views import DEFAULT_SCRAPE_TIMEOUT
from .data import sample_data, api_output_for_empty_months


Expand Down Expand Up @@ -46,19 +48,30 @@ def test_token(client, now, enrollment, access_key):
assert responses.calls[-1].request.headers['Authorization'] == "Bearer {}".format(access_key)


@responses.activate
def test_metrics(app, now, enrollment):

responses.add(
method='GET',
url="https://ea.azure.com/rest/{0}/usage-report?month={1}&type=detail&fmt=Json".format(enrollment, now),
match_querystring=True,
json=sample_data
)
@pytest.mark.parametrize('timeout,expected', [('42.3', 42.3), (None, DEFAULT_SCRAPE_TIMEOUT)])
def test_metrics(app, access_key, now, enrollment, timeout, expected):
class RequestsMock(responses.RequestsMock):
def get(self, *args, **kwargs):
assert kwargs['timeout'] == expected
return super(RequestsMock, self).get(*args, **kwargs)

rsp = app.test_client().get('/metrics')
assert rsp.status_code == 200
assert rsp.data.count(b'azure_costs_eur') == 4
with RequestsMock() as resp:
resp.add(
method='GET',
url="https://ea.azure.com/rest/{0}/usage-report?month={1}&type=detail&fmt=Json".format(enrollment, now),
match_querystring=True,
json=sample_data
)

headers = {}
if timeout is not None:
headers = {'X-Prometheus-Scrape-Timeout-Seconds': timeout}

rsp = app.test_client().get('/metrics', headers=headers)
url = 'https://ea.azure.com/rest/{enrollment}/usage-report?month={month}&type=detail&fmt=Json'
url = url.format(enrollment=enrollment, month=now)
assert rsp.status_code == 200
assert rsp.data.count(b'azure_costs_eur') == 4


@responses.activate
Expand All @@ -80,24 +93,13 @@ def test_metrics_no_usage(app, now, enrollment):


@responses.activate
def test_failing_target(client, now):
responses.add(
method='GET',
url="https://ea.azure.com/rest/{0}/usage-report?month={1}&type=detail&fmt=Json".format(enrollment, now),
match_querystring=True,
status=500
)

rsp = client.get('/metrics')

assert rsp.status_code == 502
assert rsp.data.startswith(b'Scrape failed')

@pytest.mark.parametrize('status', [500, 400])
def test_failing_target(client, now, status):
responses.add(
method='GET',
url="https://ea.azure.com/rest/{0}/usage-report?month={1}&type=detail&fmt=Json".format(enrollment, now),
match_querystring=True,
status=400
status=status
)

rsp = client.get('/metrics')
Expand Down
42 changes: 21 additions & 21 deletions tests/test_collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,18 @@ def current_month():
return now.strftime("%Y-%m")


@pytest.fixture()
def enrollment():
return '1234567'


@pytest.fixture()
def api_url():
base_url = "https://ea.azure.com/rest/{}/usage-report".format(enrollment())
params = "?month={}&type=detail&fmt=Json".format(current_month())
return base_url + params


def test_df_conversion():
df = convert_json_df(sample_data)

Expand All @@ -29,21 +41,17 @@ def test_df_missing_column():


@responses.activate
def test_extract_metrics():

enrollment = '123'
base_url = "https://ea.azure.com/rest/{}/usage-report".format(enrollment)
params = "?month={}&type=detail&fmt=Json".format(current_month())
def test_extract_metrics(api_url, enrollment):

responses.add(
method='GET',
url=base_url+params,
url=api_url,
match_querystring=True,
json=sample_data
)

registry = CollectorRegistry()
c = AzureEABillingCollector('costs', enrollment, 'ab123xy')
c = AzureEABillingCollector('costs', enrollment, 'ab123xy', 10)
registry.register(c)

result = generate_latest(registry).decode('utf8').split('\n')
Expand All @@ -57,17 +65,13 @@ def test_extract_metrics():


@responses.activate
def test_get_azure_data():

enrollment = '123'
base_url = "https://ea.azure.com/rest/{}/usage-report".format(enrollment)
params = "?month={}&type=detail&fmt=Json".format(current_month())
def test_get_azure_data(api_url, enrollment):

c = AzureEABillingCollector('cloud_costs', enrollment, 'abc123xyz')
c = AzureEABillingCollector('cloud_costs', enrollment, 'abc123xyz', 42.3)

responses.add(
method='GET',
url=base_url+params,
url=api_url,
match_querystring=True,
json=sample_data
)
Expand All @@ -77,19 +81,15 @@ def test_get_azure_data():


@responses.activate
def test_empty_month():
def test_empty_month(api_url, enrollment):
"""
If no usage details have are available for a given month the API does not return a JSON document.
"""
enrollment = '123'
base_url = "https://ea.azure.com/rest/{}/usage-report".format(enrollment)
params = "?month={}&type=detail&fmt=Json".format(current_month())

c = AzureEABillingCollector('cloud_costs', enrollment, 'abc123xyz')
c = AzureEABillingCollector('cloud_costs', enrollment, 'abc123xyz', 11)

responses.add(
method='GET',
url=base_url+params,
url=api_url,
match_querystring=True,
body=api_output_for_empty_months
)
Expand Down

0 comments on commit 6edf756

Please # to comment.