Metadata-Version: 2.4
Name: sumo-logger
Version: 1.2.3
Summary: A lightweight Python logging handler for sending logs to Sumo Logic HTTP Sources.
Author-email: Saad Ullah <saadullah.butt@caregenix.solutions>, Mujahid Rasool <mujahid.rasool@caregenix.solutions>
License: MIT
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests
Dynamic: license-file

# sumo-logger

A lightweight and production-ready Python package for sending application logs to **Sumo Logic HTTP Sources** using the built-in `logging` module.

---

## 📘 Overview

`sumo-logger` makes it easy to stream your Python logs directly to [Sumo Logic](https://www.sumologic.com/) with minimal setup.It extends Python's native `logging` system, allowing you to:

- Centralize logs across microservices or distributed apps.
- Automatically retry failed requests with exponential backoff.
- Send structured JSON logs containing rich metadata (timestamp, level, file, function, etc.).
- Use it as a drop-in replacement for your existing logging setup.

---

## 📦 Installation

You can install **sumo-logger** directly from [PyPI](https://pypi.org/project/sumo-logger/):

```bash
pip install sumo-logger
```

**Requirements:**

- Python 3.8 or later
- `requests` (installed automatically as a dependency)

You'll also need a Sumo Logic **HTTP Source presigned URL**, created on the Sumo Logic side (*Manage Data → Collection → set up an HTTP Source*). It looks like:

```
https://endpoint1.collection.us2.sumologic.com/receiver/v1/http/<long-token>
```

---

## 🚀 Quick Start

```python
from sumo_logger import setup_sumo_logger

SUMO_URL = "https://endpoint1.collection.us2.sumologic.com/receiver/v1/http/XXXXXXXX"

logger = setup_sumo_logger(SUMO_URL, "YOUR_LOGGER_NAME")

logger.info("Service started successfully")
logger.warning("Cache miss for key=%s", cache_key)
logger.error("Failed to process order", exc_info=True)
```

> 💡 **Tip:** `"YOUR_LOGGER_NAME"` becomes the logger's name (`record.name`) and is what shows up as the `logger` field in Sumo Logic — use something identifiable, like your service or module name, e.g. `"billing-service"` or `"orders.api"`.

---

## 📚 API Reference

### `setup_sumo_logger(http_url, module_name=None, log_level=logging.INFO)`

Creates (or reuses) a standard `logging.Logger` and attaches a `SumoHttpHandler` to it. This is the only function most users ever need.

| Parameter       | Type           | Default          | Description                                                                                                                |
| --------------- | -------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `http_url`    | `str`        | required         | Your Sumo Logic HTTP Source presigned URL. Raises`ValueError` if empty/`None`.                                         |
| `module_name` | `str \| None` | `None`         | Name given to the logger. If omitted, sumo-logger inspects the call stack and auto-detects the name of the calling module. |
| `log_level`   | `int`        | `logging.INFO` | Minimum severity this logger will process (e.g.`logging.DEBUG`, `logging.WARNING`).                                    |

**Returns:** a standard `logging.Logger` instance — so every normal method works: `.debug()`, `.info()`, `.warning()`, `.error()`, `.critical()`, `.exception()`.

> ✅ **Safe to call more than once:** If you call `setup_sumo_logger()` again for the same module, it will not attach a duplicate handler — it checks whether a `SumoHttpHandler` is already registered on that logger first.

---

### `SumoHttpHandler(http_source_url, max_retries=5, backoff_factor=1)`

The `logging.Handler` subclass that actually performs the HTTP POST to Sumo Logic. `setup_sumo_logger()` creates one of these for you, but you can also use it directly if you want full control (e.g. attaching it to an existing logger, or combining it with other handlers).

| Parameter           | Type    | Default  | Description                                                                   |
| ------------------- | ------- | -------- | ----------------------------------------------------------------------------- |
| `http_source_url` | `str` | required | The Sumo Logic HTTP Source URL. Raises`ValueError` if empty.                |
| `max_retries`     | `int` | `5`    | How many times to retry a send after a 429 (rate limit) response.             |
| `backoff_factor`  | `int` | `1`    | Base seconds for exponential backoff:`delay = backoff_factor × 2^attempt`. |

```python
import logging
from sumo_logger import SumoHttpHandler

logger = logging.getLogger("my.custom.logger")
logger.setLevel(logging.DEBUG)
logger.addHandler(SumoHttpHandler(SUMO_URL, max_retries=3, backoff_factor=2))
```

---

### `set_request_id(request_id=None)` / `get_request_id()`

Located in `sumo_logger.context`. These use a `contextvars.ContextVar` under the hood, so the request ID is automatically attached to `record.request_id` on every log line — no need to pass it manually to each log call.

| Parameter      | Type           | Default  | Description                                                             |
| -------------- | -------------- | -------- | ----------------------------------------------------------------------- |
| `request_id` | `str \| None` | `None` | `set_request_id()`: if omitted, a random UUID4 is generated and used. |

```python
from sumo_logger import setup_sumo_logger
from sumo_logger.context import set_request_id

logger = setup_sumo_logger(SUMO_URL, "orders-service")

def handle_request(payload):
    req_id = set_request_id()          # generates + stores a UUID4
    logger.info("Handling request")     # request_id is attached automatically
    process(payload)
    logger.info("Request complete")

def process(payload):
    # same request_id is still active here — no need to pass it around
    logger.debug("Processing payload: %s", payload)
```

> 🔎 **Why this matters:** Because the request ID rides on a `ContextVar`, it correctly follows a single request/task through nested function calls (and async code) without being passed as a parameter everywhere — every log line from that flow will carry the same `request_id` in Sumo Logic, so you can filter one request's full trace.

---

## 📤 What Actually Gets Sent to Sumo Logic

Every log call is serialized into this exact JSON structure before being POSTed:

```json
{
  "timestamp": "2026-07-09T14:32:01.512384",
  "level": "INFO",
  "logger": "orders-service",
  "message": "Handling request",
  "pathname": "/app/orders/handlers.py",
  "funcName": "handle_request",
  "request_id": "3f1c9e2a-7b44-4e9d-9c31-df1a6b0c9d21"
}
```

| Field          | Type           | Description                                                               |
| -------------- | -------------- | ------------------------------------------------------------------------- |
| `timestamp`  | `str`        | ISO-8601 timestamp derived from the log record's creation time.           |
| `level`      | `str`        | Log level name:`DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`. |
| `logger`     | `str`        | The logger name (your`module_name` / `YOUR_LOGGER_NAME`).             |
| `message`    | `str`        | The formatted log message.                                                |
| `pathname`   | `str`        | Full file path where the log call was made.                               |
| `funcName`   | `str`        | Name of the function where the log call was made.                         |
| `request_id` | `str \| null` | Current request ID if set via`set_request_id()`, otherwise `null`.    |

`Content-Type` is always `application/json`, and the POST timeout is fixed at **5 seconds**.

---

## 🔁 Retry & Backoff Behavior

If Sumo Logic responds with HTTP **429** (rate limited), the handler retries with exponential backoff:

```
delay = backoff_factor × 2^attempt   (attempt = 0, 1, 2, ...)
```

With the defaults (`backoff_factor=1`, `max_retries=5`), delays are approximately: `1s → 2s → 4s → 8s → 16s`.

After the final retry fails, sumo-logger prints `"Maximum retries reached. Log not sent to Sumo Logic."` and gives up on that particular record — it does **not** raise an exception, so your application keeps running.

> ⚠️ **Other failure modes:** Any other HTTP error, connection error, or unexpected exception during the send is caught, printed to stdout, and the handler moves on without crashing your app. Logging failures are intentionally non-fatal.

---

## ✅ Best Practices

- Call `setup_sumo_logger()` once per module, near the top of the file — it is idempotent, so importing that module multiple times won't create duplicate handlers.
- Use a distinct, human-readable `module_name` per service (e.g. `"payments-api"`, `"worker.email"`) so you can filter easily inside Sumo Logic.
- Call `set_request_id()` at the entry point of each request/job (e.g. in middleware or at the top of a task handler) rather than deep inside business logic.
- Keep your presigned HTTP Source URL out of source control — load it from an environment variable or secrets manager, not a hard-coded string.
- Use `logger.exception("message")` inside `except` blocks instead of `logger.error(...)` when you want the traceback captured in the message text.
- Since every log call blocks briefly for the HTTP POST (up to a 5-second timeout, plus retries), avoid setting `log_level=logging.DEBUG` in high-throughput production hot paths unless you also raise `backoff_factor` / lower `max_retries` deliberately.

---

## 🛠️ Troubleshooting

**`ValueError: HTTP URL is required to initialize Sumo logger`**
You called `setup_sumo_logger()` with an empty string or `None` as `http_url`. Double-check the environment variable or config value you're passing in actually resolves to a URL.

**Logs aren't showing up in Sumo Logic**

- Confirm the presigned URL is still valid (Sumo Logic HTTP Sources can be deleted/rotated on the Sumo side).
- Check stdout for `"HTTP error"`, `"Connection error"`, or `"Maximum retries reached"` messages printed by the handler — these indicate the POST failed silently from your app's perspective.
- Make sure the logger's `log_level` (and any parent logger's level) isn't filtering out the messages you expect to see, e.g. calling `.debug()` on a logger set to `logging.INFO`.

**Seeing duplicate log entries in Sumo Logic**
This usually means `setup_sumo_logger()` was called for the same `module_name` in a way that bypassed the duplicate-handler check — for example, if two different `module_name` values were used for what is logically the same logger. Standardize on one `module_name` per logical component.

**`request_id` is `null` in Sumo Logic**
`set_request_id()` was never called in that code path. The context variable defaults to `None` until it's explicitly set — call `set_request_id()` at the start of the relevant request/task.

---

## 🧩 Full Working Example

```python
import logging
import os
from sumo_logger import setup_sumo_logger
from sumo_logger.context import set_request_id

SUMO_URL = os.environ["SUMO_HTTP_SOURCE_URL"]

logger = setup_sumo_logger(SUMO_URL, "inventory-service", log_level=logging.INFO)


def restock_item(item_id, quantity):
    set_request_id()
    logger.info("Restock started for item_id=%s qty=%s", item_id, quantity)
    try:
        _update_inventory(item_id, quantity)
        logger.info("Restock completed for item_id=%s", item_id)
    except Exception:
        logger.exception("Restock failed for item_id=%s", item_id)
        raise


def _update_inventory(item_id, quantity):
    logger.debug("Writing %s units to inventory table", quantity)
    # ... database write here ...
```

---

*Every function signature, default value, and JSON field in this document was read directly from the `sumo_logger` 1.2.3 wheel (`setup_logger.py`, `handler.py`, `context.py`) — not inferred.*
