diff --git a/app/config.py b/app/config.py index c0f280ca..34f4b079 100644 --- a/app/config.py +++ b/app/config.py @@ -535,3 +535,7 @@ DISABLE_RATE_LIMIT = "DISABLE_RATE_LIMIT" in os.environ SUBSCRIPTION_CHANGE_WEBHOOK = os.environ.get("SUBSCRIPTION_CHANGE_WEBHOOK", None) MAX_API_KEYS = int(os.environ.get("MAX_API_KEYS", 30)) + +UPCLOUD_USERNAME = os.environ.get("UPCLOUD_USERNAME", None) +UPCLOUD_PASSWORD = os.environ.get("UPCLOUD_PASSWORD", None) +UPCLOUD_DB_ID = os.environ.get("UPCLOUD_DB_ID", None) diff --git a/monitor/__init__.py b/monitor/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/monitor/metric.py b/monitor/metric.py new file mode 100644 index 00000000..ae5a628c --- /dev/null +++ b/monitor/metric.py @@ -0,0 +1,21 @@ +from dataclasses import dataclass +from typing import List + + +@dataclass +class UpcloudRecord: + db_role: str + label: str + time: str + value: float + + +@dataclass +class UpcloudMetric: + metric_name: str + records: List[UpcloudRecord] + + +@dataclass +class UpcloudMetrics: + metrics: List[UpcloudMetric] diff --git a/monitor/metric_exporter.py b/monitor/metric_exporter.py new file mode 100644 index 00000000..164f25ca --- /dev/null +++ b/monitor/metric_exporter.py @@ -0,0 +1,20 @@ +from app.config import UPCLOUD_DB_ID, UPCLOUD_PASSWORD, UPCLOUD_USERNAME +from app.log import LOG +from monitor.newrelic import NewRelicClient +from monitor.upcloud import UpcloudClient + + +class MetricExporter: + def __init__(self, newrelic_license: str): + self.__upcloud = UpcloudClient( + username=UPCLOUD_USERNAME, password=UPCLOUD_PASSWORD + ) + self.__newrelic = NewRelicClient(newrelic_license) + + def run(self): + try: + metrics = self.__upcloud.get_metrics(UPCLOUD_DB_ID) + self.__newrelic.send(metrics) + LOG.info("Upcloud metrics sent to NewRelic") + except Exception as e: + LOG.warn(f"Could not export metrics: {e}") diff --git a/monitor/newrelic.py b/monitor/newrelic.py new file mode 100644 index 00000000..02507d49 --- /dev/null +++ b/monitor/newrelic.py @@ -0,0 +1,26 @@ +from monitor.metric import UpcloudMetrics + +from newrelic_telemetry_sdk import GaugeMetric, MetricClient + +_NEWRELIC_BASE_HOST = "metric-api.eu.newrelic.com" + + +class NewRelicClient: + def __init__(self, license_key: str): + self.__client = MetricClient(license_key=license_key, host=_NEWRELIC_BASE_HOST) + + def send(self, metrics: UpcloudMetrics): + batch = [] + + for metric in metrics.metrics: + for record in metric.records: + batch.append( + GaugeMetric( + name=f"upcloud.db.{metric.metric_name}", + value=record.value, + tags={"host": record.label, "db_role": record.db_role}, + ) + ) + + response = self.__client.send_batch(batch) + response.raise_for_status() diff --git a/monitor/upcloud.py b/monitor/upcloud.py new file mode 100644 index 00000000..c7f1aecc --- /dev/null +++ b/monitor/upcloud.py @@ -0,0 +1,82 @@ +from app.log import LOG +from monitor.metric import UpcloudMetric, UpcloudMetrics, UpcloudRecord + +import base64 +import requests +from typing import Any + + +BASE_URL = "https://api.upcloud.com" + + +def get_metric(json: Any, metric: str) -> UpcloudMetric: + records = [] + + if metric in json: + metric_data = json[metric] + data = metric_data["data"] + cols = list(map(lambda x: x["label"], data["cols"][1:])) + latest = data["rows"][-1] + time = latest[0] + for column_idx in range(len(cols)): + value = latest[1 + column_idx] + + # If the latest value is None, try to fetch the second to last + if value is None: + value = data["rows"][-2][1 + column_idx] + + if value is not None: + label = cols[column_idx] + if "(master)" in label: + db_role = "master" + else: + db_role = "standby" + records.append( + UpcloudRecord(time=time, db_role=db_role, label=label, value=value) + ) + else: + LOG.warn(f"Could not get value for metric {metric}") + + return UpcloudMetric(metric_name=metric, records=records) + + +def get_metrics(json: Any) -> UpcloudMetrics: + return UpcloudMetrics( + metrics=[ + get_metric(json, "cpu_usage"), + get_metric(json, "disk_usage"), + get_metric(json, "diskio_reads"), + get_metric(json, "diskio_writes"), + get_metric(json, "load_average"), + get_metric(json, "mem_usage"), + get_metric(json, "net_receive"), + get_metric(json, "net_send"), + ] + ) + + +class UpcloudClient: + def __init__(self, username: str, password: str): + if not username: + raise Exception("UpcloudClient username must be set") + if not password: + raise Exception("UpcloudClient password must be set") + + client = requests.Session() + encoded_auth = base64.b64encode( + f"{username}:{password}".encode("utf-8") + ).decode("utf-8") + client.headers = {"Authorization": f"Basic {encoded_auth}"} + self.__client = client + + def get_metrics(self, db_uuid: str) -> UpcloudMetrics: + url = f"{BASE_URL}/1.3/database/{db_uuid}/metrics?period=hour" + LOG.d(f"Performing request to {url}") + response = self.__client.get(url) + LOG.d(f"Status code: {response.status_code}") + if response.status_code != 200: + return UpcloudMetrics(metrics=[]) + + as_json = response.json() + + return get_metrics(as_json) diff --git a/monitoring.py b/monitoring.py index 2d6e280c..29c88f17 100644 --- a/monitoring.py +++ b/monitoring.py @@ -1,3 +1,4 @@ +import configparser import os import subprocess from time import sleep @@ -7,6 +8,7 @@ import newrelic.agent from app.db import Session from app.log import LOG +from monitor.metric_exporter import MetricExporter # the number of consecutive fails # if more than _max_nb_fails, alert @@ -19,6 +21,18 @@ _max_nb_fails = 10 # the maximum number of emails in incoming & active queue _max_incoming = 50 +_NR_CONFIG_FILE_LOCATION_VAR = "NEW_RELIC_CONFIG_FILE" + + +def get_newrelic_license() -> str: + nr_file = os.environ.get(_NR_CONFIG_FILE_LOCATION_VAR, None) + if nr_file is None: + raise Exception(f"{_NR_CONFIG_FILE_LOCATION_VAR} not defined") + + config = configparser.ConfigParser() + config.read(nr_file) + return config["newrelic"]["license_key"] + @newrelic.agent.background_task() def log_postfix_metrics(): @@ -80,10 +94,13 @@ def log_nb_db_connection(): if __name__ == "__main__": + exporter = MetricExporter(get_newrelic_license()) while True: log_postfix_metrics() log_nb_db_connection() Session.close() + exporter.run() + # 1 min sleep(60) diff --git a/poetry.lock b/poetry.lock index ccda6b6c..06dfe774 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,10 +1,9 @@ -# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. [[package]] name = "aiohttp" version = "3.8.4" description = "Async http client/server framework (asyncio)" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -115,7 +114,6 @@ speedups = ["Brotli", "aiodns", "cchardet"] name = "aiosignal" version = "1.2.0" description = "aiosignal: a list of registered asynchronous callbacks" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -130,7 +128,6 @@ frozenlist = ">=1.1.0" name = "aiosmtpd" version = "1.4.2" description = "aiosmtpd - asyncio based SMTP server" -category = "main" optional = false python-versions = "~=3.6" files = [ @@ -147,7 +144,6 @@ typing-extensions = {version = "*", markers = "python_version < \"3.8\""} name = "aiosmtplib" version = "1.1.4" description = "asyncio SMTP client" -category = "main" optional = false python-versions = ">=3.5.2,<4.0.0" files = [ @@ -163,7 +159,6 @@ uvloop = ["uvloop (>=0.13,<0.15)"] name = "aiospamc" version = "0.6.1" description = "An asyncio-based library to communicate with SpamAssassin's SPAMD service." -category = "main" optional = false python-versions = ">=3.5,<4.0" files = [ @@ -178,7 +173,6 @@ certifi = ">=2019.9,<2020.0" name = "alembic" version = "1.4.3" description = "A database migration tool for SQLAlchemy." -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -196,7 +190,6 @@ SQLAlchemy = ">=1.1.0" name = "appnope" version = "0.1.0" description = "Disable App Nap on OS X 10.9" -category = "main" optional = false python-versions = "*" files = [ @@ -208,7 +201,6 @@ files = [ name = "arrow" version = "0.16.0" description = "Better dates & times for Python" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -223,7 +215,6 @@ python-dateutil = ">=2.7.0" name = "astroid" version = "2.11.6" description = "An abstract syntax tree for Python with inference support." -category = "dev" optional = false python-versions = ">=3.6.2" files = [ @@ -242,7 +233,6 @@ wrapt = ">=1.11,<2" name = "async-timeout" version = "4.0.2" description = "Timeout context manager for asyncio programs" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -257,7 +247,6 @@ typing-extensions = {version = ">=3.6.5", markers = "python_version < \"3.8\""} name = "asynctest" version = "0.13.0" description = "Enhance the standard unittest package with features for testing asyncio libraries" -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -269,7 +258,6 @@ files = [ name = "atpublic" version = "2.0" description = "public -- @public for populating __all__" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -283,7 +271,6 @@ typing_extensions = {version = "*", markers = "python_version < \"3.8\""} name = "attrs" version = "20.2.0" description = "Classes Without Boilerplate" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -301,7 +288,6 @@ tests-no-zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (> name = "backcall" version = "0.2.0" description = "Specifications for callback functions passed in to an API" -category = "main" optional = false python-versions = "*" files = [ @@ -313,7 +299,6 @@ files = [ name = "backports.entry-points-selectable" version = "1.1.1" description = "Compatibility shim providing selectable entry points for older implementations" -category = "dev" optional = false python-versions = ">=2.7" files = [ @@ -332,7 +317,6 @@ testing = ["pytest", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pyte name = "bcrypt" version = "3.2.0" description = "Modern password hashing for your software and your servers" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -360,7 +344,6 @@ typecheck = ["mypy"] name = "black" version = "22.1.0" description = "The uncompromising code formatter." -category = "dev" optional = false python-versions = ">=3.6.2" files = [ @@ -408,7 +391,6 @@ uvloop = ["uvloop (>=0.15.2)"] name = "blinker" version = "1.4" description = "Fast, simple object-to-object and broadcast signaling" -category = "main" optional = false python-versions = "*" files = [ @@ -419,7 +401,6 @@ files = [ name = "boto3" version = "1.15.9" description = "The AWS SDK for Python" -category = "main" optional = false python-versions = "*" files = [ @@ -436,7 +417,6 @@ s3transfer = ">=0.3.0,<0.4.0" name = "botocore" version = "1.18.9" description = "Low-level, data-driven core of boto 3." -category = "main" optional = false python-versions = "*" files = [ @@ -453,7 +433,6 @@ urllib3 = {version = ">=1.20,<1.26", markers = "python_version != \"3.4\""} name = "cachetools" version = "4.1.1" description = "Extensible memoizing collections and decorators" -category = "main" optional = false python-versions = "~=3.5" files = [ @@ -465,7 +444,6 @@ files = [ name = "cbor2" version = "5.2.0" description = "Pure Python CBOR (de)serializer with extensive tag support" -category = "main" optional = false python-versions = "*" files = [ @@ -479,7 +457,6 @@ test = ["pytest", "pytest-cov"] name = "certifi" version = "2019.11.28" description = "Python package for providing Mozilla's CA Bundle." -category = "main" optional = false python-versions = "*" files = [ @@ -491,7 +468,6 @@ files = [ name = "cffi" version = "1.14.4" description = "Foreign Function Interface for Python calling C code." -category = "main" optional = false python-versions = "*" files = [ @@ -541,7 +517,6 @@ pycparser = "*" name = "cfgv" version = "3.2.0" description = "Validate configuration and produce human readable error messages." -category = "dev" optional = false python-versions = ">=3.6.1" files = [ @@ -553,7 +528,6 @@ files = [ name = "chardet" version = "3.0.4" description = "Universal encoding detector for Python 2 and 3" -category = "main" optional = false python-versions = "*" files = [ @@ -565,7 +539,6 @@ files = [ name = "charset-normalizer" version = "2.1.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "main" optional = false python-versions = ">=3.6.0" files = [ @@ -580,7 +553,6 @@ unicode-backport = ["unicodedata2"] name = "click" version = "8.0.3" description = "Composable command line interface toolkit" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -596,7 +568,6 @@ importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} name = "coinbase-commerce" version = "1.0.1" description = "Coinbase Commerce API client library" -category = "main" optional = false python-versions = "*" files = [ @@ -612,7 +583,6 @@ six = ">=1.9" name = "colorama" version = "0.4.5" description = "Cross-platform colored terminal text." -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -624,7 +594,6 @@ files = [ name = "coloredlogs" version = "14.0" description = "Colored terminal output for Python's logging module" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -642,7 +611,6 @@ cron = ["capturer (>=2.4)"] name = "coverage" version = "6.4.2" description = "Code coverage measurement for Python" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -699,7 +667,6 @@ toml = ["tomli"] name = "crontab" version = "0.22.8" description = "Parse and use crontab schedules in Python" -category = "main" optional = false python-versions = "*" files = [ @@ -710,7 +677,6 @@ files = [ name = "cryptography" version = "37.0.1" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -753,7 +719,6 @@ test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0 name = "decorator" version = "4.4.2" description = "Decorators for Humans" -category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*" files = [ @@ -765,7 +730,6 @@ files = [ name = "deprecated" version = "1.2.13" description = "Python @deprecated decorator to deprecate old python classes, functions or methods." -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -783,7 +747,6 @@ dev = ["PyTest", "PyTest (<5)", "PyTest-Cov", "PyTest-Cov (<2.6)", "bump2version name = "dill" version = "0.3.5.1" description = "serialize all of python" -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*" files = [ @@ -798,7 +761,6 @@ graph = ["objgraph (>=1.7.2)"] name = "distlib" version = "0.3.1" description = "Distribution utilities" -category = "dev" optional = false python-versions = "*" files = [ @@ -810,7 +772,6 @@ files = [ name = "djlint" version = "1.3.0" description = "HTML Template Linter and Formatter" -category = "dev" optional = false python-versions = ">=3.7,<4.0" files = [ @@ -837,7 +798,6 @@ test = ["coverage (>=6.3.1,<7.0.0)", "pytest (>=7.0.1,<8.0.0)", "pytest-cov (>=3 name = "dkimpy" version = "1.0.5" description = "DKIM (DomainKeys Identified Mail), ARC (Authenticated Receive Chain), and TLSRPT (TLS Report) email signing and verification" -category = "main" optional = false python-versions = "*" files = [ @@ -857,7 +817,6 @@ testing = ["authres", "pynacl"] name = "dnspython" version = "2.0.0" description = "DNS toolkit" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -876,7 +835,6 @@ trio = ["sniffio (>=1.1)", "trio (>=0.14.0)"] name = "email-validator" version = "1.1.3" description = "A robust email syntax and deliverability validation library for Python 2.x/3.x." -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" files = [ @@ -892,7 +850,6 @@ idna = ">=2.0.0" name = "exceptiongroup" version = "1.1.1" description = "Backport of PEP 654 (exception groups)" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -907,7 +864,6 @@ test = ["pytest (>=6)"] name = "facebook-sdk" version = "3.1.0" description = "This client library is designed to support the Facebook Graph API and the official Facebook JavaScript SDK, which is the canonical way to implement Facebook authentication." -category = "main" optional = false python-versions = "*" files = [ @@ -922,7 +878,6 @@ requests = "*" name = "filelock" version = "3.0.12" description = "A platform independent file lock." -category = "main" optional = false python-versions = "*" files = [ @@ -934,7 +889,6 @@ files = [ name = "flanker" version = "0.9.11" description = "Mailgun Parsing Tools" -category = "main" optional = false python-versions = "*" files = [ @@ -961,7 +915,6 @@ validator = ["dnsq (>=1.1.6)", "redis (>=2.7.1)"] name = "flask" version = "1.1.2" description = "A simple framework for building complex web applications." -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -984,7 +937,6 @@ dotenv = ["python-dotenv"] name = "flask-admin" version = "1.5.7" description = "Simple and extensible admin interface framework for Flask" -category = "main" optional = false python-versions = "*" files = [ @@ -1003,7 +955,6 @@ azure = ["azure-storage-blob"] name = "flask-cors" version = "3.0.9" description = "A Flask extension adding a decorator for CORS support" -category = "main" optional = false python-versions = "*" files = [ @@ -1019,7 +970,6 @@ Six = "*" name = "flask-debugtoolbar" version = "0.11.0" description = "A toolbar overlay for debugging Flask applications." -category = "main" optional = false python-versions = "*" files = [ @@ -1037,7 +987,6 @@ werkzeug = "*" name = "flask-debugtoolbar-sqlalchemy" version = "0.2.0" description = "Flask Debug Toolbar panel for SQLAlchemy" -category = "main" optional = false python-versions = "*" files = [ @@ -1055,7 +1004,6 @@ sqlparse = "*" name = "flask-httpauth" version = "4.1.0" description = "Basic and Digest HTTP authentication for Flask routes" -category = "main" optional = false python-versions = "*" files = [ @@ -1070,7 +1018,6 @@ Flask = "*" name = "flask-limiter" version = "1.4" description = "Rate limiting for flask applications" -category = "main" optional = false python-versions = "*" files = [ @@ -1087,7 +1034,6 @@ six = ">=1.4.1" name = "flask-login" version = "0.5.0" description = "User session management for Flask" -category = "main" optional = false python-versions = "*" files = [ @@ -1102,7 +1048,6 @@ Flask = "*" name = "flask-migrate" version = "2.5.3" description = "SQLAlchemy database migrations for Flask applications using Alembic" -category = "main" optional = false python-versions = "*" files = [ @@ -1119,7 +1064,6 @@ Flask-SQLAlchemy = ">=1.0" name = "flask-profiler" version = "1.8.1" description = "API endpoint profiler for Flask framework" -category = "main" optional = false python-versions = "*" files = [ @@ -1136,7 +1080,6 @@ simplejson = "*" name = "flask-sqlalchemy" version = "2.5.1" description = "Adds SQLAlchemy support to your Flask application." -category = "main" optional = false python-versions = ">= 2.7, != 3.0.*, != 3.1.*, != 3.2.*, != 3.3.*" files = [ @@ -1152,7 +1095,6 @@ SQLAlchemy = ">=0.8.0" name = "flask-wtf" version = "0.14.3" description = "Simple integration of Flask and WTForms." -category = "main" optional = false python-versions = "*" files = [ @@ -1169,7 +1111,6 @@ WTForms = "*" name = "frozenlist" version = "1.3.3" description = "A list-like structure which implements collections.abc.MutableSequence" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1253,7 +1194,6 @@ files = [ name = "future" version = "0.18.2" description = "Clean single-source support for Python 3 and 2" -category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -1264,7 +1204,6 @@ files = [ name = "gevent" version = "22.10.2" description = "Coroutine-based network library" -category = "main" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5" files = [ @@ -1340,7 +1279,6 @@ test = ["backports.socketpair", "cffi (>=1.12.2)", "contextvars (==2.4)", "cover name = "google-api-core" version = "1.22.2" description = "Google API client core library" -category = "main" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" files = [ @@ -1366,7 +1304,6 @@ grpcio-gcp = ["grpcio-gcp (>=0.2.2)"] name = "google-api-python-client" version = "1.12.3" description = "Google API Client Library for Python" -category = "main" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" files = [ @@ -1386,7 +1323,6 @@ uritemplate = ">=3.0.0,<4dev" name = "google-auth" version = "1.22.0" description = "Google Authentication Library" -category = "main" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" files = [ @@ -1406,7 +1342,6 @@ six = ">=1.9.0" name = "google-auth-httplib2" version = "0.0.4" description = "Google Authentication Library: httplib2 transport" -category = "main" optional = false python-versions = "*" files = [ @@ -1423,7 +1358,6 @@ six = "*" name = "googleapis-common-protos" version = "1.52.0" description = "Common protobufs used in Google APIs" -category = "main" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" files = [ @@ -1441,7 +1375,6 @@ grpc = ["grpcio (>=1.0.0)"] name = "greenlet" version = "2.0.2" description = "Lightweight in-process concurrent programming" -category = "main" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" files = [ @@ -1515,7 +1448,6 @@ test = ["objgraph", "psutil"] name = "gunicorn" version = "20.0.4" description = "WSGI HTTP Server for UNIX" -category = "main" optional = false python-versions = ">=3.4" files = [ @@ -1536,7 +1468,6 @@ tornado = ["tornado (>=0.2)"] name = "html-tag-names" version = "0.1.2" description = "List of known HTML tag names" -category = "dev" optional = false python-versions = ">=3.7,<4.0" files = [ @@ -1548,7 +1479,6 @@ files = [ name = "html-void-elements" version = "0.1.0" description = "List of HTML void tag names." -category = "dev" optional = false python-versions = ">=3.7,<4.0" files = [ @@ -1560,7 +1490,6 @@ files = [ name = "httplib2" version = "0.18.1" description = "A comprehensive HTTP client library." -category = "main" optional = false python-versions = "*" files = [ @@ -1572,7 +1501,6 @@ files = [ name = "humanfriendly" version = "8.2" description = "Human friendly output for text interfaces using Python" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -1587,7 +1515,6 @@ pyreadline = {version = "*", markers = "sys_platform == \"win32\""} name = "identify" version = "1.5.5" description = "File identification library for Python" -category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" files = [ @@ -1602,7 +1529,6 @@ license = ["editdistance"] name = "idna" version = "2.10" description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1614,7 +1540,6 @@ files = [ name = "importlib-metadata" version = "4.12.0" description = "Read metadata from Python packages" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1635,7 +1560,6 @@ testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs name = "iniconfig" version = "1.0.1" description = "iniconfig: brain-dead simple config-ini parsing" -category = "dev" optional = false python-versions = "*" files = [ @@ -1647,7 +1571,6 @@ files = [ name = "ipython" version = "7.31.1" description = "IPython: Productive Interactive Computing" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1684,7 +1607,6 @@ test = ["ipykernel", "nbformat", "nose (>=0.10.1)", "numpy (>=1.17)", "pygments" name = "ipython-genutils" version = "0.2.0" description = "Vestigial utilities from IPython" -category = "main" optional = false python-versions = "*" files = [ @@ -1696,7 +1618,6 @@ files = [ name = "isort" version = "5.10.1" description = "A Python utility / library to sort Python imports." -category = "dev" optional = false python-versions = ">=3.6.1,<4.0" files = [ @@ -1714,7 +1635,6 @@ requirements-deprecated-finder = ["pip-api", "pipreqs"] name = "itsdangerous" version = "1.1.0" description = "Various helpers to pass data to untrusted environments and back." -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1726,7 +1646,6 @@ files = [ name = "jedi" version = "0.17.2" description = "An autocompletion tool for Python that can be used for text editors." -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -1745,7 +1664,6 @@ testing = ["Django (<3.1)", "colorama", "docopt", "pytest (>=3.9.0,<5.0.0)"] name = "jinja2" version = "2.11.3" description = "A very fast and expressive template engine." -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -1763,7 +1681,6 @@ i18n = ["Babel (>=0.8)"] name = "jmespath" version = "0.10.0" description = "JSON Matching Expressions" -category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -1775,7 +1692,6 @@ files = [ name = "jwcrypto" version = "0.8" description = "Implementation of JOSE Web standards" -category = "main" optional = false python-versions = "*" files = [ @@ -1790,7 +1706,6 @@ cryptography = ">=2.3" name = "lazy-object-proxy" version = "1.7.1" description = "A fast and thorough lazy object proxy." -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1837,7 +1752,6 @@ files = [ name = "limits" version = "1.5.1" description = "Rate limiting utilities" -category = "main" optional = false python-versions = "*" files = [ @@ -1852,7 +1766,6 @@ six = ">=1.4.1" name = "mako" version = "1.1.3" description = "A super-fast templating language that borrows the best ideas from the existing templating languages." -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1871,7 +1784,6 @@ lingua = ["lingua"] name = "markupsafe" version = "1.1.1" description = "Safely add untrusted strings to HTML/XML markup." -category = "main" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" files = [ @@ -1933,7 +1845,6 @@ files = [ name = "matplotlib-inline" version = "0.1.3" description = "Inline Matplotlib backend for Jupyter" -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -1948,7 +1859,6 @@ traitlets = "*" name = "mccabe" version = "0.7.0" description = "McCabe checker, plugin for flake8" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1960,7 +1870,6 @@ files = [ name = "memory-profiler" version = "0.57.0" description = "A module for monitoring memory usage of a python program" -category = "main" optional = false python-versions = "*" files = [ @@ -1974,7 +1883,6 @@ psutil = "*" name = "multidict" version = "4.7.6" description = "multidict implementation" -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -2001,7 +1909,6 @@ files = [ name = "mypy-extensions" version = "0.4.3" description = "Experimental type system extensions for programs checked with the mypy typechecker." -category = "dev" optional = false python-versions = "*" files = [ @@ -2013,7 +1920,6 @@ files = [ name = "newrelic" version = "8.8.0" description = "New Relic Python Agent" -category = "main" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" files = [ @@ -2037,11 +1943,24 @@ files = [ [package.extras] infinite-tracing = ["grpcio", "protobuf"] +[[package]] +name = "newrelic-telemetry-sdk" +version = "0.5.0" +description = "New Relic Telemetry SDK" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "newrelic-telemetry-sdk-0.5.0.tar.gz", hash = "sha256:461ba12a54f3c44e85e89343d99f66228b4d4703cd2808cefc8fc0b83c97e4e7"}, + {file = "newrelic_telemetry_sdk-0.5.0-py2.py3-none-any.whl", hash = "sha256:f7087b2d5a3d0e686532ae3dceaffcfb8e1bb4d372d598071438631c0c8af37b"}, +] + +[package.dependencies] +urllib3 = ">=1.7,<2" + [[package]] name = "nodeenv" version = "1.5.0" description = "Node.js virtual environment builder" -category = "dev" optional = false python-versions = "*" files = [ @@ -2053,7 +1972,6 @@ files = [ name = "oauthlib" version = "3.1.0" description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -2070,7 +1988,6 @@ signedtoken = ["cryptography", "pyjwt (>=1.0.0)"] name = "packaging" version = "20.4" description = "Core utilities for Python packages" -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -2086,7 +2003,6 @@ six = "*" name = "parso" version = "0.7.1" description = "A Python Parser" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -2101,7 +2017,6 @@ testing = ["docopt", "pytest (>=3.0.7)"] name = "pathspec" version = "0.9.0" description = "Utility library for gitignore style pattern matching of file paths." -category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" files = [ @@ -2113,7 +2028,6 @@ files = [ name = "pexpect" version = "4.8.0" description = "Pexpect allows easy control of interactive console applications." -category = "main" optional = false python-versions = "*" files = [ @@ -2128,7 +2042,6 @@ ptyprocess = ">=0.5" name = "pgpy" version = "0.5.4" description = "Pretty Good Privacy for Python" -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" files = [ @@ -2145,7 +2058,6 @@ six = ">=1.9.0" name = "phpserialize" version = "1.3" description = "a port of the serialize and unserialize functions of php to python." -category = "main" optional = false python-versions = "*" files = [ @@ -2156,7 +2068,6 @@ files = [ name = "pickleshare" version = "0.7.5" description = "Tiny 'shelve'-like database with concurrency support" -category = "main" optional = false python-versions = "*" files = [ @@ -2168,7 +2079,6 @@ files = [ name = "platformdirs" version = "2.4.1" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -2184,7 +2094,6 @@ test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock name = "pluggy" version = "0.13.1" description = "plugin and hook calling mechanisms for python" -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -2202,7 +2111,6 @@ dev = ["pre-commit", "tox"] name = "ply" version = "3.11" description = "Python Lex & Yacc" -category = "main" optional = false python-versions = "*" files = [ @@ -2214,7 +2122,6 @@ files = [ name = "pre-commit" version = "2.17.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." -category = "dev" optional = false python-versions = ">=3.6.1" files = [ @@ -2235,7 +2142,6 @@ virtualenv = ">=20.0.8" name = "prompt-toolkit" version = "3.0.7" description = "Library for building powerful interactive command lines in Python" -category = "main" optional = false python-versions = ">=3.6.1" files = [ @@ -2250,7 +2156,6 @@ wcwidth = "*" name = "protobuf" version = "3.15.0" description = "Protocol Buffers" -category = "main" optional = false python-versions = "*" files = [ @@ -2283,7 +2188,6 @@ six = ">=1.9" name = "psutil" version = "5.7.2" description = "Cross-platform lib for process and system monitoring in Python." -category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -2307,7 +2211,6 @@ test = ["enum34", "ipaddress", "mock", "pywin32", "unittest2", "wmi"] name = "psycopg2-binary" version = "2.9.3" description = "psycopg2 - Python-PostgreSQL Database Adapter" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2376,7 +2279,6 @@ files = [ name = "ptyprocess" version = "0.6.0" description = "Run a subprocess in a pseudo terminal" -category = "main" optional = false python-versions = "*" files = [ @@ -2388,7 +2290,6 @@ files = [ name = "pyasn1" version = "0.4.8" description = "ASN.1 types and codecs" -category = "main" optional = false python-versions = "*" files = [ @@ -2400,7 +2301,6 @@ files = [ name = "pyasn1-modules" version = "0.2.8" description = "A collection of ASN.1-based protocols modules." -category = "main" optional = false python-versions = "*" files = [ @@ -2415,7 +2315,6 @@ pyasn1 = ">=0.4.6,<0.5.0" name = "pycparser" version = "2.20" description = "C parser in Python" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -2427,7 +2326,6 @@ files = [ name = "pycryptodome" version = "3.9.8" description = "Cryptographic library for Python" -category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -2472,7 +2370,6 @@ files = [ name = "pygments" version = "2.7.4" description = "Pygments is a syntax highlighting package written in Python." -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -2484,7 +2381,6 @@ files = [ name = "pyjwt" version = "2.4.0" description = "JSON Web Token implementation in Python" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2502,7 +2398,6 @@ tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] name = "pylint" version = "2.14.4" description = "python code static checker" -category = "dev" optional = false python-versions = ">=3.7.2" files = [ @@ -2529,7 +2424,6 @@ testutils = ["gitpython (>3)"] name = "pyopenssl" version = "19.1.0" description = "Python wrapper module around the OpenSSL library" -category = "main" optional = false python-versions = "*" files = [ @@ -2549,7 +2443,6 @@ test = ["flaky", "pretend", "pytest (>=3.0.1)"] name = "pyotp" version = "2.4.0" description = "Python One Time Password Library" -category = "main" optional = false python-versions = "*" files = [ @@ -2561,7 +2454,6 @@ files = [ name = "pyparsing" version = "2.4.7" description = "Python parsing module" -category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -2573,7 +2465,6 @@ files = [ name = "pyre2" version = "0.3.6" description = "Python wrapper for Google\\'s RE2 using Cython" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2604,7 +2495,6 @@ test = ["pytest"] name = "pyreadline" version = "2.1" description = "A python implmementation of GNU readline." -category = "main" optional = false python-versions = "*" files = [ @@ -2615,7 +2505,6 @@ files = [ name = "pyspf" version = "2.0.14" description = "SPF (Sender Policy Framework) implemented in Python." -category = "main" optional = false python-versions = "*" files = [ @@ -2626,7 +2515,6 @@ files = [ name = "pytest" version = "7.3.1" description = "pytest: simple powerful testing with Python" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -2650,7 +2538,6 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no name = "pytest-cov" version = "3.0.0" description = "Pytest plugin for measuring coverage." -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -2669,7 +2556,6 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale name = "python-dateutil" version = "2.8.1" description = "Extensions to the standard Python datetime module" -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ @@ -2684,7 +2570,6 @@ six = ">=1.5" name = "python-dotenv" version = "0.14.0" description = "Add .env support to your django/flask apps in development and deployments" -category = "main" optional = false python-versions = "*" files = [ @@ -2699,7 +2584,6 @@ cli = ["click (>=5.0)"] name = "python-editor" version = "1.0.4" description = "Programmatically open an editor, capture the result." -category = "main" optional = false python-versions = "*" files = [ @@ -2712,7 +2596,6 @@ files = [ name = "python-gnupg" version = "0.4.6" description = "A wrapper for the Gnu Privacy Guard (GPG or GnuPG)" -category = "main" optional = false python-versions = "*" files = [ @@ -2724,7 +2607,6 @@ files = [ name = "pytz" version = "2020.1" description = "World timezone definitions, modern and historical" -category = "main" optional = false python-versions = "*" files = [ @@ -2736,7 +2618,6 @@ files = [ name = "pyyaml" version = "6.0" description = "YAML parser and emitter for Python" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -2786,7 +2667,6 @@ files = [ name = "redis" version = "4.5.3" description = "Python client for Redis database and key-value store" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2807,7 +2687,6 @@ ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)" name = "regex" version = "2022.6.2" description = "Alternative regular expression module, to replace re." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2891,7 +2770,6 @@ files = [ name = "requests" version = "2.25.1" description = "Python HTTP for Humans." -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -2913,7 +2791,6 @@ socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] name = "requests-file" version = "1.5.1" description = "File transport adapter for Requests" -category = "main" optional = false python-versions = "*" files = [ @@ -2929,7 +2806,6 @@ six = "*" name = "requests-oauthlib" version = "1.3.0" description = "OAuthlib authentication support for Requests." -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -2948,7 +2824,6 @@ rsa = ["oauthlib[signedtoken] (>=3.0.0)"] name = "rsa" version = "4.6" description = "Pure-Python RSA implementation" -category = "main" optional = false python-versions = ">=3.5, <4" files = [ @@ -2963,7 +2838,6 @@ pyasn1 = ">=0.1.3" name = "ruamel.yaml" version = "0.16.12" description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" -category = "main" optional = false python-versions = "*" files = [ @@ -2982,7 +2856,6 @@ jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] name = "ruamel.yaml.clib" version = "0.2.2" description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" -category = "main" optional = false python-versions = "*" files = [ @@ -3023,7 +2896,6 @@ files = [ name = "s3transfer" version = "0.3.3" description = "An Amazon S3 Transfer Manager" -category = "main" optional = false python-versions = "*" files = [ @@ -3038,7 +2910,6 @@ botocore = ">=1.12.36,<2.0a.0" name = "sentry-sdk" version = "1.5.11" description = "Python client for Sentry (https://sentry.io)" -category = "main" optional = false python-versions = "*" files = [ @@ -3072,7 +2943,6 @@ tornado = ["tornado (>=5)"] name = "setuptools" version = "67.6.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -3089,7 +2959,6 @@ testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs ( name = "simplejson" version = "3.17.2" description = "Simple, fast, extensible JSON encoder/decoder for Python" -category = "main" optional = false python-versions = ">=2.5, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -3144,7 +3013,6 @@ files = [ name = "six" version = "1.15.0" description = "Python 2 and 3 compatibility utilities" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -3156,7 +3024,6 @@ files = [ name = "sqlalchemy" version = "1.3.24" description = "Database Abstraction Library" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -3212,7 +3079,6 @@ pymysql = ["pymysql", "pymysql (<1)"] name = "sqlalchemy-utils" version = "0.36.8" description = "Various utility functions for SQLAlchemy." -category = "main" optional = false python-versions = "*" files = [ @@ -3242,7 +3108,6 @@ url = ["furl (>=0.4.1)"] name = "sqlparse" version = "0.4.2" description = "A non-validating SQL parser." -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -3254,7 +3119,6 @@ files = [ name = "strictyaml" version = "1.1.0" description = "Strict, typed YAML parser" -category = "main" optional = false python-versions = "*" files = [ @@ -3269,7 +3133,6 @@ python-dateutil = ">=2.6.0" name = "tld" version = "0.12.6" description = "Extract the top-level domain (TLD) from the URL given." -category = "main" optional = false python-versions = ">=2.7, <4" files = [ @@ -3286,7 +3149,6 @@ files = [ name = "tldextract" version = "3.1.2" description = "Accurately separate the TLD from the registered domain and subdomains of a URL, using the Public Suffix List. By default, this includes the public ICANN TLDs and their exceptions. You can optionally support the Public Suffix List's private domains as well." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -3304,7 +3166,6 @@ requests-file = ">=1.4" name = "toml" version = "0.10.1" description = "Python Library for Tom's Obvious, Minimal Language" -category = "dev" optional = false python-versions = "*" files = [ @@ -3316,7 +3177,6 @@ files = [ name = "tomli" version = "2.0.1" description = "A lil' TOML parser" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -3328,7 +3188,6 @@ files = [ name = "tomlkit" version = "0.11.0" description = "Style preserving TOML library" -category = "dev" optional = false python-versions = ">=3.6,<4.0" files = [ @@ -3340,7 +3199,6 @@ files = [ name = "tqdm" version = "4.64.0" description = "Fast, Extensible Progress Meter" -category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" files = [ @@ -3361,7 +3219,6 @@ telegram = ["requests"] name = "traitlets" version = "5.0.4" description = "Traitlets Python configuration system" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -3379,7 +3236,6 @@ test = ["pytest"] name = "twilio" version = "7.3.2" description = "Twilio API client and TwiML generator" -category = "main" optional = false python-versions = ">=3.6.0" files = [ @@ -3396,7 +3252,6 @@ requests = ">=2.0.0" name = "typed-ast" version = "1.5.2" description = "a fork of Python 2 and 3 ast modules with type comment support" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -3430,7 +3285,6 @@ files = [ name = "typing-extensions" version = "4.0.1" description = "Backported and Experimental Type Hints for Python 3.6+" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -3442,7 +3296,6 @@ files = [ name = "unidecode" version = "1.1.1" description = "ASCII transliterations of Unicode text" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -3454,7 +3307,6 @@ files = [ name = "uritemplate" version = "3.0.1" description = "URI templates" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -3466,7 +3318,6 @@ files = [ name = "urllib3" version = "1.25.10" description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" files = [ @@ -3483,7 +3334,6 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] name = "virtualenv" version = "20.8.1" description = "Virtual Python Environment builder" -category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" files = [ @@ -3507,7 +3357,6 @@ testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", name = "watchtower" version = "0.8.0" description = "Python CloudWatch Logging" -category = "main" optional = false python-versions = "*" files = [ @@ -3522,7 +3371,6 @@ boto3 = ">=1.9.253,<2" name = "wcwidth" version = "0.2.5" description = "Measures the displayed width of unicode strings in a terminal" -category = "main" optional = false python-versions = "*" files = [ @@ -3534,7 +3382,6 @@ files = [ name = "webauthn" version = "0.4.7" description = "A WebAuthn Python module." -category = "main" optional = false python-versions = "*" files = [ @@ -3553,7 +3400,6 @@ six = ">=1.11.0" name = "webob" version = "1.8.7" description = "WSGI request and response object" -category = "main" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*" files = [ @@ -3569,7 +3415,6 @@ testing = ["coverage", "pytest (>=3.1.0)", "pytest-cov", "pytest-xdist"] name = "werkzeug" version = "1.0.1" description = "The comprehensive WSGI web application library." -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -3585,7 +3430,6 @@ watchdog = ["watchdog"] name = "wrapt" version = "1.15.0" description = "Module for decorators, wrappers and monkey patching." -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" files = [ @@ -3670,7 +3514,6 @@ files = [ name = "wtforms" version = "2.3.3" description = "A flexible forms validation and rendering library for Python web development." -category = "main" optional = false python-versions = "*" files = [ @@ -3690,7 +3533,6 @@ locale = ["Babel (>=1.3)"] name = "yacron" version = "0.11.2" description = "A modern Cron replacement that is Docker-friendly" -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -3711,7 +3553,6 @@ strictyaml = ">=0.7.2" name = "yarl" version = "1.9.2" description = "Yet another URL library" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -3800,7 +3641,6 @@ typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} name = "zipp" version = "3.2.0" description = "Backport of pathlib-compatible object wrapper for zip files" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -3816,7 +3656,6 @@ testing = ["func-timeout", "jaraco.itertools", "jaraco.test (>=3.2.0)", "pytest name = "zope.event" version = "4.5.0" description = "Very basic event publishing system" -category = "main" optional = false python-versions = "*" files = [ @@ -3835,7 +3674,6 @@ test = ["zope.testrunner"] name = "zope.interface" version = "5.1.1" description = "Interfaces for Python" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -3892,4 +3730,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "^3.7.2" -content-hash = "6eb7557a03629a48ff4a8ed6cccdbb0a4678bd80c3d1b32cfbba062474ae0356" +content-hash = "9cf184eded5a8fb41f7725ff5ed0f26ad5bbd44b9d59a9180abb4c6bf3fe278a" diff --git a/pyproject.toml b/pyproject.toml index 9ae2da9e..60883109 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -111,6 +111,7 @@ Deprecated = "^1.2.13" cryptography = "37.0.1" SQLAlchemy = "1.3.24" redis = "^4.5.3" +newrelic-telemetry-sdk = "^0.5.0" [tool.poetry.dev-dependencies] pytest = "^7.0.0" diff --git a/tests/monitor/__init__.py b/tests/monitor/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/monitor/test_upcloud_get_metric.py b/tests/monitor/test_upcloud_get_metric.py new file mode 100644 index 00000000..fb3fd27d --- /dev/null +++ b/tests/monitor/test_upcloud_get_metric.py @@ -0,0 +1,350 @@ +from monitor.upcloud import get_metric, get_metrics +from monitor.metric import UpcloudMetrics, UpcloudMetric, UpcloudRecord + +import json + +MOCK_RESPONSE = """ +{ + "cpu_usage": { + "data": { + "cols": [ + { "label": "time", "type": "date" }, + { "label": "test-1 (master)", "type": "number" }, + { "label": "test-2 (standby)", "type": "number" } + ], + "rows": [ + ["2022-01-21T13:10:30Z", 2.744682398273781, 3.054323473090861], + ["2022-01-21T13:11:00Z", 3.0735645433218366, 2.972423595745795], + ["2022-01-21T13:11:30Z", 2.61619694060839, 3.1358378052207883], + ["2022-01-21T13:12:00Z", 3.275132296130991, 4.196249043309251] + ] + }, + "hints": { "title": "CPU usage %" } + }, + "disk_usage": { + "data": { + "cols": [ + { "label": "time", "type": "date" }, + { "label": "test-1 (master)", "type": "number" }, + { "label": "test-2 (standby)", "type": "number" } + ], + "rows": [ + ["2022-01-21T13:10:30Z", 5.654416415900109, 5.58959125727556], + ["2022-01-21T13:11:00Z", 5.654416415900109, 5.58959125727556], + ["2022-01-21T13:11:30Z", 5.654416415900109, 5.58959125727556] + ] + }, + "hints": { "title": "Disk space usage %" } + }, + "diskio_reads": { + "data": { + "cols": [ + { "label": "time", "type": "date" }, + { "label": "test-1 (master)", "type": "number" }, + { "label": "test-2 (standby)", "type": "number" } + ], + "rows": [ + ["2022-01-21T13:10:30Z", 0, 0], + ["2022-01-21T13:11:00Z", 0, 0], + ["2022-01-21T13:11:30Z", 0, 0] + ] + }, + "hints": { "title": "Disk iops (reads)" } + }, + "diskio_writes": { + "data": { + "cols": [ + { "label": "time", "type": "date" }, + { "label": "test-1 (master)", "type": "number" }, + { "label": "test-2 (standby)", "type": "number" } + ], + "rows": [ + ["2022-01-21T13:10:30Z", 3, 2], + ["2022-01-21T13:11:00Z", 2, 3], + ["2022-01-21T13:11:30Z", 4, 3] + ] + }, + "hints": { "title": "Disk iops (writes)" } + }, + "load_average": { + "data": { + "cols": [ + { "label": "time", "type": "date" }, + { "label": "test-1 (master)", "type": "number" }, + { "label": "test-2 (standby)", "type": "number" } + ], + "rows": [ + ["2022-01-21T13:10:30Z", 0.11, 0.11], + ["2022-01-21T13:11:00Z", 0.14, 0.1], + ["2022-01-21T13:11:30Z", 0.14, 0.09] + ] + }, + "hints": { "title": "Load average (5 min)" } + }, + "mem_usage": { + "data": { + "cols": [ + { "label": "time", "type": "date" }, + { "label": "test-1 (master)", "type": "number" }, + { "label": "test-2 (standby)", "type": "number" } + ], + "rows": [ + ["2022-01-21T13:10:30Z", 11.491766148261078, 12.318932883261219], + ["2022-01-21T13:11:00Z", 11.511967645759277, 12.304403727425075], + ["2022-01-21T13:11:30Z", 11.488581675749048, 12.272260458006759] + ] + }, + "hints": { "title": "Memory usage %" } + }, + "net_receive": { + "data": { + "cols": [ + { "label": "time", "type": "date" }, + { "label": "test-1 (master)", "type": "number" }, + { "label": "test-2 (standby)", "type": "number" } + ], + "rows": [ + ["2022-01-21T13:10:30Z", 442, 470], + ["2022-01-21T13:11:00Z", 439, 384], + ["2022-01-21T13:11:30Z", 466, 458] + ] + }, + "hints": { "title": "Network receive (bytes/s)" } + }, + "net_send": { + "data": { + "cols": [ + { "label": "time", "type": "date" }, + { "label": "test-1 (master)", "type": "number" }, + { "label": "test-2 (standby)", "type": "number" } + ], + "rows": [ + ["2022-01-21T13:10:30Z", 672, 581], + ["2022-01-21T13:11:00Z", 660, 555], + ["2022-01-21T13:11:30Z", 694, 573] + ] + }, + "hints": { "title": "Network transmit (bytes/s)" } + } +} +""" + + +def test_get_metrics(): + response = json.loads(MOCK_RESPONSE) + metrics = get_metrics(response) + assert metrics == UpcloudMetrics( + metrics=[ + UpcloudMetric( + metric_name="cpu_usage", + records=[ + UpcloudRecord( + db_role="master", + label="test-1 " "(master)", + time="2022-01-21T13:12:00Z", + value=3.275132296130991, + ), + UpcloudRecord( + db_role="standby", + label="test-2 " "(standby)", + time="2022-01-21T13:12:00Z", + value=4.196249043309251, + ), + ], + ), + UpcloudMetric( + metric_name="disk_usage", + records=[ + UpcloudRecord( + db_role="master", + label="test-1 " "(master)", + time="2022-01-21T13:11:30Z", + value=5.654416415900109, + ), + UpcloudRecord( + db_role="standby", + label="test-2 " "(standby)", + time="2022-01-21T13:11:30Z", + value=5.58959125727556, + ), + ], + ), + UpcloudMetric( + metric_name="diskio_reads", + records=[ + UpcloudRecord( + db_role="master", + label="test-1 " "(master)", + time="2022-01-21T13:11:30Z", + value=0, + ), + UpcloudRecord( + db_role="standby", + label="test-2 " "(standby)", + time="2022-01-21T13:11:30Z", + value=0, + ), + ], + ), + UpcloudMetric( + metric_name="diskio_writes", + records=[ + UpcloudRecord( + db_role="master", + label="test-1 " "(master)", + time="2022-01-21T13:11:30Z", + value=4, + ), + UpcloudRecord( + db_role="standby", + label="test-2 " "(standby)", + time="2022-01-21T13:11:30Z", + value=3, + ), + ], + ), + UpcloudMetric( + metric_name="load_average", + records=[ + UpcloudRecord( + db_role="master", + label="test-1 " "(master)", + time="2022-01-21T13:11:30Z", + value=0.14, + ), + UpcloudRecord( + db_role="standby", + label="test-2 " "(standby)", + time="2022-01-21T13:11:30Z", + value=0.09, + ), + ], + ), + UpcloudMetric( + metric_name="mem_usage", + records=[ + UpcloudRecord( + db_role="master", + label="test-1 " "(master)", + time="2022-01-21T13:11:30Z", + value=11.488581675749048, + ), + UpcloudRecord( + db_role="standby", + label="test-2 " "(standby)", + time="2022-01-21T13:11:30Z", + value=12.272260458006759, + ), + ], + ), + UpcloudMetric( + metric_name="net_receive", + records=[ + UpcloudRecord( + db_role="master", + label="test-1 " "(master)", + time="2022-01-21T13:11:30Z", + value=466, + ), + UpcloudRecord( + db_role="standby", + label="test-2 " "(standby)", + time="2022-01-21T13:11:30Z", + value=458, + ), + ], + ), + UpcloudMetric( + metric_name="net_send", + records=[ + UpcloudRecord( + db_role="master", + label="test-1 " "(master)", + time="2022-01-21T13:11:30Z", + value=694, + ), + UpcloudRecord( + db_role="standby", + label="test-2 " "(standby)", + time="2022-01-21T13:11:30Z", + value=573, + ), + ], + ), + ] + ) + + +def test_get_metric(): + response = json.loads(MOCK_RESPONSE) + metric_name = "cpu_usage" + metric = get_metric(response, metric_name) + + assert metric.metric_name == metric_name + assert len(metric.records) == 2 + assert metric.records[0].label == "test-1 (master)" + assert metric.records[0].time == "2022-01-21T13:12:00Z" + assert metric.records[0].value == 3.275132296130991 + + assert metric.records[1].label == "test-2 (standby)" + assert metric.records[1].time == "2022-01-21T13:12:00Z" + assert metric.records[1].value == 4.196249043309251 + + +def test_get_metric_with_none_value(): + response_str = """ +{ + "cpu_usage": { + "data": { + "cols": [ + { "label": "time", "type": "date" }, + { "label": "test-1 (master)", "type": "number" }, + { "label": "test-2 (standby)", "type": "number" } + ], + "rows": [ + ["2022-01-21T13:10:30Z", 2.744682398273781, 3.054323473090861], + ["2022-01-21T13:11:00Z", 3.0735645433218366, 2.972423595745795], + ["2022-01-21T13:11:30Z", null, 3.1358378052207883], + ["2022-01-21T13:12:00Z", 3.275132296130991, null] + ] + }, + "hints": { "title": "CPU usage %" } + } +} +""" + response = json.loads(response_str) + metric = get_metric(response, "cpu_usage") + + assert metric.records[0].label == "test-1 (master)" + assert metric.records[0].value == 3.275132296130991 + assert metric.records[1].label == "test-2 (standby)" + assert metric.records[1].value == 3.1358378052207883 + + +def test_get_metric_with_none_value_in_last_two_positions(): + response_str = """ +{ + "cpu_usage": { + "data": { + "cols": [ + { "label": "time", "type": "date" }, + { "label": "test-1 (master)", "type": "number" }, + { "label": "test-2 (standby)", "type": "number" } + ], + "rows": [ + ["2022-01-21T13:10:30Z", 2.744682398273781, 3.054323473090861], + ["2022-01-21T13:11:00Z", 3.0735645433218366, 2.972423595745795], + ["2022-01-21T13:11:30Z", null, null], + ["2022-01-21T13:12:00Z", 3.275132296130991, null] + ] + }, + "hints": { "title": "CPU usage %" } + } +} +""" + response = json.loads(response_str) + metric = get_metric(response, "cpu_usage") + + assert len(metric.records) == 1 + assert metric.records[0].label == "test-1 (master)" + assert metric.records[0].value == 3.275132296130991