Metadata-Version: 2.5
Name: coralogix-otel-logger
Version: 2.0.0
Summary: A production-grade OpenTelemetry logger for Coralogix gRPC endpoints.
Project-URL: Homepage, https://github.com/nix-power/coralogix-otel-logger
Project-URL: Repository, https://github.com/nix-power/coralogix-otel-logger
Author-email: nix-power <dima@nix-power.com>
License-Expression: Apache-2.0
License-File: LICENSE
Classifier: Development Status :: 5 - Production/Stable
Classifier: Framework :: OpenTelemetry
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python
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
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: opentelemetry-api>=1.44.0
Requires-Dist: opentelemetry-exporter-otlp-proto-grpc>=1.44.0
Requires-Dist: opentelemetry-sdk>=1.44.0
Description-Content-Type: text/markdown

# Coralogix OTel Logger

A lightweight Python logging wrapper for sending structured application logs directly to Coralogix through OpenTelemetry OTLP/gRPC.

The package connects Python's standard `logging` framework to the OpenTelemetry logging SDK while supporting:

- structured dictionary payloads
- asynchronous batching
- explicit flushing
- existing Python logger integration
- accurate caller metadata
- direct OTel/gRPC exporter errors on stdout

Based on the official [Coralogix Python OpenTelemetry SDK](https://coralogix.com/docs/integrations/sdks/python-sdk/) integration model.

---

## Installation

```bash
pip install coralogix-otel-logger
```

---

## Quick Start

```python
from cxlogger import CoralogixOTelLogger


cx_logger = CoralogixOTelLogger(
    app_name="example-service",
    subsystem_name="events",
    api_key="your-coralogix-api-key"
)

payload = {
    "environment": "production",
    "request_id": "req-1042",
    "status": "completed"
}

cx_logger.info(
    "Request processing completed",
    payload=payload
)

cx_logger.flush()
---

## Configuration

```python
CoralogixOTelLogger(
    app_name,
    subsystem_name,
    logger_name=None,
    api_key=None,
    domain=None,
    log_level="info",
    flush_delay_ms=5000,
    cert_path=None
)
```

| Parameter | Default | Description |
| :--- | :--- | :--- |
| `app_name` | required | Coralogix application name. |
| `subsystem_name` | required | Coralogix subsystem name. |
| `logger_name` | `cx_{app_name}_{subsystem_name}` | Python logger name. Can also reference an existing logger. |
| `api_key` | `CORALOGIX_API_KEY` | Coralogix API key. |
| `domain` | derived from region | Coralogix domain such as `eu2.coralogix.com`. |
| `log_level` | `info` | Minimum Python logging level. |
| `flush_delay_ms` | `5000` | OTel batch scheduling delay in milliseconds. |
| `cert_path` | `None` | Optional TLS certificate file. |

If no API key is supplied through either the constructor or environment, initialization raises `CoralogixConfigurationError`.

---

## Environment Variables

| Variable | Example | Description |
| :--- | :--- | :--- |
| `CORALOGIX_API_KEY` | `cx-...` | Used if `api_key` is omitted. |
| `CORALOGIX_REGION` | `eu1`, `eu2`, `us1` | Used to build the Coralogix endpoint. Defaults to `US1`. |

Example:

```bash
export CORALOGIX_API_KEY="cx-..."
export CORALOGIX_REGION="eu2"
```

The resulting endpoint is:

```text
https://ingress.eu2.coralogix.com:443
```

An explicit `domain` overrides `CORALOGIX_REGION`.

---

## Logging API

The wrapper exposes the standard logging levels:

```python
cx_logger.debug(message, payload=None, exc_info=None)
cx_logger.info(message, payload=None, exc_info=None)
cx_logger.warning(message, payload=None, exc_info=None)
cx_logger.error(message, payload=None, exc_info=None)
cx_logger.critical(message, payload=None, exc_info=None)
```

Example:

```python
cx_logger.info(
    "Task completed",
    payload={
        "task_id": "task-42",
        "duration_ms": 187,
        "result": "success"
    }
)
```

The wrapper automatically includes the log message inside the structured payload:

```json
{
    "message": "Task completed",
    "task_id": "task-42",
    "duration_ms": 187,
    "result": "success"
}
```

---

## Structured Payloads

`payload` should be a Python dictionary:

```python
cx_logger.info(
    "Object processed",
    payload={
        "object_id": "obj-123",
        "region": "us-east-1",
        "cached": False
    }
)
```

Internally, the dictionary is attached to the Python `LogRecord` using:

```python
extra={"payload": payload}
```

This preserves normal Python logging while adding structured metadata for OpenTelemetry.

---

## Invalid Payload Types

If `payload` is not a dictionary:

```python
cx_logger.info(
    "Object processed",
    payload="unexpected string"
)
```

the wrapper does not crash.

Instead, the event is converted to `ERROR` level and includes diagnostic information similar to:

```json
{
    "event_type": "logger_payload_type_error",
    "logger_warning": "Passed an invalid payload type (str). Expected 'dict'.",
    "rejected_raw_payload": "unexpected string"
}
```

The rejected value is limited to 500 characters.

---

## JSON Serialization Validation

Before emitting an event, the wrapper validates the structured payload with:

```python
json.dumps(payload)
```

If the payload contains an unsupported object, the wrapper emits a fallback error event instead of allowing serialization failure to interrupt the application.
---

## Existing Python Logger Integration

Python returns the same `Logger` object for the same logger name:

```python
import logging


logger_a = logging.getLogger("example_service")
logger_b = logging.getLogger("example_service")

assert logger_a is logger_b
```

This allows Coralogix export to be attached to an already configured application logger.

Example:

```python
import logging
import sys

from cxlogger import CoralogixOTelLogger


logger = logging.getLogger("example_service")
logger.setLevel(logging.INFO)

console_handler = logging.StreamHandler(sys.stdout)
logger.addHandler(console_handler)

cx_logger = CoralogixOTelLogger(
    app_name="example-service",
    subsystem_name="application",
    logger_name="example_service"
)
```

The logger now has two handlers:

```text
Logger("example_service")
        │
        ├── StreamHandler
        │       ↓
        │     stdout
        │
        └── OpenTelemetry LoggingHandler
                ↓
             Coralogix
```

A normal call:

```python
logger.info("Service started")
```

is therefore processed by both handlers.

The package does not remove existing application handlers.

---

## Native Logging From Other Modules

Once `CoralogixOTelLogger` is initialized for a named logger, other modules can use standard Python logging directly.

### Application initialization

We create a cx_logger as logger name "example_service" and then we natively use it in another modules of the same
python project by logger_name.

```python
from cxlogger import CoralogixOTelLogger


cx_logger = CoralogixOTelLogger(
    app_name="example-service",
    subsystem_name="application",
    logger_name="example_service"
)
```

### Another module

```python
import logging


logger = logging.getLogger("example_service")

logger.info(
    "Connection pool initialized",
    extra={
        "payload": {
            "pool_size": 20,
            "region": "us-east-1"
        }
    }
)
```

No `cxlogger` import is required in the downstream module, it already has CoralogixOTelLogger logger object in registry initialized and ready to deliver.

---

## Asynchronous Batching

The package uses OpenTelemetry's `BatchLogRecordProcessor`.

Normal log calls are therefore asynchronous:

```text
logger.info(...)
    ↓
LogRecord
    ↓
OTel
    ↓
in-memory queue
    ↓
application continues
```

A background worker exports queued records to Coralogix.

The default scheduling delay is:

```text
5000 ms
```

and can be changed using:

```python
flush_delay_ms=1000
```

Batching reduces the need for a network request on every individual log event.

---

## Explicit `flush()`

The wrapper provides:

```python
flush_success = cx_logger.flush()
```

`flush()` calls OpenTelemetry:

```python
self.provider.force_flush(
    timeout_millis=30_000
)
```

and returns a boolean.

### Return Value

```text
True
```

means the OTel `force_flush()` operation completed within the timeout.

```text
False
```

means the flush did not complete within the 30-second timeout.

Example:

```python
cx_logger.info(
    "Final result generated",
    payload={"result_id": "result-123"}
)

if not cx_logger.flush():
    print("OpenTelemetry flush timed out")
```

### Important

`flush()` is not a per-event acknowledgement API.

A return value of `True` means the OpenTelemetry flush operation completed. It should not be interpreted as a transactional guarantee that one specific record has been permanently stored by Coralogix.

---

## When to Use `flush()`

For long-running applications, normally let the batch processor work asynchronously:

```python
cx_logger.info("Request started")
cx_logger.info("Request completed")
```

Calling `flush()` after every log event removes much of the benefit of batching.

Explicit flushing is useful for:

- short-lived scripts
- CLI tools
- CI/CD jobs
- important final events
- application paths that should not wait for the regular batch interval

Example:

```python
cx_logger.info(
    "Job completed",
    payload={"job_id": "job-123"}
)

if not cx_logger.flush():
    print("Failed to complete OTel flush before timeout")
```

---

## OTel and gRPC Errors

Network communication is performed by the OpenTelemetry OTLP/gRPC exporter.

Exporter problems may include:

```text
UNAUTHENTICATED
PERMISSION_DENIED
UNAVAILABLE
DEADLINE_EXCEEDED
network failures
OTLP/gRPC warnings
```

The wrapper configures the logger namespace:

```text
opentelemetry.exporter.otlp.proto.grpc
```

with a dedicated:

```python
logging.StreamHandler(sys.stdout)
```

at `WARNING` level.

The error path is therefore:

```text
Coralogix / network error
        ↓
OTLPLogExporter
        ↓
OTel internal logger
        ↓
stdout
```

Example output:

```text
[opentelemetry.exporter.otlp.proto.grpc.exporter] ERROR: Failed to export logs ...
```

These diagnostics intentionally bypass the Coralogix pipeline.

---

## Error Logger Isolation

The internal OTel/gRPC logger uses:

```python
otel_grpc_logger.propagate = False
```

This prevents exporter failures from propagating to root or application handlers after they have been written to stdout.

The intended behavior is:

```text
OTel exporter failure
        ↓
stdout
        ↓
STOP
```

This helps avoid duplicate messages or accidentally feeding transport errors back into the same telemetry pipeline.

---

## Caller Metadata

Logging wrappers can make records appear to originate from the wrapper itself instead of the actual application code.

`CoralogixOTelLogger` installs `_CallerMetadataFilter`, which walks the Python stack and updates:

```text
record.pathname
record.filename
record.lineno
record.funcName
```

to point to the actual caller.

This allows Coralogix to display useful source metadata even when logging through the wrapper.

---

## Exception Information

All logging methods support `exc_info`.

Example:

```python
try:
    process_item()
except Exception:
    cx_logger.error(
        "Failed to process item",
        payload={
            "item_id": "item-123"
        },
        exc_info=True
    )
```

Exception information is passed through Python logging and into the OpenTelemetry pipeline.

---

## Log Levels

Supported levels:

| Input | Python Level |
| :--- | :--- |
| `DEBUG` | `logging.DEBUG` |
| `INFO` | `logging.INFO` |
| `WARNING` / `WARN` | `logging.WARNING` |
| `ERROR` / `ERR` | `logging.ERROR` |
| `CRITICAL` / `CRIT` | `logging.CRITICAL` |

Input is case-insensitive.

Unknown values fall back to `INFO`.

---

## OpenTelemetry Provider Reuse

OpenTelemetry uses a process-wide `LoggerProvider`.

The wrapper first checks:

```python
current_provider = otel_logs.get_logger_provider()
```

If an SDK `LoggerProvider` already exists, it is reused.

Otherwise, the wrapper creates:

```text
Resource
    ↓
LoggerProvider
    ↓
BatchLogRecordProcessor
    ↓
OTLPLogExporter
```

This means the first configured provider effectively owns the process-level OTel logging pipeline.

Applications that configure OpenTelemetry independently should be aware that an existing provider will be reused rather than replaced.

---

## Multiple Instances

Creating multiple `CoralogixOTelLogger` objects with the same `logger_name` returns the same underlying Python logger.

The wrapper checks whether an OpenTelemetry `LoggingHandler` is already attached before adding another one.

It also avoids adding duplicate stdout handlers to the OTel/gRPC error logger.

This prevents repeated log delivery caused by duplicate handlers.

---

## Recommended Usage

### Long-running service

```python
cx_logger = CoralogixOTelLogger(
    app_name="example-service",
    subsystem_name="backend"
)

cx_logger.info(
    "Service ready",
    payload={"version": "1.4.0"}
)
```

Let OTel batching handle normal delivery.

### Short-lived process

```python
cx_logger = CoralogixOTelLogger(
    app_name="example-job",
    subsystem_name="worker"
)

cx_logger.info(
    "Processing completed",
    payload={"processed": 250}
)

if not cx_logger.flush():
    raise RuntimeError("OpenTelemetry flush timed out")
```
---


## License

Apache 2.0 License.