Metadata-Version: 2.4
Name: betshare-infra-logging
Version: 0.1.0
Summary: Shared logging + Slack-alerting for BetShareMarket Python services
License: ISC
Author: BetShareMarket
Author-email: dev@betsharemarket.com
Requires-Python: >=3.10,<4.0
Classifier: License :: OSI Approved
Classifier: Programming Language :: Python :: 3
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: Programming Language :: Python :: 3.14
Requires-Dist: requests (>=2.31,<3.0)
Description-Content-Type: text/markdown

# betshare-infra-logging

Shared logging + Slack-alerting for BetShareMarket Python services. Extracted
from the near-identical per-service `conf_logger` modules
(`consumer`, `joiner-events`, `betfair-exchange-scraper`).

Import name: `betshare_infra_logging`. Distribution name: `betshare-infra-logging`.

## Why

The per-service copies drifted in accidental ways that were bugs:

- **Slack POST hang.** `joiner-events` and `betfair-exchange-scraper` called
  `requests.post(webhook, ...)` with **no timeout**, so a hung Slack call could
  freeze their single-threaded loops. This package bakes in a finite timeout
  (`SLACK_TIMEOUT = 10`) with **no public parameter to override it to `None`**.
- **Level env var schism** (`LOGGER_LEVEL` vs `LOG_LEVEL`).
- **Slack channel drift** (`#error` vs `#errors`).
- Idempotency, noise suppression, and correlation IDs were each reinvented or
  missing.

## Install

**Production (private registry, version-pinned per service):**

```toml
# in the service's pyproject.toml
betshare-infra-logging = "^0.1.0"
```

**Local development (PATH dependency, used by the pilot migration):**

```toml
betshare-infra-logging = { path = "../../packages/py-infra-logging", develop = true }
```

Or with pip: `pip install -e packages/py-infra-logging`.

## Usage

One-liner at service startup. A service's thin `conf_logger.py` shim collapses to:

```python
from betshare_infra_logging import configure_logging

logger = configure_logging("joiner")  # Slack timeout now bounded => hang bug fixed
```

`configure_logging` configures the ROOT logger, so existing
`logging.getLogger(__name__)` call sites keep working unchanged via propagation.

Correlation-id / context binding:

```python
log = logger.child(correlation_id=cid, bookmaker="bet365")
log.info("processing message")
# -> "... processing message [correlation_id=... bookmaker=bet365]"
# context shows in console output AND in the Slack payload.
```

`get_logger(name)` returns a `Logger` wrapping `logging.getLogger(name)`.

## Notes

- **No import-time side effects.** Importing the package does NOT configure
  logging (unlike the old modules which called `configure_logger()` at import).
  Call `configure_logging(...)` explicitly. The package does NOT call
  `load_dotenv()` — that stays in the service entrypoint.
- **Level resolution priority** (Python majority order):
  `level` arg ?? `LOGGER_LEVEL` env ?? `LOG_LEVEL` env ?? `"INFO"`. Cutover does
  not change any service's effective level and no `.env` is edited.
- **Slack channel** defaults to `#errors` (unified from the Python services'
  `#error`). The channel field is **cosmetic** for modern Slack incoming
  webhooks — the destination is fixed by the webhook URL itself, so this change
  is safe and does not reroute alerts.
- **Idempotent.** Calling `configure_logging` twice does not stack duplicate
  console/Slack handlers; the package tracks and clears the handlers it owns.
- **Slack failures are swallowed.** A failing Slack POST never propagates into
  the caller's loop.

## Public interface

```python
DEFAULT_QUIET_LOGGERS = (
    "azure", "azure.servicebus", "azure.core",
    "motor", "pymongo", "crawl4ai", "playwright",
)
SLACK_TIMEOUT = 10  # seconds, baked in

def configure_logging(service, *, level=None, slack_webhook=None,
                      quiet_loggers=None) -> Logger: ...
def get_logger(name=None) -> Logger: ...

class Logger:
    def debug(self, message, **context) -> None
    def info(self, message, **context) -> None
    def warning(self, message, **context) -> None
    def error(self, message, exc=None, **context) -> None
    def child(self, **bindings) -> "Logger"
```

