Skip to content

Commit

Permalink
Expose metrics and other improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
actualwitch committed Aug 15, 2023
1 parent 44ba726 commit 0453388
Show file tree
Hide file tree
Showing 14 changed files with 347 additions and 268 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ jobs:
strategy:
matrix:
python-version: ["3.8", "3.11", "pypy3.10"]
env:
FORCE_COLOR: 1
steps:
- uses: actions/checkout@v3
- name: Install poetry
Expand All @@ -28,4 +30,4 @@ jobs:
- name: Lint code
run: poetry run pyright
- name: Run tests
run: poetry run pytest
run: poetry run pytest -n auto
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ Exemplars are a way to associate a metric sample to a trace by attaching `trace_
To use exemplars, you need to first switch to a tracker that supports them by setting `AUTOMETRICS_TRACKER=prometheus` and enable
exemplar collection by setting `AUTOMETRICS_EXEMPLARS=true`. You also need to enable exemplars in Prometheus by launching Prometheus with the `--enable-feature=exemplar-storage` flag.

## Exporting metrics

After collecting metrics with Autometrics, you need to export them to Prometheus. You can either add a separate route to your server and use the `generate_latest` function from the `prometheus_client` package, or you can use the `start_http_server` function from the same package to start a separate server that will expose the metrics. Autometrics also re-exports the `start_http_server` function with a preselected port 9464 for compatibility with other Autometrics packages.

## Development of the package

This package uses [poetry](https://python-poetry.org) as a package manager, with all dependencies separated into three groups:
Expand Down
2 changes: 1 addition & 1 deletion examples/starlette-otel-exemplars.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def inner_function():

def metrics(request):
# Exemplars are not supported by default prometheus format, so we specifically
# make an endpoint that uses the OpenMetrics format that supoorts exemplars.
# make an endpoint that uses the OpenMetrics format that supports exemplars.
body = generate_latest(REGISTRY)
return PlainTextResponse(body, media_type="application/openmetrics-text")

Expand Down
395 changes: 186 additions & 209 deletions poetry.lock

Large diffs are not rendered by default.

26 changes: 18 additions & 8 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,21 @@ license = "MIT OR Apache-2.0"
readme = "README.md"
repository = "https://github.com/autometrics-dev/autometrics-py"
homepage = "https://github.com/autometrics-dev/autometrics-py"
keywords = ["metrics", "telemetry", "prometheus", "monitoring", "observability", "instrumentation"]
# classifiers = [
# "Topic :: Software Development :: Build Tools",
# "Topic :: Software Development :: Libraries :: Python Modules"
# ]
packages = [{include = "autometrics", from = "src"}]
keywords = [
"metrics",
"telemetry",
"prometheus",
"monitoring",
"observability",
"instrumentation",
"tracing",
]
classifiers = [
"Topic :: Software Development :: Build Tools",
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: System :: Monitoring",
]
packages = [{ include = "autometrics", from = "src" }]

[tool.poetry.dependencies]
opentelemetry-api = "^1.17.0"
Expand All @@ -31,6 +40,7 @@ pyright = "^1.1.307"
pytest = "^7.3.0"
pytest-asyncio = "^0.21.0"
black = "^23.3.0"
pytest-xdist = "^3.3.1"

[tool.poetry.group.examples]
optional = true
Expand All @@ -39,7 +49,7 @@ optional = true
anyio = "3.6.2"
bleach = "6.0.0"
build = "0.10.0"
certifi = "2022.12.7"
certifi = "2023.7.22"
charset-normalizer = "3.1.0"
click = "8.1.3"
django = "^4.2"
Expand All @@ -57,7 +67,7 @@ more-itertools = "9.1.0"
packaging = "23.0"
pkginfo = "1.9.6"
pydantic = "1.10.6"
pygments = "2.14.0"
pygments = "2.16.1"
pyproject-hooks = "1.0.0"
readme-renderer = "37.3"
requests = "2.31.0"
Expand Down
19 changes: 19 additions & 0 deletions src/autometrics/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,20 @@
from prometheus_client import start_wsgi_server, REGISTRY, CollectorRegistry
from typing_extensions import Unpack

from .decorator import *
from .settings import AutometricsOptions, init_settings
from .tracker import set_tracker, get_tracker_type


def init(**kwargs: Unpack[AutometricsOptions]):
"""Optional initialization function to be used instead of setting environment variables. You can override settings by calling init with the new settings."""
init_settings(**kwargs)
tracker_type = get_tracker_type()
set_tracker(tracker_type)


def start_http_server(
port: int = 9464, addr: str = "0.0.0.0", registry: CollectorRegistry = REGISTRY
):
"""Starts a WSGI server for prometheus metrics as a daemon thread."""
start_wsgi_server(port, addr, registry)
2 changes: 1 addition & 1 deletion src/autometrics/decorator.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"""Autometrics module."""
from contextvars import ContextVar, Token
import time
import inspect

from contextvars import ContextVar, Token
from functools import wraps
from typing import overload, TypeVar, Callable, Optional, Awaitable
from typing_extensions import ParamSpec
Expand Down
76 changes: 76 additions & 0 deletions src/autometrics/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import os

from typing import List, TypedDict, Optional
from typing_extensions import Unpack

from .objectives import ObjectiveLatency


class AutometricsSettings(TypedDict):
"""Settings for autometrics."""

histogram_buckets: List[float]
tracker: str
enable_exemplars: bool
service_name: str
commit: str
version: str
branch: str


class AutometricsOptions(TypedDict, total=False):
"""User supplied overrides for autometrics settings."""

histogram_buckets: List[float]
tracker: str
enable_exemplars: bool
service_name: str
commit: str
version: str
branch: str


def get_objective_boundaries():
"""Get the objective latency boundaries as float values in seconds (instead of strings)"""
return list(map(lambda c: float(c.value), ObjectiveLatency))


settings: Optional[AutometricsSettings] = None


def init_settings(**overrides: Unpack[AutometricsOptions]) -> AutometricsSettings:
config: AutometricsSettings = {
"histogram_buckets": overrides.get("histogram_buckets")
or get_objective_boundaries(),
"tracker": overrides.get("tracker")
or os.getenv("AUTOMETRICS_TRACKER")
or "opentelemetry",
"enable_exemplars": overrides.get(
"enable_exemplars", os.getenv("AUTOMETRICS_EXEMPLARS") == "true"
),
"service_name": overrides.get(
"service_name",
os.getenv(
"AUTOMETRICS_SERVICE_NAME",
os.getenv("OTEL_SERVICE_NAME", __package__.rsplit(".", 1)[0]),
),
),
"commit": overrides.get(
"commit", os.getenv("AUTOMETRICS_COMMIT", os.getenv("COMMIT_SHA", ""))
),
"branch": overrides.get(
"branch", os.getenv("AUTOMETRICS_BRANCH", os.getenv("BRANCH_NAME", ""))
),
"version": overrides.get("version", os.getenv("AUTOMETRICS_VERSION", "")),
}
global settings
settings = config
return settings


def get_settings() -> AutometricsSettings:
"""Get the current settings."""
global settings
if settings is None:
return init_settings()
return settings
21 changes: 8 additions & 13 deletions src/autometrics/tracker/opentelemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,7 @@
OBJECTIVE_PERCENTILE,
OBJECTIVE_LATENCY_THRESHOLD,
)
from ..utils import get_service_name


def get_objective_boundaries():
"""Get the objective latency boundaries as float values in seconds (instead of strings)"""
return list(map(lambda c: float(c.value), ObjectiveLatency))
from ..settings import get_settings


class OpenTelemetryTracker:
Expand All @@ -52,7 +47,7 @@ def __init__(self):
description=HISTOGRAM_DESCRIPTION,
instrument_name=HISTOGRAM_NAME,
aggregation=ExplicitBucketHistogramAggregation(
boundaries=get_objective_boundaries()
boundaries=get_settings()["histogram_buckets"]
),
)
meter_provider = MeterProvider(metric_readers=[exporter], views=[view])
Expand Down Expand Up @@ -103,7 +98,7 @@ def __count(
"caller.function": caller_function,
OBJECTIVE_NAME: objective_name,
OBJECTIVE_PERCENTILE: percentile,
SERVICE_NAME: get_service_name(),
SERVICE_NAME: get_settings()["service_name"],
},
)

Expand Down Expand Up @@ -131,7 +126,7 @@ def __histogram(
attributes={
"function": function,
"module": module,
SERVICE_NAME: get_service_name(),
SERVICE_NAME: get_settings()["service_name"],
OBJECTIVE_NAME: objective_name,
OBJECTIVE_PERCENTILE: percentile,
OBJECTIVE_LATENCY_THRESHOLD: threshold,
Expand All @@ -147,7 +142,7 @@ def set_build_info(self, commit: str, version: str, branch: str):
"commit": commit,
"version": version,
"branch": branch,
SERVICE_NAME: get_service_name(),
SERVICE_NAME: get_settings()["service_name"],
},
)

Expand All @@ -164,7 +159,7 @@ def start(
attributes={
"function": function,
"module": module,
SERVICE_NAME: get_service_name(),
SERVICE_NAME: get_settings()["service_name"],
},
)

Expand All @@ -184,7 +179,7 @@ def finish(
exemplar = None
# Currently, exemplars are only supported by prometheus-client
# https://github.com/autometrics-dev/autometrics-py/issues/41
# if os.getenv("AUTOMETRICS_EXEMPLARS") == "true":
# if get_settings()["exemplars"]:
# exemplar = get_exemplar()
self.__count(
function,
Expand All @@ -202,7 +197,7 @@ def finish(
attributes={
"function": function,
"module": module,
SERVICE_NAME: get_service_name(),
SERVICE_NAME: get_settings()["service_name"],
},
)

Expand Down
16 changes: 8 additions & 8 deletions src/autometrics/tracker/prometheus.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import os
import time
from typing import Optional
from prometheus_client import Counter, Histogram, Gauge
Expand All @@ -24,7 +23,7 @@
from .exemplar import get_exemplar
from .tracker import Result
from ..objectives import Objective
from ..utils import get_service_name
from ..settings import get_settings


class PrometheusTracker:
Expand Down Expand Up @@ -56,6 +55,7 @@ class PrometheusTracker:
OBJECTIVE_LATENCY_THRESHOLD_PROMETHEUS,
],
unit="seconds",
buckets=get_settings()["histogram_buckets"],
)
prom_gauge_build_info = Gauge(
BUILD_INFO_NAME,
Expand Down Expand Up @@ -93,7 +93,7 @@ def _count(
if objective is None or objective.success_rate is None
else objective.success_rate.value
)
service_name = get_service_name()
service_name = get_settings()["service_name"]

self.prom_counter.labels(
func_name,
Expand Down Expand Up @@ -124,7 +124,7 @@ def _histogram(
if latency is not None:
threshold = latency[0].value
percentile = latency[1].value
service_name = get_service_name()
service_name = get_settings()["service_name"]

self.prom_histogram.labels(
func_name,
Expand All @@ -138,7 +138,7 @@ def _histogram(
def set_build_info(self, commit: str, version: str, branch: str):
if not self._has_set_build_info:
self._has_set_build_info = True
service_name = get_service_name()
service_name = get_settings()["service_name"]
self.prom_gauge_build_info.labels(
commit, version, branch, service_name
).set(1)
Expand All @@ -148,7 +148,7 @@ def start(
):
"""Start tracking metrics for a function call."""
if track_concurrency:
service_name = get_service_name()
service_name = get_settings()["service_name"]
self.prom_gauge_concurrency.labels(function, module, service_name).inc()

def finish(
Expand All @@ -164,7 +164,7 @@ def finish(
):
"""Finish tracking metrics for a function call."""
exemplar = None
if os.getenv("AUTOMETRICS_EXEMPLARS") == "true":
if get_settings()["enable_exemplars"]:
exemplar = get_exemplar()

self._count(
Expand All @@ -179,7 +179,7 @@ def finish(
self._histogram(function, module, start_time, objective, exemplar)

if track_concurrency:
service_name = get_service_name()
service_name = get_settings()["service_name"]
self.prom_gauge_concurrency.labels(function, module, service_name).dec()

def initialize_counters(
Expand Down
6 changes: 3 additions & 3 deletions src/autometrics/tracker/test_format.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
from prometheus_client.exposition import generate_latest
import pytest

from . import init_tracker, TrackerType
from .opentelemetry import OpenTelemetryTracker
from . import TrackerType
from ..decorator import autometrics
from .. import init


@pytest.mark.parametrize("tracker", TrackerType)
def test_metrics_format(tracker):
"""Test that the metrics are formatted correctly."""
init_tracker(tracker)
init(tracker=tracker.value)

@autometrics
def test_function():
Expand Down
Loading

0 comments on commit 0453388

Please sign in to comment.