Metadata-Version: 2.4
Name: gen3logging
Version: 2.0.0
Summary: Standardized logging tool and format for Gen3
License-Expression: Apache-2.0
License-File: LICENSE
License-File: NOTICE
Author: CTDS UChicago
Author-email: cdis@uchicago.edu
Requires-Python: >=3.13,<4
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: System :: Logging
Project-URL: Homepage, https://github.com/uc-cdis/gen3logging
Project-URL: Repository, https://github.com/uc-cdis/gen3logging
Description-Content-Type: text/markdown

# gen3logging

Logging routines to centralize format.

## Basic usage

---

In the parent/root module:

```python
from gen3logging import get_logger

logger = get_logger("{{service_name}}", log_level='info')
logger.info('Hello world!')
```

In a child module:

```python
from gen3logging import get_logger

logger = get_logger(__name__)
logger.info('Hello world!')

logger = get_logger(__name__, log_level='debug')
logger.debug('I am a child logger but I now have my own custom log level!')
```

You _must_ set the logging level on the parent logger!

Inheritance is by dotted name, so the root logger's name must be a prefix of the
child module's `__name__`. Naming the root after the top-level package (a module
in `fence/auth.py` has `__name__ == "fence.auth"`, which inherits from `fence`)
is what makes this work. A root name that is not a prefix of its modules' names
gets no children.

The default `log_level` argument in `get_logger` is `None`, which means the default logging level for new loggers is `NOTSET`. This means that child loggers whose levels were not explicitly set will inherit logging levels and handlers from ancestor loggers. This makes it easier to do things like change application-wide logging levels by just setting the level on the parent logger. Levels and handlers can still be individually set on child nodes by providing a `log_level` argument.

See more about Python logging [here](https://docs.python.org/3/library/logging.html).

## Parameters

---

`get_logger` takes:

* `logger_name` - name of the logger, or `None` for the root logger
* `file_name` (default: `None`) - if present, the logger will output logs to a file as well as stdout
* `log_level` (default: `None`) - one of `notset`, `debug`, `info`, `warning`, `warn`, `error`, `critical`, in any case. Anything else raises `ValueError`
* `format` (default: `None`, meaning the format below) - `%`-style format string for text output
* `json_logs` (default: `None`, meaning the `GEN3_JSON_LOGS` env var decides) - emit JSON instead of text

`file_name`, `format` and `json_logs` only apply when a logger first gets its
handlers, on the first call that sets a level other than `notset`. Calling
`get_logger` again for the same logger keeps its existing handlers, so set
`GEN3_JSON_LOGS` to switch a whole service to JSON regardless of call order.

## Output format

---

By default logs are plain text:

```
[2025-01-15 12:22:04,031][      fence][   INFO] user logged in
```

When OpenTelemetry is active, trace context is appended automatically:

```
[2025-01-15 12:22:04,031][      fence][   INFO] user logged in [trace_id=4bf92f3577b34da6a3ce929d0e0e4736 span_id=00f067aa0ba902b7 service=fence]
```

The suffix carries only the fields that are available, and is absent entirely
when none are. It follows the message, ahead of any traceback or stack.
`gen3logging` does not depend on OpenTelemetry: populate these by
installing `opentelemetry-instrumentation-logging` and calling
`LoggingInstrumentor().instrument()`. Nothing needs to change here to pick them
up, and nothing fails when they are missing.

`trace_id` and `span_id` are either a usable id or absent.

Any `%`-style format string can replace the default:

```python
logger = get_logger("gen3foo", log_level='info', format="%(levelname)s %(message)s")
```

## Enabling JSON Logs

---

Set `json_logs=True` to emit one JSON object per line instead of the default format:

```python
logger = get_logger("gen3foo", log_level='info', json_logs=True)
```

```json
{"timestamp": "2025-01-15T18:22:04.031Z", "logger": "fence.auth", "level": "INFO", "message": "user logged in", "trace_id": null, "span_id": null, "service": null}
```

| Key                              | Value                                                             |
| -------------------------------- | ----------------------------------------------------------------- |
| `timestamp`                      | ISO 8601 UTC, millisecond precision                               |
| `logger`                         | logger name                                                       |
| `level`                          | level name, e.g. `INFO`                                           |
| `message`                        | the formatted message, with `%`-style arguments interpolated      |
| `trace_id`, `span_id`, `service` | OpenTelemetry correlation, `null` when unavailable                |
| `exception`                      | traceback text, present only on records that carry exception info |
| `stack_info`                     | stack text, present only on records logged with `stack_info=True` |

Fields passed through a logging call's `extra=` are not included.

Because the message and traceback are JSON-escaped, a multi-line traceback stays
on a single log line and can be parsed by a log aggregator without stitching.

To turn JSON on for a deployment without a code change, set the
`GEN3_JSON_LOGS` environment variable:

```
GEN3_JSON_LOGS=true
```

An explicit `json_logs` argument always wins over the environment variable, so a
service can force either format. When `json_logs` is omitted the environment
variable decides, and when neither is set, output is text. Enabling JSON
supersedes any `format` string.

## Migrating from `cdislogging`

---

1. Replace the `cdislogging` dependency with `gen3logging>=2.0.0`.
2. Replace `from cdislogging import ...` with `from gen3logging import ...`.

That is the whole migration. Output is identical to `cdislogging` 1.2.0
until you enable JSON or instrument with OpenTelemetry.

> NOTE: The library also now contains a PEP 561 py.typed marker, which tells type checkers to enforce our typing.

