Metadata-Version: 2.5
Name: elven-unified-observability-py
Version: 0.1.23
Summary: Unified wrapper for Elven logs interception and OpenTelemetry instrumentation in Python.
Project-URL: Homepage, https://github.com/elven-observability/elven-unified-observability-py
Project-URL: Repository, https://github.com/elven-observability/elven-unified-observability-py
Author: Leonardo Zwirtes
License-Expression: MIT
Keywords: logs,observability,opentelemetry,python,telemetry
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries
Classifier: Topic :: System :: Monitoring
Requires-Python: >=3.10
Requires-Dist: elven-logs-interceptor-python<0.2.0,>=0.1.18
Requires-Dist: elven-opentelemetry-instrumentation-py<0.2.0,>=0.1.6
Requires-Dist: python-dotenv<2.0.0,>=1.0.0
Provides-Extra: dev
Requires-Dist: build>=1.2.2; extra == 'dev'
Requires-Dist: fastapi>=0.115.0; extra == 'dev'
Requires-Dist: httpx<0.29.0,>=0.28.0; extra == 'dev'
Requires-Dist: pyright>=1.1.408; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
Requires-Dist: pytest-cov>=5.0.0; extra == 'dev'
Requires-Dist: pytest>=8.3.0; extra == 'dev'
Requires-Dist: ruff>=0.6.0; extra == 'dev'
Requires-Dist: twine>=5.1.1; extra == 'dev'
Provides-Extra: full
Requires-Dist: elven-logs-interceptor-python[loguru,otel,perf,structlog]<0.2.0,>=0.1.18; extra == 'full'
Description-Content-Type: text/markdown

# Unified Observability Py

## Release 0.1.23

