Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"name": "mgo-metric-service",
"image": "mcr.microsoft.com/devcontainers/python:3.11",
"containerEnv": {
"PYTHONUNBUFFERED": "1",
"UV_CACHE_DIR": "/home/vscode/.cache/uv",
"PYTHONDONTWRITEBYTECODE": "1",
"UV_LINK_MODE": "copy",
"UV_PROJECT_ENVIRONMENT": "/home/vscode/.venv",
"UV_COMPILE_BYTECODE": "1"
},
"features": {
"ghcr.io/va-h/devcontainers-features/uv:1": {
"shellAutocompletion": true
},
"ghcr.io/jsburckhardt/devcontainer-features/ruff:1": {}
},
"customizations": {
"vscode": {
"extensions": [
"ms-python.python",
"ms-python.vscode-pylance",
"charliermarsh.ruff",
"EditorConfig.EditorConfig",
"redhat.vscode-yaml",
"esbenp.prettier-vscode"
],
"settings": {
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.formatOnSave": true
},
"[markdown]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
},
"editor.codeActionsOnSave": {
"source.fixAll.ruff": true,
"source.organizeImports.ruff": true
},
"python-envs.workspaceSearchPaths": [
"/home/vscode/.venv"
]
},
"mypy-type-checker.importStrategy": "fromEnvironment"
}
},
"postCreateCommand": "uv sync --locked",
"postStartCommand": "uv run pre-commit install",
"remoteUser": "vscode",
"containerUser": "vscode"
}
9 changes: 9 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
root = true

[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
11 changes: 8 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ on:

jobs:
ci:
name: Checkout, SonarQube scan
name: CI Checks (Code Quality, Static Analysis and Security Scan)
runs-on: ubuntu-24.04

steps:
Expand All @@ -19,9 +19,14 @@ jobs:
with:
fetch-depth: 0

- name: Run SonarQube scan
- name: Install uv
uses: astral-sh/setup-uv@v7

- name: Code Quality Checks
run: make check

- name: Static Analysis and Security Scan
uses: minvws/action-sonarqube@v1
with:
sonar-token: ${{ secrets.SONAR_TOKEN }}
allow-run-on-dependabot: 'true'

3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
__pycache__/
.coverage
coverage.xml
13 changes: 13 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
repos:
- repo: https://github.com/astral-sh/uv-pre-commit
rev: 0.11.14
hooks:
- id: uv-lock

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.13
hooks:
- id: ruff-check
args: [--fix]
- id: ruff-format

4 changes: 4 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"printWidth": 80,
"proseWrap": "always"
}
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.11
6 changes: 6 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"sonarlint.connectedMode.project": {
"connectionId": "vws",
"projectKey": "minvws_nl-mgo-metric-service"
}
}
21 changes: 15 additions & 6 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,31 +1,40 @@
# Contributing to MGO Metric Service

Thank you for considering contributing to the MGO Metric Service! We welcome contributions from everyone. Below are some guidelines to help you get started.
Thank you for considering contributing to the MGO Metric Service! We welcome
contributions from everyone. Below are some guidelines to help you get started.

## How to Report Issues

If you encounter any issues or have suggestions for improvements, please feel free to open an issue on the GitHub repository. When reporting an issue, please include as much detail as possible, including steps to reproduce the issue and any relevant logs or screenshots.
If you encounter any issues or have suggestions for improvements, please feel
free to open an issue on the GitHub repository. When reporting an issue, please
include as much detail as possible, including steps to reproduce the issue and
any relevant logs or screenshots.

## How to Submit Pull Requests

1. Fork the repository and create your branch from `main`.
2. If you have added code that should be tested, add tests.
3. Ensure the test suite passes (`make test`).
4. Make sure your code passes the linters and analyzers (`make check`).
5. Submit a pull request to the `main` branch with a clear description of your changes.
5. Submit a pull request to the `main` branch with a clear description of your
changes.

