edutap.observability-settings 0.1.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- edutap_observability_settings-0.1.3/.github/workflows/ci.yml +39 -0
- edutap_observability_settings-0.1.3/.github/workflows/release.yaml +90 -0
- edutap_observability_settings-0.1.3/.gitignore +17 -0
- edutap_observability_settings-0.1.3/.pre-commit-config.yaml +14 -0
- edutap_observability_settings-0.1.3/CLAUDE.md +85 -0
- edutap_observability_settings-0.1.3/LICENSE +11 -0
- edutap_observability_settings-0.1.3/Makefile +25 -0
- edutap_observability_settings-0.1.3/PKG-INFO +145 -0
- edutap_observability_settings-0.1.3/README.md +112 -0
- edutap_observability_settings-0.1.3/docs/superpowers/README.md +15 -0
- edutap_observability_settings-0.1.3/docs/superpowers/specs/2026-08-10-observability-settings-design.md +141 -0
- edutap_observability_settings-0.1.3/pyproject.toml +93 -0
- edutap_observability_settings-0.1.3/renovate.json5 +79 -0
- edutap_observability_settings-0.1.3/src/edutap/observability_settings/__init__.py +31 -0
- edutap_observability_settings-0.1.3/src/edutap/observability_settings/install.py +153 -0
- edutap_observability_settings-0.1.3/src/edutap/observability_settings/pseudonym.py +57 -0
- edutap_observability_settings-0.1.3/src/edutap/observability_settings/settings.py +72 -0
- edutap_observability_settings-0.1.3/tests/test_install.py +132 -0
- edutap_observability_settings-0.1.3/tests/test_pseudonym.py +60 -0
- edutap_observability_settings-0.1.3/tests/test_settings.py +60 -0
- edutap_observability_settings-0.1.3/tox.ini +16 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [main]
|
|
5
|
+
pull_request:
|
|
6
|
+
# Nothing here is locked: uv.lock is git-ignored and every requirement is a lower
|
|
7
|
+
# bound. A suite that passed last week can fail today with nothing changed here.
|
|
8
|
+
schedule:
|
|
9
|
+
- cron: "17 5 * * 1"
|
|
10
|
+
# Callable, so the release workflow runs *these* checks rather than its own copy.
|
|
11
|
+
# A release gate that drifts from CI is a gate that passes what CI would have
|
|
12
|
+
# stopped -- and it drifts silently, because nobody reads two files side by side.
|
|
13
|
+
workflow_call:
|
|
14
|
+
jobs:
|
|
15
|
+
test:
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
strategy:
|
|
18
|
+
matrix:
|
|
19
|
+
python: ["3.13", "3.14"]
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v5
|
|
22
|
+
- uses: astral-sh/setup-uv@v7
|
|
23
|
+
with:
|
|
24
|
+
python-version: ${{ matrix.python }}
|
|
25
|
+
- run: uv venv
|
|
26
|
+
- run: uv pip install -e ".[dev]"
|
|
27
|
+
- run: .venv/bin/python -m pytest -v
|
|
28
|
+
lint:
|
|
29
|
+
runs-on: ubuntu-latest
|
|
30
|
+
steps:
|
|
31
|
+
- uses: actions/checkout@v5
|
|
32
|
+
- uses: astral-sh/setup-uv@v7
|
|
33
|
+
with:
|
|
34
|
+
python-version: "3.13"
|
|
35
|
+
- run: uv venv
|
|
36
|
+
- run: uv pip install -e ".[dev]"
|
|
37
|
+
- run: .venv/bin/python -m ruff check src tests
|
|
38
|
+
- run: .venv/bin/python -m ruff format --check src tests
|
|
39
|
+
- run: .venv/bin/python -m ty check src
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Build & upload PyPI package
|
|
3
|
+
|
|
4
|
+
on:
|
|
5
|
+
push:
|
|
6
|
+
branches: [main]
|
|
7
|
+
tags: ["*"]
|
|
8
|
+
release:
|
|
9
|
+
types:
|
|
10
|
+
- published
|
|
11
|
+
workflow_dispatch:
|
|
12
|
+
|
|
13
|
+
# Without ``github.event_name`` in the group, the ``push`` event fired
|
|
14
|
+
# by tagging a release and the ``release`` event fired by publishing
|
|
15
|
+
# it land in the same group. ``cancel-in-progress: true`` then kills
|
|
16
|
+
# whichever arrived second — typically the ``release`` run that
|
|
17
|
+
# uploads to pypi.org, leaving the tag unpublished.
|
|
18
|
+
concurrency:
|
|
19
|
+
group: release-${{ github.ref }}-${{ github.event_name }}
|
|
20
|
+
cancel-in-progress: true
|
|
21
|
+
|
|
22
|
+
jobs:
|
|
23
|
+
# The repository's own CI, called rather than copied. A release gate that
|
|
24
|
+
# restates the checks drifts from them, and it drifts silently.
|
|
25
|
+
tests:
|
|
26
|
+
uses: "./.github/workflows/ci.yml"
|
|
27
|
+
|
|
28
|
+
# Always build & lint package.
|
|
29
|
+
build-package:
|
|
30
|
+
name: Build & verify package
|
|
31
|
+
needs:
|
|
32
|
+
- tests
|
|
33
|
+
runs-on: ubuntu-latest
|
|
34
|
+
permissions:
|
|
35
|
+
attestations: write
|
|
36
|
+
id-token: write
|
|
37
|
+
|
|
38
|
+
steps:
|
|
39
|
+
- uses: actions/checkout@v7
|
|
40
|
+
with:
|
|
41
|
+
fetch-depth: 0
|
|
42
|
+
persist-credentials: false
|
|
43
|
+
|
|
44
|
+
- uses: hynek/build-and-inspect-python-package@v3.0.1
|
|
45
|
+
with:
|
|
46
|
+
attest-build-provenance-github: 'true'
|
|
47
|
+
|
|
48
|
+
# Upload to Test PyPI on every commit on main.
|
|
49
|
+
release-test-pypi:
|
|
50
|
+
name: Publish in-dev package to test.pypi.org
|
|
51
|
+
environment: release-test-pypi
|
|
52
|
+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
|
53
|
+
runs-on: ubuntu-latest
|
|
54
|
+
needs:
|
|
55
|
+
- build-package
|
|
56
|
+
permissions:
|
|
57
|
+
id-token: write
|
|
58
|
+
|
|
59
|
+
steps:
|
|
60
|
+
- name: Download packages built by build-and-inspect-python-package
|
|
61
|
+
uses: actions/download-artifact@v8
|
|
62
|
+
with:
|
|
63
|
+
name: Packages
|
|
64
|
+
path: dist
|
|
65
|
+
|
|
66
|
+
- name: Upload package to Test PyPI
|
|
67
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
68
|
+
with:
|
|
69
|
+
repository-url: https://test.pypi.org/legacy/
|
|
70
|
+
|
|
71
|
+
# Upload to real PyPI on GitHub Releases.
|
|
72
|
+
release-pypi:
|
|
73
|
+
name: Publish released package to pypi.org
|
|
74
|
+
environment: release-pypi
|
|
75
|
+
if: github.event.action == 'published'
|
|
76
|
+
runs-on: ubuntu-latest
|
|
77
|
+
needs:
|
|
78
|
+
- build-package
|
|
79
|
+
permissions:
|
|
80
|
+
id-token: write
|
|
81
|
+
|
|
82
|
+
steps:
|
|
83
|
+
- name: Download packages built by build-and-inspect-python-package
|
|
84
|
+
uses: actions/download-artifact@v8
|
|
85
|
+
with:
|
|
86
|
+
name: Packages
|
|
87
|
+
path: dist
|
|
88
|
+
|
|
89
|
+
- name: Upload package to PyPI
|
|
90
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
.venv/
|
|
4
|
+
.pytest_cache/
|
|
5
|
+
.ruff_cache/
|
|
6
|
+
.mypy_cache/
|
|
7
|
+
dist/
|
|
8
|
+
build/
|
|
9
|
+
*.egg-info/
|
|
10
|
+
.env
|
|
11
|
+
docs/_build/
|
|
12
|
+
uv.lock
|
|
13
|
+
|
|
14
|
+
.superpowers/
|
|
15
|
+
|
|
16
|
+
# mutmut copies the whole project here to run the suite against each mutant.
|
|
17
|
+
mutants/
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
3
|
+
rev: v0.16.0
|
|
4
|
+
hooks:
|
|
5
|
+
- id: ruff-check
|
|
6
|
+
args: [--fix]
|
|
7
|
+
- id: ruff-format
|
|
8
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
9
|
+
rev: v6.0.0
|
|
10
|
+
hooks:
|
|
11
|
+
- id: trailing-whitespace
|
|
12
|
+
- id: end-of-file-fixer
|
|
13
|
+
- id: check-yaml
|
|
14
|
+
- id: check-toml
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# CLAUDE.md — edutap.observability_settings
|
|
2
|
+
|
|
3
|
+
Repository-specific rules. They take precedence over the global defaults.
|
|
4
|
+
|
|
5
|
+
## Language
|
|
6
|
+
|
|
7
|
+
**English only.** This repository belongs to eduTAP proper, not to any single
|
|
8
|
+
institution: README, changelog, documentation, docstrings, code comments, commit
|
|
9
|
+
messages, pull request titles and bodies, and replies to review comments.
|
|
10
|
+
|
|
11
|
+
The language follows the repository, not the conversation. A discussion held in
|
|
12
|
+
German still produces English artefacts here.
|
|
13
|
+
|
|
14
|
+
## What this package is
|
|
15
|
+
|
|
16
|
+
Error reporting, tracing and structured logging, wired the same way in every eduTAP
|
|
17
|
+
service, plus the decision about what a service may say about a person. It is
|
|
18
|
+
installed before a service resolves its own settings.
|
|
19
|
+
|
|
20
|
+
## Guard rails
|
|
21
|
+
|
|
22
|
+
**Every option here is a decision about what leaves the process.** The Sentry options
|
|
23
|
+
are not defaults someone liked; each was chosen against a measurement and each
|
|
24
|
+
contradicts the backend's own recommendation. Changing one means repeating the
|
|
25
|
+
measurement and recording it, not reasoning about it.
|
|
26
|
+
|
|
27
|
+
**`pseudonym` without a salt yields nothing, never the raw value.** A deployment that
|
|
28
|
+
asked for pseudonyms and forgot the key has to lose the datum. Any code path that
|
|
29
|
+
falls back to the plain `person_uid` defeats the whole package.
|
|
30
|
+
|
|
31
|
+
**`send_to_logfire` stays `False`.** The library defaults it to `True`. This estate
|
|
32
|
+
exports to its own collector, and an unset value ships spans to a hosted third party
|
|
33
|
+
the first time a token happens to be present.
|
|
34
|
+
|
|
35
|
+
**Never import from an eduTAP service.** This is installed before a service resolves
|
|
36
|
+
its settings; it can know nothing about them. Depending on `edutap.data_models` is
|
|
37
|
+
the one exception, and it is a library.
|
|
38
|
+
|
|
39
|
+
**Options are returned before they are applied.** `sentry_options` and
|
|
40
|
+
`logfire_options` are pure so a test can assert the exact set rather than assert that
|
|
41
|
+
something was configured. Do not inline them into the `init` call.
|
|
42
|
+
|
|
43
|
+
**No `uv.lock`.** This is a library; pinning here would push a resolution onto every
|
|
44
|
+
consumer.
|
|
45
|
+
|
|
46
|
+
## Working practice
|
|
47
|
+
|
|
48
|
+
Branch first, never commit on `main`. Push only when asked. `make lint` and
|
|
49
|
+
`make test-local` green before opening a pull request.
|
|
50
|
+
|
|
51
|
+
Design records live under `docs/superpowers/`. They are records of a decision at a
|
|
52
|
+
point in time — do not rewrite them to match a later state; write a new one.
|
|
53
|
+
|
|
54
|
+
## Sources and confidentiality
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
**No vendor internals — from any vendor, not just the ones currently in play.**
|
|
58
|
+
Neither in files nor in commit messages.
|
|
59
|
+
|
|
60
|
+
The standard is academic: a statement counts as reliable only where it can be
|
|
61
|
+
evidenced from public information, with a link. Everything else was obtained either
|
|
62
|
+
by our own testing or through insider knowledge, and the three are not
|
|
63
|
+
interchangeable:
|
|
64
|
+
|
|
65
|
+
* **Documented** — public source, linked. May be written as fact.
|
|
66
|
+
* **Verified, not citable** — obtained by a person from an access-protected area and
|
|
67
|
+
checked there; the reference is recorded internally but must not be published; and
|
|
68
|
+
the statement has been reduced to what is not confidential. May be written as fact,
|
|
69
|
+
carrying this label. It is the rule journalism uses for source protection: the claim
|
|
70
|
+
stands, we know where it comes from, the reader does not get the source.
|
|
71
|
+
|
|
72
|
+
The four conditions hold together. A statement for which nobody can name the
|
|
73
|
+
internal reference does not fall here — that is insider knowledge.
|
|
74
|
+
* **Measured** — established by our own tests. May be written down, but always marked
|
|
75
|
+
as such, because it describes what a platform did on the day we looked, not what it
|
|
76
|
+
guarantees. It can change with the next release, without notice and without an
|
|
77
|
+
entry in any changelog.
|
|
78
|
+
* **Insider knowledge** — is not written down at all.
|
|
79
|
+
|
|
80
|
+
What a platform's behaviour *means for us* stays documentable even where the
|
|
81
|
+
mechanism does not: "the platform enforces a deadline, it is self-healing, it is
|
|
82
|
+
outside our control" carries the design consequence without disclosing anything.
|
|
83
|
+
|
|
84
|
+
Contract and regulatory material is wanted and citable: eduPersonAssurance, GÉANT and
|
|
85
|
+
eduGAIN terms, published wallet programme obligations.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
|
|
3
|
+
Licensed under the European Union Public Licence, Version 1.2 (the "EUPL").
|
|
4
|
+
|
|
5
|
+
You may obtain a copy of the licence at:
|
|
6
|
+
|
|
7
|
+
https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
|
|
8
|
+
|
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
distributed under the Licence is distributed on an "AS IS" basis, WITHOUT
|
|
11
|
+
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
PYTHON := .venv/bin/python
|
|
2
|
+
VENV := .venv
|
|
3
|
+
|
|
4
|
+
.DEFAULT_GOAL := help
|
|
5
|
+
.PHONY: help venv lint reformat test-local
|
|
6
|
+
|
|
7
|
+
help: ## Show available targets
|
|
8
|
+
@grep -hE '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) \
|
|
9
|
+
| awk -F':.*?## ' '{printf " %-14s %s\n", $$1, $$2}'
|
|
10
|
+
|
|
11
|
+
venv: ## Create .venv and install the package with its dev extra
|
|
12
|
+
test -d $(VENV) || uv venv
|
|
13
|
+
uv pip install -U -e ".[dev]"
|
|
14
|
+
|
|
15
|
+
lint: venv ## Run ruff checks and the type checker
|
|
16
|
+
$(PYTHON) -m ruff check src tests
|
|
17
|
+
$(PYTHON) -m ruff format --check src tests
|
|
18
|
+
$(PYTHON) -m ty check src
|
|
19
|
+
|
|
20
|
+
reformat: venv ## Autoformat and autofix
|
|
21
|
+
$(PYTHON) -m ruff format src tests
|
|
22
|
+
$(PYTHON) -m ruff check --fix src tests
|
|
23
|
+
|
|
24
|
+
test-local: venv ## Run the test suite
|
|
25
|
+
$(PYTHON) -m pytest -v
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: edutap.observability_settings
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: Error reporting, tracing and structured logging, wired the same way in every eduTAP service
|
|
5
|
+
Project-URL: Source, https://github.com/edutap-collective/edutap.observability_settings
|
|
6
|
+
Project-URL: Issues, https://github.com/edutap-collective/edutap.observability_settings/issues
|
|
7
|
+
Author: eduTAP
|
|
8
|
+
License-Expression: EUPL-1.2
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
15
|
+
Classifier: Topic :: System :: Monitoring
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Requires-Python: >=3.13
|
|
18
|
+
Requires-Dist: edutap-data-models<0.3,>=0.2.1
|
|
19
|
+
Requires-Dist: logfire>=4.40
|
|
20
|
+
Requires-Dist: pydantic-settings>=2.4
|
|
21
|
+
Requires-Dist: pydantic>=2.8
|
|
22
|
+
Requires-Dist: sentry-sdk>=2.67
|
|
23
|
+
Requires-Dist: structlog>=25
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pdbp; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest>=8.2; extra == 'dev'
|
|
27
|
+
Requires-Dist: ruff<0.17,>=0.16; extra == 'dev'
|
|
28
|
+
Requires-Dist: ty; extra == 'dev'
|
|
29
|
+
Provides-Extra: docs
|
|
30
|
+
Requires-Dist: myst-parser; extra == 'docs'
|
|
31
|
+
Requires-Dist: sphinx>=8; extra == 'docs'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# edutap.observability_settings
|
|
35
|
+
|
|
36
|
+
Error reporting, tracing and structured logging, wired the same way in every eduTAP
|
|
37
|
+
service — and one decision written down: what a service may say about a person.
|
|
38
|
+
|
|
39
|
+
## Why this exists
|
|
40
|
+
|
|
41
|
+
**Because the options are the point, not the code.** Three lines would start Sentry.
|
|
42
|
+
Which options those three lines carry decides whether a bearer token or a person's
|
|
43
|
+
identifier leaves the process, and each of the options below was chosen against a
|
|
44
|
+
measurement rather than against a backend's recommendation. Separated from the
|
|
45
|
+
`sentry_sdk.init()` call that applies them, those measurements are worth nothing —
|
|
46
|
+
which is why this is a package and not a paragraph in a README.
|
|
47
|
+
|
|
48
|
+
**Because a person's identifier is not an opaque handle.** At a university a
|
|
49
|
+
`person_uid` resolves to a human being for far more people than hold directory
|
|
50
|
+
administration rights; at the LMU it is the LMU identifier with `@lmu.de` appended.
|
|
51
|
+
Pseudonymised it remains personal data. What a deployment decides here is not
|
|
52
|
+
*whether* it is personal data but *who may see it*.
|
|
53
|
+
|
|
54
|
+
## Usage
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from edutap.observability_settings import install_observability, person_label
|
|
58
|
+
|
|
59
|
+
install_observability(service_name="lmu_edutap_worker") # first thing in main()
|
|
60
|
+
|
|
61
|
+
log.warning("no view for person", person=person_label(uid, settings))
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Call it before the service resolves the settings it needs to run, so that a process
|
|
65
|
+
refusing to start is reported rather than silently absent. Nothing here can fail for
|
|
66
|
+
want of a value.
|
|
67
|
+
|
|
68
|
+
## Configuration
|
|
69
|
+
|
|
70
|
+
One prefix for the whole estate, `EDUTAP_`. These fields are defined by an eduTAP
|
|
71
|
+
package, and another university deploying them should not have to learn an LMU name.
|
|
72
|
+
|
|
73
|
+
| Variable | Default | Meaning |
|
|
74
|
+
|---|---|---|
|
|
75
|
+
| `EDUTAP_ENVIRONMENT` | `production` | Labels every event and every span. Unset must not masquerade as development. |
|
|
76
|
+
| `EDUTAP_TELEMETRY_ENABLED` | `true` | The deliberate off switch for tracing, metrics and log export. |
|
|
77
|
+
| `EDUTAP_LOG_LEVEL` | `INFO` | A closed set; a misspelled level fails at startup. |
|
|
78
|
+
| `EDUTAP_SENTRY_DSN` | unset | Unset means no error tracker. One project per service. |
|
|
79
|
+
| `EDUTAP_PSEUDONYM_SALT` | unset | The HMAC key behind the person pseudonym. Without it there is no pseudonym at all. |
|
|
80
|
+
| `EDUTAP_PERSON_UID_MODE` | `pseudonym` | `pseudonym` · `plain` · `omit` |
|
|
81
|
+
| `OTEL_EXPORTER_OTLP_ENDPOINT` | unset | **Not** an `EDUTAP_` field — see below. |
|
|
82
|
+
|
|
83
|
+
### Why the endpoint is not one of ours
|
|
84
|
+
|
|
85
|
+
A Sentry DSN names a **project**, so it belongs to the service and every service gets
|
|
86
|
+
its own. An OTLP endpoint names a **receiver**, normally one per host or cluster, and
|
|
87
|
+
which service sent a span rides in the resource attributes rather than in the address.
|
|
88
|
+
Every OpenTelemetry SDK already reads `OTEL_EXPORTER_OTLP_ENDPOINT` by itself; a
|
|
89
|
+
field under `EDUTAP_` would be a second name for the same value.
|
|
90
|
+
|
|
91
|
+
```{note}
|
|
92
|
+
A shared prefix means shared *names*, not shared *values*. `EDUTAP_SENTRY_DSN` differs
|
|
93
|
+
per service by being set in that service's own compose `environment:` block. Only
|
|
94
|
+
genuinely stack-wide values belong in the shared `.env`.
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### The three modes for a person
|
|
98
|
+
|
|
99
|
+
| Mode | What travels | When it is right |
|
|
100
|
+
|---|---|---|
|
|
101
|
+
| `pseudonym` | a keyed, 12-character label | The default. Correlation survives — forty errors about one person still read as one person — identification does not. |
|
|
102
|
+
| `plain` | the `person_uid` itself | Where the error tracker is read by exactly the people who may read the directory anyway. A configured decision, not an accident. |
|
|
103
|
+
| `omit` | nothing | Where the tracker is read more widely than the directory. |
|
|
104
|
+
|
|
105
|
+
`pseudonym` without `EDUTAP_PSEUDONYM_SALT` yields **nothing**, never the raw value: a
|
|
106
|
+
deployment that asked for pseudonyms and forgot the key has to lose the datum rather
|
|
107
|
+
than publish it. An empty salt counts as no salt — compose writes `${VAR:-}`, which
|
|
108
|
+
sets a variable to the empty string, and an HMAC under an empty key is a plain digest
|
|
109
|
+
of a small, enumerable value space and reversible by anyone who can hash the directory.
|
|
110
|
+
|
|
111
|
+
## What the three backends do
|
|
112
|
+
|
|
113
|
+
Sentry takes errors. An OTLP collector takes traces and metrics. structlog produces
|
|
114
|
+
the records that reach both, bridged by `logfire.StructlogProcessor`, so a log line
|
|
115
|
+
and the span it happened inside share a trace id without the caller doing anything.
|
|
116
|
+
|
|
117
|
+
Nothing travels two paths: Sentry's own tracing stays off (`traces_sample_rate=0`),
|
|
118
|
+
because the spans already go to the collector and Bugsink — the tracker this estate
|
|
119
|
+
runs — states that it does not support traces.
|
|
120
|
+
|
|
121
|
+
```{important}
|
|
122
|
+
`send_to_logfire=False` is not a detail. The library defaults it to `True`, so leaving
|
|
123
|
+
it unset would ship spans to a hosted third party the first time a token happened to
|
|
124
|
+
be present.
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
While no collector exists, the console stands in. Measured against logfire 4.40: with
|
|
128
|
+
`send_to_logfire=False` and no `OTEL_EXPORTER_OTLP_ENDPOINT`, no exporter is installed
|
|
129
|
+
at all, so an instrumented service would be indistinguishable from an uninstrumented
|
|
130
|
+
one — which is how instrumentation reaches production broken. Once the endpoint is
|
|
131
|
+
set, the console stands down.
|
|
132
|
+
|
|
133
|
+
## Development
|
|
134
|
+
|
|
135
|
+
```shell
|
|
136
|
+
make venv
|
|
137
|
+
make lint
|
|
138
|
+
make test-local
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`tox` runs the suite across every supported Python version.
|
|
142
|
+
|
|
143
|
+
## Design records
|
|
144
|
+
|
|
145
|
+
Under [`docs/superpowers/specs/`](docs/superpowers/specs/).
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# edutap.observability_settings
|
|
2
|
+
|
|
3
|
+
Error reporting, tracing and structured logging, wired the same way in every eduTAP
|
|
4
|
+
service — and one decision written down: what a service may say about a person.
|
|
5
|
+
|
|
6
|
+
## Why this exists
|
|
7
|
+
|
|
8
|
+
**Because the options are the point, not the code.** Three lines would start Sentry.
|
|
9
|
+
Which options those three lines carry decides whether a bearer token or a person's
|
|
10
|
+
identifier leaves the process, and each of the options below was chosen against a
|
|
11
|
+
measurement rather than against a backend's recommendation. Separated from the
|
|
12
|
+
`sentry_sdk.init()` call that applies them, those measurements are worth nothing —
|
|
13
|
+
which is why this is a package and not a paragraph in a README.
|
|
14
|
+
|
|
15
|
+
**Because a person's identifier is not an opaque handle.** At a university a
|
|
16
|
+
`person_uid` resolves to a human being for far more people than hold directory
|
|
17
|
+
administration rights; at the LMU it is the LMU identifier with `@lmu.de` appended.
|
|
18
|
+
Pseudonymised it remains personal data. What a deployment decides here is not
|
|
19
|
+
*whether* it is personal data but *who may see it*.
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from edutap.observability_settings import install_observability, person_label
|
|
25
|
+
|
|
26
|
+
install_observability(service_name="lmu_edutap_worker") # first thing in main()
|
|
27
|
+
|
|
28
|
+
log.warning("no view for person", person=person_label(uid, settings))
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Call it before the service resolves the settings it needs to run, so that a process
|
|
32
|
+
refusing to start is reported rather than silently absent. Nothing here can fail for
|
|
33
|
+
want of a value.
|
|
34
|
+
|
|
35
|
+
## Configuration
|
|
36
|
+
|
|
37
|
+
One prefix for the whole estate, `EDUTAP_`. These fields are defined by an eduTAP
|
|
38
|
+
package, and another university deploying them should not have to learn an LMU name.
|
|
39
|
+
|
|
40
|
+
| Variable | Default | Meaning |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| `EDUTAP_ENVIRONMENT` | `production` | Labels every event and every span. Unset must not masquerade as development. |
|
|
43
|
+
| `EDUTAP_TELEMETRY_ENABLED` | `true` | The deliberate off switch for tracing, metrics and log export. |
|
|
44
|
+
| `EDUTAP_LOG_LEVEL` | `INFO` | A closed set; a misspelled level fails at startup. |
|
|
45
|
+
| `EDUTAP_SENTRY_DSN` | unset | Unset means no error tracker. One project per service. |
|
|
46
|
+
| `EDUTAP_PSEUDONYM_SALT` | unset | The HMAC key behind the person pseudonym. Without it there is no pseudonym at all. |
|
|
47
|
+
| `EDUTAP_PERSON_UID_MODE` | `pseudonym` | `pseudonym` · `plain` · `omit` |
|
|
48
|
+
| `OTEL_EXPORTER_OTLP_ENDPOINT` | unset | **Not** an `EDUTAP_` field — see below. |
|
|
49
|
+
|
|
50
|
+
### Why the endpoint is not one of ours
|
|
51
|
+
|
|
52
|
+
A Sentry DSN names a **project**, so it belongs to the service and every service gets
|
|
53
|
+
its own. An OTLP endpoint names a **receiver**, normally one per host or cluster, and
|
|
54
|
+
which service sent a span rides in the resource attributes rather than in the address.
|
|
55
|
+
Every OpenTelemetry SDK already reads `OTEL_EXPORTER_OTLP_ENDPOINT` by itself; a
|
|
56
|
+
field under `EDUTAP_` would be a second name for the same value.
|
|
57
|
+
|
|
58
|
+
```{note}
|
|
59
|
+
A shared prefix means shared *names*, not shared *values*. `EDUTAP_SENTRY_DSN` differs
|
|
60
|
+
per service by being set in that service's own compose `environment:` block. Only
|
|
61
|
+
genuinely stack-wide values belong in the shared `.env`.
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### The three modes for a person
|
|
65
|
+
|
|
66
|
+
| Mode | What travels | When it is right |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| `pseudonym` | a keyed, 12-character label | The default. Correlation survives — forty errors about one person still read as one person — identification does not. |
|
|
69
|
+
| `plain` | the `person_uid` itself | Where the error tracker is read by exactly the people who may read the directory anyway. A configured decision, not an accident. |
|
|
70
|
+
| `omit` | nothing | Where the tracker is read more widely than the directory. |
|
|
71
|
+
|
|
72
|
+
`pseudonym` without `EDUTAP_PSEUDONYM_SALT` yields **nothing**, never the raw value: a
|
|
73
|
+
deployment that asked for pseudonyms and forgot the key has to lose the datum rather
|
|
74
|
+
than publish it. An empty salt counts as no salt — compose writes `${VAR:-}`, which
|
|
75
|
+
sets a variable to the empty string, and an HMAC under an empty key is a plain digest
|
|
76
|
+
of a small, enumerable value space and reversible by anyone who can hash the directory.
|
|
77
|
+
|
|
78
|
+
## What the three backends do
|
|
79
|
+
|
|
80
|
+
Sentry takes errors. An OTLP collector takes traces and metrics. structlog produces
|
|
81
|
+
the records that reach both, bridged by `logfire.StructlogProcessor`, so a log line
|
|
82
|
+
and the span it happened inside share a trace id without the caller doing anything.
|
|
83
|
+
|
|
84
|
+
Nothing travels two paths: Sentry's own tracing stays off (`traces_sample_rate=0`),
|
|
85
|
+
because the spans already go to the collector and Bugsink — the tracker this estate
|
|
86
|
+
runs — states that it does not support traces.
|
|
87
|
+
|
|
88
|
+
```{important}
|
|
89
|
+
`send_to_logfire=False` is not a detail. The library defaults it to `True`, so leaving
|
|
90
|
+
it unset would ship spans to a hosted third party the first time a token happened to
|
|
91
|
+
be present.
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
While no collector exists, the console stands in. Measured against logfire 4.40: with
|
|
95
|
+
`send_to_logfire=False` and no `OTEL_EXPORTER_OTLP_ENDPOINT`, no exporter is installed
|
|
96
|
+
at all, so an instrumented service would be indistinguishable from an uninstrumented
|
|
97
|
+
one — which is how instrumentation reaches production broken. Once the endpoint is
|
|
98
|
+
set, the console stands down.
|
|
99
|
+
|
|
100
|
+
## Development
|
|
101
|
+
|
|
102
|
+
```shell
|
|
103
|
+
make venv
|
|
104
|
+
make lint
|
|
105
|
+
make test-local
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`tox` runs the suite across every supported Python version.
|
|
109
|
+
|
|
110
|
+
## Design records
|
|
111
|
+
|
|
112
|
+
Under [`docs/superpowers/specs/`](docs/superpowers/specs/).
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Design records (Claude/Superpowers documents)
|
|
2
|
+
|
|
3
|
+
* **Spec** (`specs/YYYY-MM-DD-<topic>-design.md`) — the design worked out in dialogue.
|
|
4
|
+
* **Plan** (`plans/YYYY-MM-DD-<topic>.md`) — the implementation derived from it.
|
|
5
|
+
|
|
6
|
+
| Date | Topic | Spec |
|
|
7
|
+
|---|---|---|
|
|
8
|
+
| 2026-08-10 | Why this package exists, the three backends, and what may be said about a person | [`specs/2026-08-10-observability-settings-design.md`](specs/2026-08-10-observability-settings-design.md) |
|
|
9
|
+
|
|
10
|
+
Records are snapshots of a decision at a point in time. They are not rewritten to
|
|
11
|
+
match a later state; a changed decision gets a new record.
|
|
12
|
+
|
|
13
|
+
The Sentry half of the design is inherited rather than invented here: the options and
|
|
14
|
+
the measurements behind them come from `edutap.data_provider`,
|
|
15
|
+
`docs/superpowers/specs/2026-08-04-observability-design.md`.
|