dlogify 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dlogify-0.1.0/.gitignore +5 -0
- dlogify-0.1.0/LICENSE +21 -0
- dlogify-0.1.0/PKG-INFO +111 -0
- dlogify-0.1.0/README.md +94 -0
- dlogify-0.1.0/pyproject.toml +43 -0
- dlogify-0.1.0/src/dlogify/__init__.py +160 -0
- dlogify-0.1.0/src/dlogify/__main__.py +3 -0
- dlogify-0.1.0/src/dlogify/_capture_logging.py +56 -0
- dlogify-0.1.0/src/dlogify/_client.py +239 -0
- dlogify-0.1.0/src/dlogify/_config.py +194 -0
- dlogify-0.1.0/src/dlogify/_diagnostics.py +54 -0
- dlogify-0.1.0/src/dlogify/_encode.py +100 -0
- dlogify-0.1.0/src/dlogify/_exporter.py +407 -0
- dlogify-0.1.0/src/dlogify/_guard.py +29 -0
- dlogify-0.1.0/src/dlogify/_hooks.py +95 -0
- dlogify-0.1.0/src/dlogify/_http.py +52 -0
- dlogify-0.1.0/src/dlogify/_levels.py +88 -0
- dlogify-0.1.0/src/dlogify/_parse/__init__.py +0 -0
- dlogify-0.1.0/src/dlogify/_parse/compact_json.py +55 -0
- dlogify-0.1.0/src/dlogify/_parse/json_line.py +178 -0
- dlogify-0.1.0/src/dlogify/_parse/lines.py +64 -0
- dlogify-0.1.0/src/dlogify/_parse/stream_parser.py +106 -0
- dlogify-0.1.0/src/dlogify/_parse/text_event.py +150 -0
- dlogify-0.1.0/src/dlogify/_record.py +149 -0
- dlogify-0.1.0/src/dlogify/_singleton.py +12 -0
- dlogify-0.1.0/src/dlogify/_version.py +4 -0
- dlogify-0.1.0/src/dlogify/handler.py +70 -0
- dlogify-0.1.0/src/dlogify/py.typed +0 -0
- dlogify-0.1.0/src/dlogify/run/__init__.py +0 -0
- dlogify-0.1.0/src/dlogify/run/child.py +215 -0
- dlogify-0.1.0/src/dlogify/run/cli.py +22 -0
dlogify-0.1.0/.gitignore
ADDED
dlogify-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Logify
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
dlogify-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: dlogify
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Send Python logs and uncaught exceptions to dlogify.
|
|
5
|
+
Project-URL: Homepage, https://dlogify.com
|
|
6
|
+
Project-URL: Documentation, https://docs.dlogify.com/send-logs/python
|
|
7
|
+
Author: Logify
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: dlogify,errors,exceptions,logging,opentelemetry
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: System :: Logging
|
|
14
|
+
Classifier: Typing :: Typed
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# dlogify
|
|
19
|
+
|
|
20
|
+
Send your Python logs and uncaught exceptions to [Logify](https://dlogify.com), which groups errors, classifies them with AI and delivers tickets with the full context.
|
|
21
|
+
|
|
22
|
+
Requires Python 3.10 or later. No dependencies.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install dlogify
|
|
28
|
+
export LOGIFY_API_KEY=lgf_live_…
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## With the SDK
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
import logging
|
|
35
|
+
|
|
36
|
+
import dlogify
|
|
37
|
+
|
|
38
|
+
dlogify.init(service="billing")
|
|
39
|
+
logging.basicConfig(level=logging.INFO)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`init()` sends every record that passes your `logging` configuration, and every uncaught exception (main thread and other threads). It changes no logger's level and adds no handler, so `logging.basicConfig()` works before or after it and your console output stays as it was. Python's root logger shows `WARNING` and above unless you configure it, as above.
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
log = logging.getLogger("billing")
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
charge(order_id)
|
|
49
|
+
except PaymentError:
|
|
50
|
+
log.exception("charge failed", extra={"order_id": order_id})
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Keys passed with `extra=` become attributes; `log.exception()` (or `exc_info=True`) attaches the traceback.
|
|
54
|
+
|
|
55
|
+
You can also log directly:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
dlogify.info("order placed", attributes={"order_id": order_id})
|
|
59
|
+
dlogify.error("charge failed", exc=err, attributes={"order_id": order_id})
|
|
60
|
+
dlogify.log("warning", "disk almost full") # any level name, any case
|
|
61
|
+
dlogify.capture_exception() # inside an except block
|
|
62
|
+
dlogify.flush() # before os._exit()
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`log()` takes `trace`, `debug`, `info`, `warn`, `error` and `fatal`, and also Python's `warning` and `critical`, in any case; an unknown name logs at `info`.
|
|
66
|
+
|
|
67
|
+
- `init(capture_logging=False)` or `init(capture_uncaught=False)` turns a capture off.
|
|
68
|
+
- Calls before `init()` are dropped, and a second `init()` is ignored.
|
|
69
|
+
- `dlogify.Client(**options)` creates an independent client.
|
|
70
|
+
- `dictConfig` users can name the handler instead: `{"class": "dlogify.LogifyHandler"}` with `init(capture_logging=False)`; it sends through the `init()` client. With the capture left on, each record is still sent once.
|
|
71
|
+
- `structlog` and `loguru` can write to the standard `logging` module, which dlogify captures.
|
|
72
|
+
|
|
73
|
+
## Any process: logify run
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
LOGIFY_API_KEY=lgf_live_… pipx run dlogify run -- python manage.py runserver
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
With the package installed, the command is `logify run -- <cmd>`. The output appears in your terminal exactly as without dlogify, and the exit code is the command's. Python tracebacks, stack traces from other languages and Go panics become single events; JSON lines are read as structured records. A non-zero exit or a death by signal is reported as one more error, except a `SIGPIPE` after the reader of the output went away, as in `| head`. Not available on Windows.
|
|
80
|
+
|
|
81
|
+
## Configuration
|
|
82
|
+
|
|
83
|
+
Options passed to `init()` win over environment variables, which win over the defaults.
|
|
84
|
+
|
|
85
|
+
| Variable | Option | Default |
|
|
86
|
+
|---|---|---|
|
|
87
|
+
| `LOGIFY_API_KEY` | `api_key` | Required; without it the client is disabled. |
|
|
88
|
+
| `LOGIFY_ENDPOINT` | `endpoint` | `https://api.dlogify.com` |
|
|
89
|
+
| `LOGIFY_SERVICE` | `service` | `OTEL_SERVICE_NAME`, then the `[project] name` of the nearest `pyproject.toml` from the working directory, then `unknown_service` |
|
|
90
|
+
| `LOGIFY_ENVIRONMENT` | `environment` | `default` |
|
|
91
|
+
| `LOGIFY_RELEASE` | `release` | empty |
|
|
92
|
+
| `LOGIFY_MIN_LEVEL` | `min_level` | `info` (`trace`, `debug`, `info`, `warn`, `error`, `fatal`) |
|
|
93
|
+
| `LOGIFY_DISABLED` | `disabled` | off (`1` or `true` turns the client into a no-op) |
|
|
94
|
+
| `LOGIFY_DEBUG` | `debug` | off (`1` or `true` prints the client's diagnostics to stderr) |
|
|
95
|
+
|
|
96
|
+
Also `attributes` (added to every record) and `max_queue_size` (default 2000). `disabled`, `debug`, `capture_logging` and `capture_uncaught` take `True` or `False`; any other value is ignored with a warning.
|
|
97
|
+
|
|
98
|
+
## Guarantees
|
|
99
|
+
|
|
100
|
+
- No call raises, and logging never waits for the network: records are sent by a background daemon thread, in compressed batches, with retries for up to 5 minutes.
|
|
101
|
+
- If the key is rejected, the client stops for the rest of the process with one warning. Redirects are not followed: one warning names the URL to put in `LOGIFY_ENDPOINT`.
|
|
102
|
+
- If the endpoint's TLS certificate cannot be verified, one warning says how to fix it: with Python from python.org on macOS, run `Install Certificates.command`; behind a proxy that inspects TLS, set `SSL_CERT_FILE` to its CA bundle.
|
|
103
|
+
- After an uncaught exception the client sends what is queued, then Python reports the exception as usual. At normal exit it flushes too. An exit spends at most 2 seconds on this in total. `os._exit()` skips both, so call `dlogify.flush()` first.
|
|
104
|
+
- Works with pre-fork servers such as gunicorn, and with `multiprocessing` children, which flush when their target returns. `Pool.terminate()` (also run when a `with Pool()` block ends) stops workers before they flush: call `pool.close()` and `pool.join()` instead. Other worker pools that end with `os._exit()` should call `dlogify.flush()` first.
|
|
105
|
+
- The client's own messages go to stderr, never through `logging`.
|
|
106
|
+
|
|
107
|
+
## What is sent
|
|
108
|
+
|
|
109
|
+
Only log records: message, level, time, logger name, the attributes you pass, the exception's type, message and traceback, and the service name, environment, release, host name and SDK version. The SDK makes no other network calls and opens no ports. Personal data is redacted by Logify on arrival.
|
|
110
|
+
|
|
111
|
+
Documentation: https://docs.dlogify.com/send-logs/python
|
dlogify-0.1.0/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# dlogify
|
|
2
|
+
|
|
3
|
+
Send your Python logs and uncaught exceptions to [Logify](https://dlogify.com), which groups errors, classifies them with AI and delivers tickets with the full context.
|
|
4
|
+
|
|
5
|
+
Requires Python 3.10 or later. No dependencies.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install dlogify
|
|
11
|
+
export LOGIFY_API_KEY=lgf_live_…
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## With the SDK
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
import logging
|
|
18
|
+
|
|
19
|
+
import dlogify
|
|
20
|
+
|
|
21
|
+
dlogify.init(service="billing")
|
|
22
|
+
logging.basicConfig(level=logging.INFO)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`init()` sends every record that passes your `logging` configuration, and every uncaught exception (main thread and other threads). It changes no logger's level and adds no handler, so `logging.basicConfig()` works before or after it and your console output stays as it was. Python's root logger shows `WARNING` and above unless you configure it, as above.
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
log = logging.getLogger("billing")
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
charge(order_id)
|
|
32
|
+
except PaymentError:
|
|
33
|
+
log.exception("charge failed", extra={"order_id": order_id})
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Keys passed with `extra=` become attributes; `log.exception()` (or `exc_info=True`) attaches the traceback.
|
|
37
|
+
|
|
38
|
+
You can also log directly:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
dlogify.info("order placed", attributes={"order_id": order_id})
|
|
42
|
+
dlogify.error("charge failed", exc=err, attributes={"order_id": order_id})
|
|
43
|
+
dlogify.log("warning", "disk almost full") # any level name, any case
|
|
44
|
+
dlogify.capture_exception() # inside an except block
|
|
45
|
+
dlogify.flush() # before os._exit()
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`log()` takes `trace`, `debug`, `info`, `warn`, `error` and `fatal`, and also Python's `warning` and `critical`, in any case; an unknown name logs at `info`.
|
|
49
|
+
|
|
50
|
+
- `init(capture_logging=False)` or `init(capture_uncaught=False)` turns a capture off.
|
|
51
|
+
- Calls before `init()` are dropped, and a second `init()` is ignored.
|
|
52
|
+
- `dlogify.Client(**options)` creates an independent client.
|
|
53
|
+
- `dictConfig` users can name the handler instead: `{"class": "dlogify.LogifyHandler"}` with `init(capture_logging=False)`; it sends through the `init()` client. With the capture left on, each record is still sent once.
|
|
54
|
+
- `structlog` and `loguru` can write to the standard `logging` module, which dlogify captures.
|
|
55
|
+
|
|
56
|
+
## Any process: logify run
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
LOGIFY_API_KEY=lgf_live_… pipx run dlogify run -- python manage.py runserver
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
With the package installed, the command is `logify run -- <cmd>`. The output appears in your terminal exactly as without dlogify, and the exit code is the command's. Python tracebacks, stack traces from other languages and Go panics become single events; JSON lines are read as structured records. A non-zero exit or a death by signal is reported as one more error, except a `SIGPIPE` after the reader of the output went away, as in `| head`. Not available on Windows.
|
|
63
|
+
|
|
64
|
+
## Configuration
|
|
65
|
+
|
|
66
|
+
Options passed to `init()` win over environment variables, which win over the defaults.
|
|
67
|
+
|
|
68
|
+
| Variable | Option | Default |
|
|
69
|
+
|---|---|---|
|
|
70
|
+
| `LOGIFY_API_KEY` | `api_key` | Required; without it the client is disabled. |
|
|
71
|
+
| `LOGIFY_ENDPOINT` | `endpoint` | `https://api.dlogify.com` |
|
|
72
|
+
| `LOGIFY_SERVICE` | `service` | `OTEL_SERVICE_NAME`, then the `[project] name` of the nearest `pyproject.toml` from the working directory, then `unknown_service` |
|
|
73
|
+
| `LOGIFY_ENVIRONMENT` | `environment` | `default` |
|
|
74
|
+
| `LOGIFY_RELEASE` | `release` | empty |
|
|
75
|
+
| `LOGIFY_MIN_LEVEL` | `min_level` | `info` (`trace`, `debug`, `info`, `warn`, `error`, `fatal`) |
|
|
76
|
+
| `LOGIFY_DISABLED` | `disabled` | off (`1` or `true` turns the client into a no-op) |
|
|
77
|
+
| `LOGIFY_DEBUG` | `debug` | off (`1` or `true` prints the client's diagnostics to stderr) |
|
|
78
|
+
|
|
79
|
+
Also `attributes` (added to every record) and `max_queue_size` (default 2000). `disabled`, `debug`, `capture_logging` and `capture_uncaught` take `True` or `False`; any other value is ignored with a warning.
|
|
80
|
+
|
|
81
|
+
## Guarantees
|
|
82
|
+
|
|
83
|
+
- No call raises, and logging never waits for the network: records are sent by a background daemon thread, in compressed batches, with retries for up to 5 minutes.
|
|
84
|
+
- If the key is rejected, the client stops for the rest of the process with one warning. Redirects are not followed: one warning names the URL to put in `LOGIFY_ENDPOINT`.
|
|
85
|
+
- If the endpoint's TLS certificate cannot be verified, one warning says how to fix it: with Python from python.org on macOS, run `Install Certificates.command`; behind a proxy that inspects TLS, set `SSL_CERT_FILE` to its CA bundle.
|
|
86
|
+
- After an uncaught exception the client sends what is queued, then Python reports the exception as usual. At normal exit it flushes too. An exit spends at most 2 seconds on this in total. `os._exit()` skips both, so call `dlogify.flush()` first.
|
|
87
|
+
- Works with pre-fork servers such as gunicorn, and with `multiprocessing` children, which flush when their target returns. `Pool.terminate()` (also run when a `with Pool()` block ends) stops workers before they flush: call `pool.close()` and `pool.join()` instead. Other worker pools that end with `os._exit()` should call `dlogify.flush()` first.
|
|
88
|
+
- The client's own messages go to stderr, never through `logging`.
|
|
89
|
+
|
|
90
|
+
## What is sent
|
|
91
|
+
|
|
92
|
+
Only log records: message, level, time, logger name, the attributes you pass, the exception's type, message and traceback, and the service name, environment, release, host name and SDK version. The SDK makes no other network calls and opens no ports. Personal data is redacted by Logify on arrival.
|
|
93
|
+
|
|
94
|
+
Documentation: https://docs.dlogify.com/send-logs/python
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "dlogify"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Send Python logs and uncaught exceptions to dlogify."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
authors = [{ name = "Logify" }]
|
|
10
|
+
keywords = ["logging", "errors", "exceptions", "opentelemetry", "dlogify"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Programming Language :: Python :: 3",
|
|
13
|
+
"Operating System :: OS Independent",
|
|
14
|
+
"Topic :: System :: Logging",
|
|
15
|
+
"Typing :: Typed",
|
|
16
|
+
]
|
|
17
|
+
dependencies = []
|
|
18
|
+
|
|
19
|
+
[project.urls]
|
|
20
|
+
Homepage = "https://dlogify.com"
|
|
21
|
+
Documentation = "https://docs.dlogify.com/send-logs/python"
|
|
22
|
+
|
|
23
|
+
[project.scripts]
|
|
24
|
+
logify = "dlogify.run.cli:main"
|
|
25
|
+
dlogify = "dlogify.run.cli:main"
|
|
26
|
+
|
|
27
|
+
[dependency-groups]
|
|
28
|
+
dev = ["pytest>=8.3"]
|
|
29
|
+
|
|
30
|
+
[build-system]
|
|
31
|
+
requires = ["hatchling>=1.27"]
|
|
32
|
+
build-backend = "hatchling.build"
|
|
33
|
+
|
|
34
|
+
[tool.hatch.build.targets.wheel]
|
|
35
|
+
packages = ["src/dlogify"]
|
|
36
|
+
|
|
37
|
+
[tool.hatch.build.targets.sdist]
|
|
38
|
+
# Anchored: an unanchored "LICENSE" also matches the licenses inside .venv*/.
|
|
39
|
+
include = ["/src/dlogify", "/README.md", "/LICENSE", "/pyproject.toml"]
|
|
40
|
+
|
|
41
|
+
[tool.pytest.ini_options]
|
|
42
|
+
testpaths = ["tests"]
|
|
43
|
+
addopts = "-p no:cacheprovider"
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""dlogify: send Python logs and uncaught exceptions to Logify.
|
|
2
|
+
|
|
3
|
+
import dlogify
|
|
4
|
+
dlogify.init()
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from collections.abc import Mapping
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from . import _singleton
|
|
14
|
+
from ._client import Client
|
|
15
|
+
from ._diagnostics import Diagnostics, is_flag
|
|
16
|
+
from ._version import VERSION as __version__
|
|
17
|
+
from .handler import LogifyHandler
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"init",
|
|
21
|
+
"trace",
|
|
22
|
+
"debug",
|
|
23
|
+
"info",
|
|
24
|
+
"warn",
|
|
25
|
+
"warning",
|
|
26
|
+
"error",
|
|
27
|
+
"fatal",
|
|
28
|
+
"critical",
|
|
29
|
+
"log",
|
|
30
|
+
"capture_exception",
|
|
31
|
+
"flush",
|
|
32
|
+
"shutdown",
|
|
33
|
+
"Client",
|
|
34
|
+
"LogifyHandler",
|
|
35
|
+
"__version__",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
_noted_before_init = False
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _active() -> Optional[Client]:
|
|
42
|
+
global _noted_before_init
|
|
43
|
+
client = _singleton.current
|
|
44
|
+
if client is None and not _noted_before_init:
|
|
45
|
+
_noted_before_init = True
|
|
46
|
+
try:
|
|
47
|
+
Diagnostics(is_flag(os.environ.get("LOGIFY_DEBUG"))).debug("a log call before init() was dropped")
|
|
48
|
+
except Exception:
|
|
49
|
+
pass
|
|
50
|
+
return client
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def init(
|
|
54
|
+
*,
|
|
55
|
+
api_key: Optional[str] = None,
|
|
56
|
+
endpoint: Optional[str] = None,
|
|
57
|
+
service: Optional[str] = None,
|
|
58
|
+
environment: Optional[str] = None,
|
|
59
|
+
release: Optional[str] = None,
|
|
60
|
+
min_level: Optional[str] = None,
|
|
61
|
+
capture_logging: bool = True,
|
|
62
|
+
capture_uncaught: bool = True,
|
|
63
|
+
attributes: Optional[Mapping[str, object]] = None,
|
|
64
|
+
max_queue_size: Optional[int] = None,
|
|
65
|
+
disabled: Optional[bool] = None,
|
|
66
|
+
debug: Optional[bool] = None,
|
|
67
|
+
) -> None:
|
|
68
|
+
"""Starts the client: captures logging and uncaught exceptions unless turned off.
|
|
69
|
+
Safe to call from any thread; a second call is ignored."""
|
|
70
|
+
try:
|
|
71
|
+
with _singleton.lock:
|
|
72
|
+
existing = _singleton.current
|
|
73
|
+
if existing is not None:
|
|
74
|
+
existing.diagnostics.debug("init() was called again; ignored")
|
|
75
|
+
return
|
|
76
|
+
_singleton.current = Client(
|
|
77
|
+
api_key=api_key,
|
|
78
|
+
endpoint=endpoint,
|
|
79
|
+
service=service,
|
|
80
|
+
environment=environment,
|
|
81
|
+
release=release,
|
|
82
|
+
min_level=min_level,
|
|
83
|
+
capture_logging=capture_logging,
|
|
84
|
+
capture_uncaught=capture_uncaught,
|
|
85
|
+
attributes=attributes,
|
|
86
|
+
max_queue_size=max_queue_size,
|
|
87
|
+
disabled=disabled,
|
|
88
|
+
debug=debug,
|
|
89
|
+
)
|
|
90
|
+
except Exception:
|
|
91
|
+
pass # init() never raises
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def trace(message: object, *, exc: Optional[BaseException] = None, attributes: Optional[Mapping[str, object]] = None) -> None:
|
|
95
|
+
client = _active()
|
|
96
|
+
if client is not None:
|
|
97
|
+
client.trace(message, exc=exc, attributes=attributes)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def debug(message: object, *, exc: Optional[BaseException] = None, attributes: Optional[Mapping[str, object]] = None) -> None:
|
|
101
|
+
client = _active()
|
|
102
|
+
if client is not None:
|
|
103
|
+
client.debug(message, exc=exc, attributes=attributes)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def info(message: object, *, exc: Optional[BaseException] = None, attributes: Optional[Mapping[str, object]] = None) -> None:
|
|
107
|
+
client = _active()
|
|
108
|
+
if client is not None:
|
|
109
|
+
client.info(message, exc=exc, attributes=attributes)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def warn(message: object, *, exc: Optional[BaseException] = None, attributes: Optional[Mapping[str, object]] = None) -> None:
|
|
113
|
+
client = _active()
|
|
114
|
+
if client is not None:
|
|
115
|
+
client.warn(message, exc=exc, attributes=attributes)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
warning = warn
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def error(message: object, *, exc: Optional[BaseException] = None, attributes: Optional[Mapping[str, object]] = None) -> None:
|
|
122
|
+
client = _active()
|
|
123
|
+
if client is not None:
|
|
124
|
+
client.error(message, exc=exc, attributes=attributes)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def fatal(message: object, *, exc: Optional[BaseException] = None, attributes: Optional[Mapping[str, object]] = None) -> None:
|
|
128
|
+
client = _active()
|
|
129
|
+
if client is not None:
|
|
130
|
+
client.fatal(message, exc=exc, attributes=attributes)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
critical = fatal
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def log(level: str, message: object, *, exc: Optional[BaseException] = None, attributes: Optional[Mapping[str, object]] = None) -> None:
|
|
137
|
+
client = _active()
|
|
138
|
+
if client is not None:
|
|
139
|
+
client.log(level, message, exc=exc, attributes=attributes)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def capture_exception(exc: Optional[BaseException] = None, *, attributes: Optional[Mapping[str, object]] = None) -> None:
|
|
143
|
+
client = _active()
|
|
144
|
+
if client is not None:
|
|
145
|
+
client.capture_exception(exc, attributes=attributes)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def flush(timeout: float = 2.0) -> None:
|
|
149
|
+
client = _singleton.current
|
|
150
|
+
if client is not None:
|
|
151
|
+
client.flush(timeout)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def shutdown(timeout: float = 2.0) -> None:
|
|
155
|
+
"""Flushes, stops and restores what init() installed; init() may run again."""
|
|
156
|
+
with _singleton.lock:
|
|
157
|
+
client = _singleton.current
|
|
158
|
+
_singleton.current = None
|
|
159
|
+
if client is not None:
|
|
160
|
+
client.shutdown(timeout)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""init() hands every logging record to its LogifyHandler by wrapping
|
|
2
|
+
Logger.callHandlers instead of adding a handler to the root logger: a root
|
|
3
|
+
logger without handlers keeps logging.basicConfig() and logging.lastResort
|
|
4
|
+
working (spec D3)."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import threading
|
|
10
|
+
from typing import Callable
|
|
11
|
+
|
|
12
|
+
_lock = threading.Lock()
|
|
13
|
+
_handlers: list[logging.Handler] = []
|
|
14
|
+
# The wrapper we installed last, while Logger.callHandlers is still it.
|
|
15
|
+
_installed: "Callable | None" = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _wrap(previous: Callable) -> Callable:
|
|
19
|
+
def call_handlers(self: logging.Logger, record: logging.LogRecord) -> None:
|
|
20
|
+
try:
|
|
21
|
+
previous(self, record)
|
|
22
|
+
finally:
|
|
23
|
+
# Another library may have wrapped us and been wrapped again: each
|
|
24
|
+
# record reaches our handlers once.
|
|
25
|
+
if not getattr(record, "_dlogify_seen", False):
|
|
26
|
+
try:
|
|
27
|
+
record._dlogify_seen = True
|
|
28
|
+
for handler in tuple(_handlers):
|
|
29
|
+
if record.levelno >= handler.level:
|
|
30
|
+
handler.handle(record)
|
|
31
|
+
except Exception:
|
|
32
|
+
pass # never break the application's logging call
|
|
33
|
+
|
|
34
|
+
call_handlers._dlogify_previous = previous # type: ignore[attr-defined]
|
|
35
|
+
return call_handlers
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def install_logging_capture(handler: logging.Handler) -> Callable[[], None]:
|
|
39
|
+
global _installed
|
|
40
|
+
with _lock:
|
|
41
|
+
current = logging.Logger.callHandlers
|
|
42
|
+
if not hasattr(current, "_dlogify_previous"):
|
|
43
|
+
_installed = _wrap(current)
|
|
44
|
+
logging.Logger.callHandlers = _installed # type: ignore[method-assign]
|
|
45
|
+
_handlers.append(handler)
|
|
46
|
+
|
|
47
|
+
def undo() -> None:
|
|
48
|
+
global _installed
|
|
49
|
+
with _lock:
|
|
50
|
+
if handler in _handlers:
|
|
51
|
+
_handlers.remove(handler)
|
|
52
|
+
if not _handlers and _installed is not None and logging.Logger.callHandlers is _installed:
|
|
53
|
+
logging.Logger.callHandlers = _installed._dlogify_previous # type: ignore[attr-defined,method-assign]
|
|
54
|
+
_installed = None
|
|
55
|
+
|
|
56
|
+
return undo
|