## Coding Standards

- Follow the existing coding style.
- Write clear and concise commit messages following the [Conventional Commits specification](https://www.conventionalcommits.org/).
- Write clear and concise commit messages following the
[Conventional Commits specification](https://www.conventionalcommits.org/).
- Include comments in your code where necessary.

## Code of Conduct

Please note that this project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project, you agree to abide by its terms.
Please note that this project is released with a
[Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this
project, you agree to abide by its terms.

## License

By contributing to this project, you agree that your contributions will be licensed under the same license as the project.
By contributing to this project, you agree that your contributions will be
licensed under the same license as the project.

Thank you for your contributions!
30 changes: 23 additions & 7 deletions LICENSES.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,33 @@
The Dutch government reserves all rights to the works that belong to the Dutch Government published on this GitHub account. Nevertheless, reuse is permitted under the following conditions.
The Dutch government reserves all rights to the works that belong to the Dutch
Government published on this GitHub account. Nevertheless, reuse is permitted
under the following conditions.

First - The work that is created in the Covid19 context, such as the notification app, is available under the EUPL/1.2 license; and any outside contributions to it are under an individual Committer License Agreement (iCLA) that references this license explicitly.
First - The work that is created in the Covid19 context, such as the
notification app, is available under the EUPL/1.2 license; and any outside
contributions to it are under an individual Committer License Agreement (iCLA)
that references this license explicitly.

However - that does not mean that all (derived) work is solely under that single license. For example - where appropriate certain (open source) modules may be required and included. These will remain under their original Copyright and License (though some may be released under the EUPL/1.2 if they are unsuitable to be contributed back upstream).
However - that does not mean that all (derived) work is solely under that single
license. For example - where appropriate certain (open source) modules may be
required and included. These will remain under their original Copyright and
License (though some may be released under the EUPL/1.2 if they are unsuitable
to be contributed back upstream).

Secondly – The documents of this project that are held in the directory ‘documentation’ are released under the CC0 license (see www.rijksoverheid.nl/copyright). If you use (parts) of these documents, you are not obliged to quote the source, but you must not imply that the Dutch government endorses the derivative work.
Secondly – The documents of this project that are held in the directory
‘documentation’ are released under the CC0 license (see
www.rijksoverheid.nl/copyright). If you use (parts) of these documents, you are
not obliged to quote the source, but you must not imply that the Dutch
government endorses the derivative work.

The CC0 does not apply to:

- Patent rights and trademark rights.
- Third-party rights.

Finally – The EUPL 1.2 and the CC0 do not apply to photos, videos, infographics or other forms of images. It is therefore not permitted to reuse or transfer the image, unless the image explicitly states that it is permitted or you have received written permission of the Dutch Government. Such permission will not be withheld on unreasonable grounds.
Finally – The EUPL 1.2 and the CC0 do not apply to photos, videos, infographics
or other forms of images. It is therefore not permitted to reuse or transfer the
image, unless the image explicitly states that it is permitted or you have
received written permission of the Dutch Government. Such permission will not be
withheld on unreasonable grounds.

--
v1.01 / 2020-5-26
-- v1.01 / 2020-5-26
28 changes: 28 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
.SILENT: help
all: help

lint: ## Check for linting errors
uv run ruff check

lint-fix: ## Fix linting errors
uv run ruff check --fix --show-fixes

format: ## Format code
uv run ruff format

format-diff: ## Show formatting differences
uv run ruff format --diff

type-check: ## Check for typing errors
uv run mypy

test: ## Runs automated tests with code coverage and exports a code coverage report to an XML file
uv run pytest --cov --cov-fail-under=100 --cov-branch --cov-report=xml

check: lint format-diff type-check test ## Runs all checks
fix: lint-fix format ## Runs all fixers

help: ## Display available commands
echo "Available make commands:"
echo
grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m %-30s\033[0m %s\n", $$1, $$2}'
141 changes: 140 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,140 @@
Auto-generated README for nl-mgo-metric-service
# mgo-metric-service

A metrics client for Python, to register metrics with support for various
backends (e.g., StatsD) while keeping application code decoupled from metrics
implementation details.

This library offers a minimal, composable interface for emitting application
metrics (counters, gauges, timings) with flexible formatting and tagging
support.

---

## Installation

```bash
uv add "mgo-metric-service @ git+https://github.com/minvws/nl-mgo-metric-service.git"
```

---

## How it Works

The core of `mgo-metric-service` is the `MetricsClient`, which delegates metric
emission to a backend. You provide a backend instance (such as `StatsDBackend`,
`InMemoryBackend`, or `NoopBackend`). Each built-in backend uses a fixed
formatter; custom formatters are only supported on backends you define by
subclassing `MetricsBackend`.

The structure is:

- **Backends**: Handle communication with the actual metrics server or transport
(e.g., StatsD), in-memory recording, or no-op for ignored metrics.
- **Formatters**: Define how metric names and tags are turned into the final key
for transmission.
- **MetricsClient**: The high-level API your app uses (`incr`, `gauge`,
`timing`), delegating formatting and transport.

---

## Usage

### Basic Example with the StatsD Backend

```python
from mgo_metric_service import MetricsClient, StatsDBackend

# Set up your backend; point to the StatsD server (defaults shown)
backend = StatsDBackend(host="localhost", port=8125)

# Create your metrics client
client = MetricsClient(backend=backend)

# Emit a counter increment (with optional tags)
client.incr("api.request", count=1, tags=(("route", "health"), ("status", "200")))

# Emit a gauge metric
client.gauge("db.connections", value=5)

# Measure timing (in milliseconds)
client.timing("process.time_ms", duration_ms=42.3)
```

### No-Op Backend for Testing

```python
from mgo_metric_service import MetricsClient, NoopBackend

client = MetricsClient(backend=NoopBackend())
client.incr("any.metric") # Does nothing
```

### Fake Metrics Client for Testing

For tests where you want to assert that specific metrics were emitted, use
`FakeMetricsClient` and its assertion helpers.

```python
from mgo_metric_service import FakeMetricsClient

client = FakeMetricsClient()

client.incr("request.count", count=2)
client.gauge("memory.usage", value=123.4)
client.timing("db.latency", duration_ms=7.89)

# Assert that a matching metric was emitted
client.assert_metric_call({"type": "gauge", "key": "memory.usage", "value": 123.4})

# You can partially match fields
client.assert_metric_call({"type": "incr"})

# Or require that a metric was called multiple times
client.incr("request.count")
client.assert_metric_call({"type": "incr", "key": "request.count"}, times=2)

# Convenience helpers for exact typed assertions
client.assert_incr("request.count", count=2)
client.assert_gauge("memory.usage", value=123.4)
client.assert_timing("db.latency", duration_ms=7.89)
client.assert_incr("request.count", count=1, times=1)
```

---

### Customizing Metric Formatting

Built-in backends (`StatsDBackend`, `InMemoryBackend`, `NoopBackend`) ship with
a fixed formatter and cannot be overridden. To use a custom formatter, subclass
`MetricsBackend` and implement `create_formatter()`:

```python
from mgo_metric_service import MetricsBackend, MetricsClient
from mgo_metric_service.formatting import MetricFormatter
from your_app.formatter import CustomFormatter


class CustomBackend(MetricsBackend):
def create_formatter(self) -> MetricFormatter:
return CustomFormatter()

def incr(self, key: str, count: int = 1) -> None:
...

def gauge(self, key: str, value: float) -> None:
...

def timing(self, key: str, duration_ms: float) -> None:
...


client = MetricsClient(backend=CustomBackend())
```

---

## Running Tests with Coverage

```bash
make test
```
Loading
Loading