This release requires Elven instrumentation >=0.1.6 and logs >=0.1.18, moving
the OpenTelemetry pipeline to the patched 1.45.1 / 0.66b1 release family for
[CVE-2026-114603](https://github.com/open-telemetry/opentelemetry-python/security/advisories/GHSA-xcq7-p3pf-p88h).
It removes the dependency conflict with FastAPI 0.142+ without requiring
applications on earlier FastAPI versions to upgrade their framework.

The compatibility matrix covers FastAPI 0.115.0, 0.141.1, 0.142.0, 0.142.2,
and 0.162.0, including manual and CLI bootstrap with real HTTP export of
traces, metrics, and logs. The Unified configuration and public APIs are
unchanged. Initialize before constructing the app. Elven owns exporter setup;
do not also opt into FastAPI automatic exporter configuration.

Regenerate application lockfiles when upgrading. Explicit pins to vulnerable
OpenTelemetry versions must be updated; bypassing dependency resolution is not
a supported migration path.

Pacote unificado para juntar:

- `elven-logs-interceptor-python`
- `elven-opentelemetry-instrumentation-py`

com uma experiência única de bootstrap, config por env e API manual em Python.

## Instalação

Base:

```bash
pip install elven-unified-observability-py
```

Com extras de performance/logging, sem forçar frameworks da aplicação:

```bash
pip install "elven-unified-observability-py[full]"
```

O extra `full` não instala nem atualiza `fastapi`, `django`, `flask` ou `celery`.
Mantenha esses frameworks versionados no `requirements` da própria aplicação.

Para desenvolvimento:

```bash
pip install -e ".[dev]"
```

## O que a lib entrega

- `init(...)` manual com config Python-first
- `init_from_env()` para bootstrap por ambiente
- `logger`, `tracer` e `metrics` reexportados como proxies seguros
- preload zero-code via `sitecustomize`
- CLI `elven-unified-observability -- <command>`
- CLI usa `exec`, então não fica um daemon/wrapper pai rodando no container
- shutdown e flush coordenados
- auto-bridge para libs de logging suportadas (`loguru`, `structlog`)
- fallback `LOKI_*` para `LOGS_*`
- aliases legados `ENABLE_LOGGING`, `ENABLE_METRICS`, `ENABLE_TRACING`

## Quick Start Manual

```python
from elven_unified_observability import init, logger, metrics, tracer

handle = init(
    {
        "service": {
            "serviceName": "billing-service",
            "serviceVersion": "1.2.3",
            "environment": "production",
        },
        "logging": {
            "transport": {
                "url": "https://loki.example.com/loki/api/v1/push",
                "tenantId": "tenant-a",
                "authToken": "token",
                "compression": "gzip",
            },
            "interceptConsole": True,
        },
        "telemetry": {
            "metrics": {"exportIntervalMillis": 15000},
            "privacy": {"redactDbStatement": True},
        },
        "enableLogging": True,
        "enableMetrics": True,
        "enableTracing": True,
    }
)

logger.info("service started", {"service": "billing-service"})
metrics.increment("bootstrap.success", 1)
tracer.with_span("startup", lambda span: span.set_attribute("boot.ready", True))

handle.force_flush()
handle.shutdown()
```

O `logger` também aceita padrões comuns do `logging` nativo durante migração:

```python
logger.info("payload criado", {"request_id": "req-1"})
logger.info("payload criado: %s", payload)
logger.info("payload criado: %(id)s", {"id": payload_id})
logger.info("payload criado: {}", payload_id)
logger.info("payload criado: %s", payload_id, extra={"request_id": "req-1"})
logger.exception("falha ao criar payload: %s", payload_id)
```

Quando a wrapper intercepta o `logging` padrão do Python, `extra={...}` é
capturado em `context.extra`:

```python
logging.getLogger(__name__).info(
    "payload criado",
    extra={"payload": payload},
)
```

Aliases suportados: `warning(...)`, `exception(...)` e `critical(...)`.

Campos sensíveis como `cpf`, `cnpj`, `cpfCnpj`, `documento`,
`numeroDocumento`, `taxId`, `apiKey`, `clientSecret`, `senha`, `password`,
`token`, `authorization`, `cartao/cartão`, `chavePix` e campos relacionados a
credenciais são mascarados por padrão. Em mensagens, a lib mascara apenas o
fragmento/valor sensível e mantém o restante do log. Em `extra`/contexto
estruturado, chaves sensíveis mascaram o valor inteiro. O matcher normaliza
variações comuns em `snake_case`, `camelCase`, `kebab-case`, nomes com ponto e
acentos. Para desligar, use `LOGS_FILTER_SANITIZE=false`,
`LOGS_SANITIZE=false` ou `UO_LOGS_SANITIZE=false` apenas quando o envio do dado
sensível em texto puro for realmente necessário e aprovado.

## Quick Start Zero-Code

1. Crie um `.env` com a identidade do serviço e os endpoints.
2. Rode a app com a CLI:

```bash
elven-unified-observability -- python app.py
```

Também funciona com:

```bash
elven-unified-observability -- uvicorn app:app
elven-unified-observability -- celery -A worker.app worker
elven-unified-observability -- python -m my_service
```

## Como a precedence funciona

1. Config explícita passada para `init(...)`
2. Env da wrapper (`UO_*`)
3. Env dos componentes (`LOGS_*`, `LOKI_*`, `OTEL_*`)
4. Defaults das libs-base

## Variáveis da wrapper

| Variável | Descrição |
| --- | --- |
| `UO_ENABLED` | Liga/desliga tudo por default |
| `UO_ENABLE_LOGGING` | Override unificado para logs |
| `UO_ENABLE_METRICS` | Override unificado para métricas |
| `UO_ENABLE_TRACING` | Override unificado para tracing |
| `UO_STRICT` | Falha o bootstrap se qualquer componente falhar |
| `UO_LOAD_DOTENV` | Controla leitura de `.env` no preload/CLI |
| `UO_DOTENV_PATH` | Caminho alternativo para o `.env` |
| `UO_LOGGING_INTEGRATIONS` | Auto-instala bridges de logging: `auto`, `logging`, `loguru`, `structlog`, `none` |
| `UO_LOGGING_EXCLUDE_PREFIXES` | Prefixos globais de bibliotecas barulhentas para ignorar nos bridges |
| `UO_ENABLE_PYTHON_LOGGING_BRIDGE` | Override específico para bridge agressivo do `logging` padrão |
| `UO_PYTHON_LOGGING_EXCLUDE_PREFIXES` | Prefixos extras para ignorar especificamente no bridge de `logging` |
| `UO_ENABLE_LOGURU_BRIDGE` | Override específico para bridge de `loguru` |
| `UO_ENABLE_STRUCTLOG_BRIDGE` | Override específico para bridge de `structlog` |
| `UO_LOGURU_LEVEL` | Nível do sink `loguru` instalado pela wrapper |
| `UO_LOGURU_ENQUEUE` | Ativa `enqueue` no sink `loguru` |
| `UO_LOGURU_BACKTRACE` | Ativa `backtrace` no sink `loguru` |
| `UO_LOGURU_DIAGNOSE` | Ativa `diagnose` no sink `loguru` |
| `UO_LOGURU_EXCLUDE_PREFIXES` | Prefixos extras para ignorar especificamente no bridge de `loguru` |
| `UO_TRACING_MAX_SPANS_PER_TRACE` | Máximo de spans gravados por trace neste processo. Desligado quando ausente |
| `UO_TRACING_RATIO` | Ratio do `SmartSampler`. Quando definido, tem precedência sobre `OTEL_TRACES_SAMPLER` |

Aliases legados suportados:

- `ENABLE_LOGGING`
- `ENABLE_METRICS`
- `ENABLE_TRACING`

## Identidade unificada

A identidade do serviço é sempre derivada nesta ordem:

1. `OTEL_SERVICE_NAME`
2. `LOGS_APP_NAME` ou `LOKI_APP_NAME`
3. default `unknown-service`

Versão:

1. `OTEL_SERVICE_VERSION`
2. `LOGS_APP_VERSION` ou `LOKI_APP_VERSION`
3. default `1.0.0`

Ambiente:

1. `OTEL_DEPLOYMENT_ENVIRONMENT`
2. `LOGS_ENVIRONMENT` ou `LOKI_ENVIRONMENT`
3. `ENVIRONMENT`
4. `APP_ENV`
5. default `development`

## Logging

Prefixo principal: `LOGS_*`

Fallback suportado: `LOKI_*`

Exemplos mais usados:

```bash
LOGS_URL=https://loki.example.com/loki/api/v1/push
LOGS_TENANT=my-tenant
LOGS_TOKEN=secret
LOGS_COMPRESSION=gzip
LOGS_ENABLE_STRUCTURED_METADATA=false
LOGS_BUFFER_MAX_SIZE=500
LOGS_FILTER_LEVELS=info,warn,error
LOGS_INTERCEPT_CONSOLE=true
LOGS_ENABLE_METRICS=true
```

Modo recomendado para clientes com OpenTelemetry Collector:

```bash
LOGS_EXPORTER=otlp
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=http://collector:4318/v1/logs
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_TRACES_EXPORTER=otlp
OTEL_METRICS_EXPORTER=otlp
```

Com `LOGS_EXPORTER=otlp`, a wrapper envia logs para
`OTEL_EXPORTER_OTLP_LOGS_ENDPOINT`. Se o endpoint explícito de logs não existir,
a wrapper deriva `/v1/logs` de `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`,
`OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` ou `OTEL_EXPORTER_OTLP_ENDPOINT`.
Nesse modo, `LOGS_URL`, `LOKI_URL`, `LOGS_TENANT` e `LOGS_TOKEN` deixam de ser
obrigatórios pela lib. `LOGS_URL` é ignorado de propósito em collector mode para
evitar enviar payload OTLP para um endpoint Loki direto. Qualquer tenant ou
autenticação deve ficar nos headers do collector/gateway.

`LOGS_ENABLE_METRICS` controla apenas as métricas internas do logger. Se ele não estiver definido, o pacote usa o valor unificado de `UO_ENABLE_METRICS`.

`LOGS_ENABLE_STRUCTURED_METADATA` fica `false` por default para compatibilidade com Loki que não permite structured metadata. Habilite apenas quando o tenant/cluster aceitar `limits_config.allow_structured_metadata`.

## Logging Bridges

Quando a aplicação usa bibliotecas de logging além do `logging` padrão do Python, a wrapper pode instalar bridges automaticamente no bootstrap.

Hoje os bridges suportados são:

- `loguru`
- `logging` padrão do Python
- `structlog`

No caso de `loguru`, a wrapper também reaplica o bridge automaticamente quando a aplicação chama `logger.configure(...)` ou `logger.remove()`, evitando perder o sink da observabilidade durante o bootstrap da app.

No caso de `logging`, a wrapper captura diretamente os `LogRecord` criados pelo
Python, inclusive quando a aplicação usa `logging.getLogger(__name__)`, troca
handlers, ou define `propagate=False`.

Habilitar autodetecção para todas as bibliotecas suportadas:

```bash
UO_LOGGING_INTEGRATIONS=auto
```

Filtros padrão já silenciam bibliotecas barulhentas como `httpx`, `httpcore`, `urllib3`, `h11`, `h2`, `hpack` e `opentelemetry`.

Adicionar/forçar `loguru` explicitamente:

```bash
UO_LOGGING_INTEGRATIONS=loguru
```

Desligar o bridge agressivo do `logging` padrão, se realmente necessário:

```bash
UO_ENABLE_PYTHON_LOGGING_BRIDGE=false
```

Forçar override granular:

```bash
UO_ENABLE_LOGURU_BRIDGE=true
UO_ENABLE_STRUCTLOG_BRIDGE=false
```

Refinar exclusões globais:

```bash
UO_LOGGING_EXCLUDE_PREFIXES=httpx,httpcore,urllib3,h11,h2,hpack,opentelemetry
```

Adicionar exclusões extras só para `loguru`:

```bash
UO_LOGURU_EXCLUDE_PREFIXES=botocore,boto3
```

Para apps `loguru-first`, a recomendação é manter o bridge ligado e usar `LOGS_INTERCEPT_CONSOLE=false` quando você não quiser capturar `print()` da aplicação.

## Telemetry

A wrapper reaproveita os `OTEL_*` já suportados pela lib-base.

Mais comuns:

```bash
OTEL_SERVICE_NAME=billing-service
OTEL_SERVICE_VERSION=1.2.3
OTEL_DEPLOYMENT_ENVIRONMENT=production
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_TRACES_EXPORTER=otlp
OTEL_METRICS_EXPORTER=otlp
OTEL_METRIC_EXPORT_INTERVAL=15000
OTEL_PRIVACY_REDACT_DB_STATEMENT=true
OTEL_INSTR_FASTAPI=true
OTEL_INSTR_REQUESTS=true
```

## Sampling e limite de spans por trace

### Samplers padrão

`OTEL_TRACES_SAMPLER` e `OTEL_TRACES_SAMPLER_ARG` são respeitados e chegam ao
`TracerProvider` antes de qualquer tracer ser criado, inclusive no modo zero-code. Valores
suportados: `always_on`, `always_off`, `traceidratio`, `parentbased_always_on`,
`parentbased_always_off`, `parentbased_traceidratio`.

```bash
OTEL_TRACES_SAMPLER=parentbased_traceidratio
OTEL_TRACES_SAMPLER_ARG=0.10
```

Precedência de sampling:

1. `sampler` explícito em `telemetry.tracing.sampler`
2. `UO_TRACING_RATIO` ou `telemetry.tracing.ratio` / `rules`
3. `OTEL_TRACES_SAMPLER` + `OTEL_TRACES_SAMPLER_ARG`
4. Default compatível (`ParentBased(SmartSampler(ratio=1.0))`)

`UO_TRACING_RATIO` fica **sem valor** por default justamente para que os samplers padrão
possam decidir. Defini-lo desliga o uso de `OTEL_TRACES_SAMPLER`.

### Limite de spans por trace

`UO_TRACING_MAX_SPANS_PER_TRACE` limita quantos spans **gravados** um único trace pode
iniciar neste processo. O alias de componente é `ELVEN_OTEL_MAX_SPANS_PER_TRACE`.

```bash
UO_TRACING_MAX_SPANS_PER_TRACE=500
```

Precedência: config explícita > `UO_TRACING_MAX_SPANS_PER_TRACE` >
`ELVEN_OTEL_MAX_SPANS_PER_TRACE` > desligado. **O default é desligado**, para não truncar
traces de ninguém silenciosamente. Valores não positivos ou inválidos mantêm o guardrail
desligado.

O limitador envolve o sampler efetivo no topo, então vale para **todos** os spans,
inclusive filhos de um pai já amostrado. A decisão do sampler delegado é avaliada primeiro
e nunca é promovida: só spans que seriam gravados consomem orçamento, e um trace não
amostrado continua não amostrado. O span raiz conta dentro do limite — com `500`, o raiz
mais 499 filhos são gravados e o restante do trace é descartado. Spans descartados
preservam o `tracestate` do pai.

> **O limite é por processo, não distribuído.** Cada worker Gunicorn/Uvicorn e cada serviço
> no caminho da requisição tem seu próprio contador.

> **Não é a correção arquitetural.** Processamento em chunks dentro de uma requisição HTTP
> ainda precisa virar um trace raiz independente por chunk — `Context()` sem o pai atual
> mais um `Link` para o contexto HTTP original — ou ir para job assíncrono com resposta
> `202`. Criar apenas um span filho por chunk mantém tudo no mesmo trace. Tail sampling no
> collector reduz quantos traces são armazenados, mas não torna seguro um trace amostrado
> gigante.

### Trocando o sampler em runtime

Não atribua em `provider.sampler`, `provider._sampler` ou `provider._delegate` depois da
inicialização. `TracerProvider.get_tracer()` entrega o sampler ao `Tracer` no momento da
construção, então tracers já criados continuam com o sampler antigo. `init()` é idempotente
e não refaz a configuração. Para trocar, use `shutdown()` seguido de novo `init()`, ou
configure antes do bootstrap zero-code.

## Framework examples

### FastAPI

```bash
elven-unified-observability -- uvicorn app:app --host 0.0.0.0 --port 8000
```

### Flask

```bash
elven-unified-observability -- flask --app app run
```

### Celery

```bash
elven-unified-observability -- celery -A worker.app worker -l info
```

## API pública

- `init(config=None)`
- `init_from_env(strict=None)`
- `load_config_from_env(env=None)`
- `get_handle()`
- `is_initialized()`
- `force_flush()`
- `shutdown()`
- `logger`
- `tracer`
- `metrics`

## Build e publish

Validação local:

```bash
ruff check .
pyright
pytest
python -m build
python -m twine check dist/*
```

Publish local:

```bash
./publish.sh --repository testpypi --dry-run
./publish.sh --repository testpypi
./publish.sh --repository pypi
```

Tokens esperados:

- `TEST_PYPI_API_TOKEN`
- `PYPI_API_TOKEN`

## Segurança

- Não versione `.env` real
- Não grave token em arquivo
- Use secrets de CI ou variáveis de ambiente locais
