omnixys-observability 2.0.4__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.
@@ -0,0 +1,2 @@
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
@@ -0,0 +1,101 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ env:
10
+ PYTHON_VERSION: "3.14"
11
+ PACKAGE: ${{ vars.PACKAGE }}
12
+
13
+ jobs:
14
+ test:
15
+ name: Test
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+
20
+ - name: Setup uv
21
+ uses: astral-sh/setup-uv@v4
22
+ with:
23
+ python-version: ${{ env.PYTHON_VERSION }}
24
+
25
+ - name: Install dependencies
26
+ run: uv sync
27
+
28
+ - name: Lint (ruff)
29
+ run: uv run ruff check src/
30
+
31
+ - name: Typecheck (mypy)
32
+ run: uv run mypy src/ || true
33
+
34
+ - name: Test (pytest)
35
+ run: uv run pytest
36
+
37
+ release:
38
+ needs: test
39
+ runs-on: ubuntu-latest
40
+ permissions:
41
+ contents: write
42
+ issues: write
43
+ pull-requests: write
44
+ outputs:
45
+ released: ${{ steps.semantic.outputs.released }}
46
+ version: ${{ steps.semantic.outputs.version }}
47
+ steps:
48
+ - uses: actions/checkout@v6
49
+ with:
50
+ fetch-depth: 0
51
+
52
+ - name: Setup Node.js
53
+ uses: actions/setup-node@v4
54
+ with:
55
+ node-version: "22"
56
+
57
+ - name: Install semantic-release
58
+ run: |
59
+ npm install -g semantic-release
60
+ npm install -g @semantic-release/changelog
61
+ npm install -g @semantic-release/exec
62
+ npm install -g @semantic-release/git
63
+ npm install -g @semantic-release/github
64
+ npm install -g conventional-changelog-conventionalcommits
65
+
66
+ - name: Semantic Release
67
+ id: semantic
68
+ run: |
69
+ OUTPUT=$(npx semantic-release --no-ci 2>&1 || true)
70
+ echo "$OUTPUT"
71
+
72
+ if echo "$OUTPUT" | grep -q "Published"; then
73
+ echo "released=true" >> $GITHUB_OUTPUT
74
+ else
75
+ echo "released=false" >> $GITHUB_OUTPUT
76
+ fi
77
+
78
+ VERSION=$(grep -oP '__version__\s*=\s*"\K[^"]+' src/${PACKAGE}/__init__.py)
79
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
80
+ env:
81
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
82
+
83
+ publish:
84
+ needs: release
85
+ if: needs.release.outputs.released == 'true'
86
+ runs-on: ubuntu-latest
87
+ steps:
88
+ - uses: actions/checkout@v4
89
+
90
+ - name: Setup uv
91
+ uses: astral-sh/setup-uv@v4
92
+ with:
93
+ python-version: ${{ env.PYTHON_VERSION }}
94
+
95
+ - name: Build package
96
+ run: uv build
97
+
98
+ - name: Publish to PyPI
99
+ run: uv publish dist/*
100
+ env:
101
+ UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
@@ -0,0 +1,2 @@
1
+ __pycache__/
2
+ *.pyc
@@ -0,0 +1,42 @@
1
+ ## [2.0.4](https://github.com/omnixys/observability-python/compare/v2.0.3...v2.0.4) (2026-07-22)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **publish:** add uv build before uv publish ([d5aa1fa](https://github.com/omnixys/observability-python/commit/d5aa1fa58a97432ebd70926451eb74a6916333ee))
7
+
8
+ ## [2.0.3](https://github.com/omnixys/observability-python/compare/v2.0.2...v2.0.3) (2026-07-22)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **publish:** replace gh release upload with uv publish to PyPI ([420ef67](https://github.com/omnixys/observability-python/commit/420ef6724596d9fb311e7af3c1a0d17d7b151d2a))
14
+
15
+ ## [2.0.2](https://github.com/omnixys/observability-python/compare/v2.0.1...v2.0.2) (2026-07-22)
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * **release:** add @semantic-release/exec to update __version__ in __init__.py ([969ed6f](https://github.com/omnixys/observability-python/commit/969ed6f6b6725cd611b6f73aad36242d24c6b97c))
21
+
22
+ ## [2.0.1](https://github.com/omnixys/observability-python/compare/v2.0.0...v2.0.1) (2026-07-22)
23
+
24
+
25
+ ### Bug Fixes
26
+
27
+ * **cicd:** use version comparison for release detection ([eab84dd](https://github.com/omnixys/observability-python/commit/eab84ddbc0ff4a8563537204fdcba19058b8b7af))
28
+
29
+ # [2.0.0](https://github.com/omnixys/observability-python/compare/v1.1.1...v2.0.0) (2026-07-22)
30
+
31
+ # Changelog
32
+
33
+ All notable changes in this project will be documented in this file.
34
+
35
+
36
+ ## [1.1.1](https://github.com/omnixys/observability-python/compare/v1.1.0...v1.1.1) (2026-07-22)
37
+
38
+ ## [1.0.2](https://github.com/omnixys/observability-python/compare/v1.0.1...v1.0.2) (2026-07-22)
39
+
40
+ ## [1.0.1](https://github.com/omnixys/observability-python/compare/v1.0.0...v1.0.1) (2026-07-15)
41
+
42
+ ## 1.0.0 (2026-07-15)
@@ -0,0 +1,57 @@
1
+ Metadata-Version: 2.4
2
+ Name: omnixys-observability
3
+ Version: 2.0.4
4
+ Summary: Omnixys shared observability package (OTel tracing, structured logging, Prometheus metrics)
5
+ Project-URL: Homepage, https://github.com/omnixys/omnixys
6
+ Project-URL: Repository, https://github.com/omnixys/omnixys
7
+ Author-email: Omnixys Team <team@omnixys.com>
8
+ License: GPL-3.0-or-later
9
+ Keywords: logging,metrics,observability,omnixys,opentelemetry,tracing
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
13
+ Classifier: Programming Language :: Python :: 3.14
14
+ Classifier: Typing :: Typed
15
+ Requires-Python: >=3.14
16
+ Requires-Dist: fastapi>=0.115.0
17
+ Requires-Dist: httpx>=0.28.0
18
+ Requires-Dist: opentelemetry-api>=1.42.1
19
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.42.1
20
+ Requires-Dist: opentelemetry-instrumentation-asgi>=0.63b1
21
+ Requires-Dist: opentelemetry-instrumentation-fastapi>=0.63b1
22
+ Requires-Dist: opentelemetry-instrumentation-httpx>=0.63b1
23
+ Requires-Dist: opentelemetry-instrumentation-logging>=0.63b1
24
+ Requires-Dist: opentelemetry-instrumentation-sqlalchemy>=0.63b1
25
+ Requires-Dist: opentelemetry-instrumentation>=0.63b1
26
+ Requires-Dist: opentelemetry-sdk>=1.42.1
27
+ Requires-Dist: prometheus-client>=0.25.0
28
+ Requires-Dist: structlog>=26.1.0
29
+ Description-Content-Type: text/markdown
30
+
31
+ # omnixys-observability
32
+
33
+ Omnixys shared observability package with OpenTelemetry tracing, structured logging, and Prometheus metrics.
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ pip install omnixys-observability
39
+ ```
40
+
41
+ ## Features
42
+
43
+ - OpenTelemetry distributed tracing
44
+ - Structured logging with structlog
45
+ - Prometheus metrics collection
46
+ - FastAPI instrumentation
47
+ - Request context propagation
48
+
49
+ ## Usage
50
+
51
+ ```python
52
+ from observability import configure_logging, configure_tracing, instrument_fastapi
53
+ ```
54
+
55
+ ## License
56
+
57
+ GPL-3.0-or-later
@@ -0,0 +1,27 @@
1
+ # omnixys-observability
2
+
3
+ Omnixys shared observability package with OpenTelemetry tracing, structured logging, and Prometheus metrics.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install omnixys-observability
9
+ ```
10
+
11
+ ## Features
12
+
13
+ - OpenTelemetry distributed tracing
14
+ - Structured logging with structlog
15
+ - Prometheus metrics collection
16
+ - FastAPI instrumentation
17
+ - Request context propagation
18
+
19
+ ## Usage
20
+
21
+ ```python
22
+ from observability import configure_logging, configure_tracing, instrument_fastapi
23
+ ```
24
+
25
+ ## License
26
+
27
+ GPL-3.0-or-later
@@ -0,0 +1,76 @@
1
+ [project]
2
+ name = "omnixys-observability"
3
+ dynamic = ["version"]
4
+ description = "Omnixys shared observability package (OTel tracing, structured logging, Prometheus metrics)"
5
+ readme = "README.md"
6
+ license = { text = "GPL-3.0-or-later" }
7
+ requires-python = ">=3.14"
8
+ authors = [{ name = "Omnixys Team", email = "team@omnixys.com" }]
9
+ keywords = ["omnixys", "observability", "opentelemetry", "tracing", "metrics", "logging"]
10
+ classifiers = [
11
+ "Development Status :: 4 - Beta",
12
+ "Intended Audience :: Developers",
13
+ "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
14
+ "Programming Language :: Python :: 3.14",
15
+ "Typing :: Typed",
16
+ ]
17
+ dependencies = [
18
+ "fastapi>=0.115.0",
19
+ "httpx>=0.28.0",
20
+ "structlog>=26.1.0",
21
+ "prometheus-client>=0.25.0",
22
+ "opentelemetry-api>=1.42.1",
23
+ "opentelemetry-sdk>=1.42.1",
24
+ "opentelemetry-exporter-otlp-proto-http>=1.42.1",
25
+ "opentelemetry-instrumentation>=0.63b1",
26
+ "opentelemetry-instrumentation-fastapi>=0.63b1",
27
+ "opentelemetry-instrumentation-sqlalchemy>=0.63b1",
28
+ "opentelemetry-instrumentation-logging>=0.63b1",
29
+ "opentelemetry-instrumentation-httpx>=0.63b1",
30
+ "opentelemetry-instrumentation-asgi>=0.63b1",
31
+ ]
32
+
33
+ [project.urls]
34
+ Homepage = "https://github.com/omnixys/omnixys"
35
+ Repository = "https://github.com/omnixys/omnixys"
36
+
37
+ [build-system]
38
+ requires = ["hatchling"]
39
+ build-backend = "hatchling.build"
40
+
41
+ [tool.hatch.version]
42
+ path = "src/observability/__init__.py"
43
+
44
+ [tool.hatch.build.targets.wheel]
45
+ packages = ["src/observability"]
46
+
47
+ [dependency-groups]
48
+ dev = [
49
+ "mypy>=2.3.0",
50
+ "ruff>=0.15.21",
51
+ "pytest>=9.1.0",
52
+ "pytest-asyncio>=0.26.0",
53
+ ]
54
+
55
+ [tool.ruff]
56
+ line-length = 120
57
+ target-version = "py314"
58
+
59
+ [tool.ruff.lint]
60
+ select = ["ALL"]
61
+
62
+ [tool.ruff.lint.per-file-ignores]
63
+ "tests/**" = [
64
+ "ANN101", "ANN401", "S101",
65
+ "T20", "FBT", "D", "INP001",
66
+ ]
67
+ "src/**" = [
68
+ "S104", "D", "ANN401", "ARG002",
69
+ ]
70
+
71
+ [tool.mypy]
72
+ strict = true
73
+
74
+ [tool.pytest.ini_options]
75
+ testpaths = ["tests"]
76
+ pythonpath = ["src"]
@@ -0,0 +1,56 @@
1
+ /**
2
+ * @license GPL-3.0-or-later
3
+ * Copyright (C) 2025 Caleb Gyamfi
4
+ * Omnixys Technologies
5
+ *
6
+ * This program is free software: you can redistribute it and/or modify
7
+ * it under the terms of the GNU General Public License as published by
8
+ * the Free Software Foundation, either version 3 of the License, or
9
+ * (at your option) any later version.
10
+ *
11
+ * This program is distributed in the hope that it will be useful,
12
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
14
+ * See the GNU General Public License for more details.
15
+ *
16
+ * For more information, visit <https://www.gnu.org/licenses/>.
17
+ */
18
+
19
+ export default {
20
+ branches: ['main'],
21
+ tagFormat: 'v${version}',
22
+ plugins: [
23
+ ['@semantic-release/commit-analyzer', {
24
+ preset: 'conventionalcommits',
25
+ releaseRules: [
26
+ { type: 'breaking', release: 'major' },
27
+ { type: 'feat', release: 'minor' },
28
+ { type: 'fix', release: 'patch' },
29
+ { type: 'perf', release: 'patch' },
30
+ { type: 'refactor', release: 'patch' },
31
+ { type: 'revert', release: 'patch' },
32
+ { type: 'docs', release: false },
33
+ { type: 'style', release: false },
34
+ { type: 'test', release: false },
35
+ { type: 'chore', release: false },
36
+ { type: 'ci', release: false },
37
+ { type: 'build', release: false },
38
+ ],
39
+ }],
40
+ '@semantic-release/release-notes-generator',
41
+ ['@semantic-release/changelog', { changelogFile: 'CHANGELOG.md' }],
42
+ ['@semantic-release/exec', {
43
+ prepareCmd: 'sed -i "s/__version__ = \\".*\\"/__version__ = \\"${nextRelease.version}\\"/" src/observability/__init__.py',
44
+ }],
45
+ ['@semantic-release/git', {
46
+ assets: ['pyproject.toml', 'CHANGELOG.md', 'src/observability/__init__.py'],
47
+ message: 'chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}',
48
+ }],
49
+ ['@semantic-release/github', {
50
+ assets: [
51
+ { path: 'CHANGELOG.md', label: 'Changelog' },
52
+ { path: 'dist/**', label: 'Build Artifacts' },
53
+ ],
54
+ }],
55
+ ],
56
+ };
@@ -0,0 +1,61 @@
1
+ from observability.logging import configure_logging, get_logger, shutdown_logging
2
+ from observability.metrics import (
3
+ ObservabilityMetrics,
4
+ ObservabilityMiddleware,
5
+ auth_failures,
6
+ cache_hits,
7
+ cache_misses,
8
+ db_query_duration,
9
+ graphql_operations,
10
+ http_request_duration,
11
+ http_requests_total,
12
+ kafka_publish_failures,
13
+ kafka_publish_total,
14
+ outbox_failed,
15
+ outbox_pending,
16
+ outbox_processed,
17
+ outbox_processing_duration,
18
+ rate_limit_blocked,
19
+ )
20
+ from observability.request_context import (
21
+ RequestContext,
22
+ current_request_context,
23
+ reset_request_context,
24
+ set_request_context,
25
+ )
26
+ from observability.runtime import configure_observability, shutdown_observability
27
+ from observability.tracing import configure_tracing, instrument_fastapi, shutdown_tracing, uninstrument_fastapi
28
+
29
+ __version__ = "2.0.4"
30
+
31
+ __all__ = [
32
+ "ObservabilityMetrics",
33
+ "ObservabilityMiddleware",
34
+ "RequestContext",
35
+ "auth_failures",
36
+ "cache_hits",
37
+ "cache_misses",
38
+ "configure_logging",
39
+ "configure_observability",
40
+ "configure_tracing",
41
+ "current_request_context",
42
+ "db_query_duration",
43
+ "get_logger",
44
+ "graphql_operations",
45
+ "http_request_duration",
46
+ "http_requests_total",
47
+ "instrument_fastapi",
48
+ "kafka_publish_failures",
49
+ "kafka_publish_total",
50
+ "outbox_failed",
51
+ "outbox_pending",
52
+ "outbox_processed",
53
+ "outbox_processing_duration",
54
+ "rate_limit_blocked",
55
+ "reset_request_context",
56
+ "set_request_context",
57
+ "shutdown_logging",
58
+ "shutdown_observability",
59
+ "shutdown_tracing",
60
+ "uninstrument_fastapi",
61
+ ]
@@ -0,0 +1,150 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import os
5
+ import re
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ import structlog
9
+ from opentelemetry import trace
10
+ from opentelemetry._logs import set_logger_provider
11
+ from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
12
+ from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
13
+ from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
14
+ from opentelemetry.sdk.resources import Resource
15
+
16
+ from observability.request_context import current_request_context
17
+
18
+ if TYPE_CHECKING:
19
+ from collections.abc import MutableMapping
20
+
21
+ _SENSITIVE_KEY = re.compile(r"authorization|cookie|password|secret|token|api[-_]?key", re.IGNORECASE)
22
+
23
+
24
+ def _add_context(_logger: Any, _method_name: str, event_dict: MutableMapping[str, Any]) -> MutableMapping[str, Any]: # noqa: C901
25
+ if _service_name:
26
+ event_dict["service"] = _service_name
27
+ ctx = current_request_context()
28
+ if ctx:
29
+ if ctx.request_id:
30
+ event_dict["request_id"] = ctx.request_id
31
+ if ctx.correlation_id:
32
+ event_dict["correlation_id"] = ctx.correlation_id
33
+ if ctx.user_id:
34
+ event_dict["user_id"] = ctx.user_id
35
+ event_dict["actor_id"] = ctx.user_id
36
+ if ctx.organization_id:
37
+ event_dict["organization_id"] = ctx.organization_id
38
+ if ctx.tenant_id:
39
+ event_dict["tenant_id"] = ctx.tenant_id
40
+
41
+ span = trace.get_current_span()
42
+ span_context = span.get_span_context()
43
+ if span_context.is_valid:
44
+ event_dict["traceId"] = format(span_context.trace_id, "032x")
45
+ event_dict["spanId"] = format(span_context.span_id, "016x")
46
+ elif ctx:
47
+ if ctx.trace_id:
48
+ event_dict["traceId"] = ctx.trace_id
49
+ if ctx.span_id:
50
+ event_dict["spanId"] = ctx.span_id
51
+
52
+ return event_dict
53
+
54
+
55
+ def _redact_sensitive(
56
+ _logger: Any, _method_name: str, event_dict: MutableMapping[str, Any],
57
+ ) -> MutableMapping[str, Any]:
58
+ return _redact_mapping(event_dict)
59
+
60
+
61
+ def _redact_mapping(value: MutableMapping[str, Any]) -> MutableMapping[str, Any]:
62
+ for key, nested in tuple(value.items()):
63
+ if _SENSITIVE_KEY.search(key):
64
+ value[key] = "[REDACTED]"
65
+ elif isinstance(nested, dict):
66
+ value[key] = _redact_mapping(nested)
67
+ elif isinstance(nested, list):
68
+ value[key] = [
69
+ _redact_mapping(item) if isinstance(item, dict) else item for item in nested
70
+ ]
71
+ return value
72
+
73
+
74
+ _logger_provider: LoggerProvider | None = None
75
+ _service_name: str | None = None
76
+
77
+
78
+ def _signal_endpoint(endpoint: str, signal: str) -> str:
79
+ base = endpoint.rstrip("/")
80
+ for suffix in ("/v1/traces", "/v1/logs"):
81
+ if base.endswith(suffix):
82
+ base = base[: -len(suffix)]
83
+ break
84
+ return f"{base}/v1/{signal}"
85
+
86
+
87
+ def _setup_otel_logging(service_name: str, endpoint: str, environment: str) -> LoggerProvider:
88
+ global _logger_provider, _service_name # noqa: PLW0603
89
+ resource = Resource.create(
90
+ {
91
+ "service.name": service_name,
92
+ "service.namespace": "omnixys",
93
+ "service.version": os.environ.get("OTEL_SERVICE_VERSION", "unknown"),
94
+ "deployment.environment.name": environment,
95
+ },
96
+ )
97
+
98
+ logger_provider = LoggerProvider(resource=resource)
99
+ logger_provider.add_log_record_processor(
100
+ BatchLogRecordProcessor(OTLPLogExporter(endpoint=_signal_endpoint(endpoint, "logs"))),
101
+ )
102
+ set_logger_provider(logger_provider)
103
+
104
+ handler = LoggingHandler(logger_provider=logger_provider)
105
+ handler.setLevel(logging.DEBUG)
106
+ logging.getLogger().addHandler(handler)
107
+ _logger_provider = logger_provider
108
+ _service_name = service_name
109
+ return logger_provider
110
+
111
+
112
+ def configure_logging(
113
+ log_level: str = "INFO",
114
+ service_name: str | None = None,
115
+ *,
116
+ otlp_endpoint: str | None = None,
117
+ environment: str = "local",
118
+ ) -> None:
119
+ if service_name:
120
+ endpoint = otlp_endpoint or os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318")
121
+ _setup_otel_logging(service_name, endpoint, environment)
122
+
123
+ structlog.configure(
124
+ processors=[
125
+ structlog.contextvars.merge_contextvars,
126
+ structlog.stdlib.add_log_level,
127
+ _add_context,
128
+ _redact_sensitive,
129
+ structlog.processors.TimeStamper(fmt="iso", utc=True),
130
+ structlog.processors.JSONRenderer(),
131
+ ],
132
+ wrapper_class=structlog.stdlib.BoundLogger,
133
+ logger_factory=structlog.stdlib.LoggerFactory(),
134
+ cache_logger_on_first_use=True,
135
+ )
136
+ logging.basicConfig(
137
+ level=getattr(logging, log_level.upper(), logging.INFO),
138
+ format="%(message)s",
139
+ )
140
+
141
+
142
+ def get_logger(name: str | None = None) -> Any:
143
+ return structlog.get_logger(name)
144
+
145
+
146
+ def shutdown_logging() -> None:
147
+ global _logger_provider # noqa: PLW0603
148
+ if _logger_provider is not None:
149
+ _logger_provider.shutdown()
150
+ _logger_provider = None