bunnylogs 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.
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
environment: pypi
|
|
12
|
+
permissions:
|
|
13
|
+
id-token: write # required for trusted publishing (no API token needed)
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: "3.12"
|
|
21
|
+
|
|
22
|
+
- name: Build
|
|
23
|
+
run: |
|
|
24
|
+
pip install build
|
|
25
|
+
python -m build
|
|
26
|
+
|
|
27
|
+
- name: Publish
|
|
28
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
bunnylogs-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bunnylogs
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python logging handler for BunnyLogs
|
|
5
|
+
Project-URL: Homepage, https://bunnylogs.com
|
|
6
|
+
Project-URL: Repository, https://github.com/RubenNorgaard/bunnylogs-python
|
|
7
|
+
License: MIT
|
|
8
|
+
Keywords: bunnylogs,log handler,logging
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: System :: Logging
|
|
19
|
+
Requires-Python: >=3.8
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# bunnylogs
|
|
23
|
+
|
|
24
|
+
Python logging handler for [BunnyLogs](https://bunnylogs.com) — ship your logs to a live stream with three lines of code.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install bunnylogs
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
import logging
|
|
36
|
+
from bunnylogs import BunnyLogsHandler
|
|
37
|
+
|
|
38
|
+
logging.getLogger().addHandler(BunnyLogsHandler("your-uuid-here"))
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
That's it. Every log record at `WARNING` and above (or whatever level your root logger is set to) will appear in your BunnyLogs stream in real time.
|
|
42
|
+
|
|
43
|
+
### Capture a specific logger
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import logging
|
|
47
|
+
from bunnylogs import BunnyLogsHandler
|
|
48
|
+
|
|
49
|
+
handler = BunnyLogsHandler("your-uuid-here")
|
|
50
|
+
handler.setLevel(logging.DEBUG)
|
|
51
|
+
|
|
52
|
+
logger = logging.getLogger("myapp")
|
|
53
|
+
logger.addHandler(handler)
|
|
54
|
+
logger.setLevel(logging.DEBUG)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Django — `settings.py`
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
LOGGING = {
|
|
61
|
+
"version": 1,
|
|
62
|
+
"handlers": {
|
|
63
|
+
"bunnylogs": {
|
|
64
|
+
"class": "bunnylogs.BunnyLogsHandler",
|
|
65
|
+
"uuid": "your-uuid-here",
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
"root": {
|
|
69
|
+
"handlers": ["bunnylogs"],
|
|
70
|
+
"level": "WARNING",
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Self-hosted deployments
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
BunnyLogsHandler("your-uuid-here", endpoint="https://your-own-host.com")
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## How it works
|
|
82
|
+
|
|
83
|
+
Log records are placed on an in-process queue and flushed by a daemon thread, so `emit()` is non-blocking (~microseconds on the calling thread). The background thread sends one HTTPS POST per record using only the Python standard library — no external dependencies.
|
|
84
|
+
|
|
85
|
+
On process exit the daemon thread is killed. Call `handler.close()` explicitly if you need to guarantee all queued records are flushed before shutdown.
|
|
86
|
+
|
|
87
|
+
## Parameters
|
|
88
|
+
|
|
89
|
+
| Parameter | Default | Description |
|
|
90
|
+
|------------|----------------------------|------------------------------------------|
|
|
91
|
+
| `uuid` | — | Your logspace UUID |
|
|
92
|
+
| `level` | `logging.NOTSET` | Minimum level to forward |
|
|
93
|
+
| `endpoint` | `https://bunnylogs.com` | Base URL (override for self-hosted) |
|
|
94
|
+
| `timeout` | `5` | HTTP request timeout in seconds |
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
|
|
98
|
+
MIT
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# bunnylogs
|
|
2
|
+
|
|
3
|
+
Python logging handler for [BunnyLogs](https://bunnylogs.com) — ship your logs to a live stream with three lines of code.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install bunnylogs
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
import logging
|
|
15
|
+
from bunnylogs import BunnyLogsHandler
|
|
16
|
+
|
|
17
|
+
logging.getLogger().addHandler(BunnyLogsHandler("your-uuid-here"))
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
That's it. Every log record at `WARNING` and above (or whatever level your root logger is set to) will appear in your BunnyLogs stream in real time.
|
|
21
|
+
|
|
22
|
+
### Capture a specific logger
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
import logging
|
|
26
|
+
from bunnylogs import BunnyLogsHandler
|
|
27
|
+
|
|
28
|
+
handler = BunnyLogsHandler("your-uuid-here")
|
|
29
|
+
handler.setLevel(logging.DEBUG)
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger("myapp")
|
|
32
|
+
logger.addHandler(handler)
|
|
33
|
+
logger.setLevel(logging.DEBUG)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Django — `settings.py`
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
LOGGING = {
|
|
40
|
+
"version": 1,
|
|
41
|
+
"handlers": {
|
|
42
|
+
"bunnylogs": {
|
|
43
|
+
"class": "bunnylogs.BunnyLogsHandler",
|
|
44
|
+
"uuid": "your-uuid-here",
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
"root": {
|
|
48
|
+
"handlers": ["bunnylogs"],
|
|
49
|
+
"level": "WARNING",
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Self-hosted deployments
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
BunnyLogsHandler("your-uuid-here", endpoint="https://your-own-host.com")
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## How it works
|
|
61
|
+
|
|
62
|
+
Log records are placed on an in-process queue and flushed by a daemon thread, so `emit()` is non-blocking (~microseconds on the calling thread). The background thread sends one HTTPS POST per record using only the Python standard library — no external dependencies.
|
|
63
|
+
|
|
64
|
+
On process exit the daemon thread is killed. Call `handler.close()` explicitly if you need to guarantee all queued records are flushed before shutdown.
|
|
65
|
+
|
|
66
|
+
## Parameters
|
|
67
|
+
|
|
68
|
+
| Parameter | Default | Description |
|
|
69
|
+
|------------|----------------------------|------------------------------------------|
|
|
70
|
+
| `uuid` | — | Your logspace UUID |
|
|
71
|
+
| `level` | `logging.NOTSET` | Minimum level to forward |
|
|
72
|
+
| `endpoint` | `https://bunnylogs.com` | Base URL (override for self-hosted) |
|
|
73
|
+
| `timeout` | `5` | HTTP request timeout in seconds |
|
|
74
|
+
|
|
75
|
+
## License
|
|
76
|
+
|
|
77
|
+
MIT
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "bunnylogs"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python logging handler for BunnyLogs"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.8"
|
|
12
|
+
dependencies = []
|
|
13
|
+
keywords = ["logging", "bunnylogs", "log handler"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.8",
|
|
20
|
+
"Programming Language :: Python :: 3.9",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Topic :: System :: Logging",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://bunnylogs.com"
|
|
29
|
+
Repository = "https://github.com/RubenNorgaard/bunnylogs-python"
|
|
30
|
+
|
|
31
|
+
[tool.hatch.build.targets.wheel]
|
|
32
|
+
packages = ["src/bunnylogs"]
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BunnyLogs logging handler.
|
|
3
|
+
|
|
4
|
+
Sends log records to a BunnyLogs stream endpoint in a background thread so
|
|
5
|
+
that logging calls never block the caller.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import queue
|
|
10
|
+
import threading
|
|
11
|
+
import urllib.error
|
|
12
|
+
import urllib.parse
|
|
13
|
+
import urllib.request
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
_STOP = object() # sentinel that tells the worker thread to exit
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BunnyLogsHandler(logging.Handler):
|
|
21
|
+
"""
|
|
22
|
+
A logging.Handler that ships records to a BunnyLogs live stream.
|
|
23
|
+
|
|
24
|
+
Records are placed on an in-process queue and flushed by a daemon thread,
|
|
25
|
+
so ``emit()`` is non-blocking (~microseconds).
|
|
26
|
+
|
|
27
|
+
Usage::
|
|
28
|
+
|
|
29
|
+
import logging
|
|
30
|
+
from bunnylogs import BunnyLogsHandler
|
|
31
|
+
|
|
32
|
+
logging.getLogger().addHandler(BunnyLogsHandler("your-uuid-here"))
|
|
33
|
+
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
uuid:
|
|
37
|
+
The logspace UUID shown in your BunnyLogs stream URL.
|
|
38
|
+
level:
|
|
39
|
+
Minimum log level to forward (default: ``logging.NOTSET``, i.e. all).
|
|
40
|
+
endpoint:
|
|
41
|
+
Override the base URL (default: ``https://bunnylogs.com``).
|
|
42
|
+
Useful for self-hosted deployments.
|
|
43
|
+
timeout:
|
|
44
|
+
HTTP request timeout in seconds (default: 5).
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
uuid: str,
|
|
50
|
+
level: int = logging.NOTSET,
|
|
51
|
+
endpoint: str = "https://bunnylogs.com",
|
|
52
|
+
timeout: float = 5,
|
|
53
|
+
) -> None:
|
|
54
|
+
super().__init__(level)
|
|
55
|
+
self._url = f"{endpoint.rstrip('/')}/live/{uuid}/"
|
|
56
|
+
self._timeout = timeout
|
|
57
|
+
self._queue: queue.SimpleQueue = queue.SimpleQueue()
|
|
58
|
+
self._thread = threading.Thread(
|
|
59
|
+
target=self._worker,
|
|
60
|
+
name="bunnylogs-worker",
|
|
61
|
+
daemon=True, # won't block process exit
|
|
62
|
+
)
|
|
63
|
+
self._thread.start()
|
|
64
|
+
|
|
65
|
+
# ------------------------------------------------------------------
|
|
66
|
+
# logging.Handler interface
|
|
67
|
+
# ------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
70
|
+
"""Put the record on the queue; returns immediately."""
|
|
71
|
+
try:
|
|
72
|
+
self._queue.put_nowait(record)
|
|
73
|
+
except Exception:
|
|
74
|
+
self.handleError(record)
|
|
75
|
+
|
|
76
|
+
def close(self) -> None:
|
|
77
|
+
"""Flush remaining records then shut down the worker thread."""
|
|
78
|
+
self._queue.put_nowait(_STOP)
|
|
79
|
+
# Give the worker up to 5 s to drain before the handler is torn down.
|
|
80
|
+
self._thread.join(timeout=5)
|
|
81
|
+
super().close()
|
|
82
|
+
|
|
83
|
+
# ------------------------------------------------------------------
|
|
84
|
+
# Background worker
|
|
85
|
+
# ------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
def _worker(self) -> None:
|
|
88
|
+
while True:
|
|
89
|
+
item = self._queue.get()
|
|
90
|
+
if item is _STOP:
|
|
91
|
+
return
|
|
92
|
+
self._send(item)
|
|
93
|
+
|
|
94
|
+
def _send(self, record: logging.LogRecord) -> None:
|
|
95
|
+
try:
|
|
96
|
+
ts = datetime.fromtimestamp(record.created, tz=timezone.utc).isoformat()
|
|
97
|
+
data = urllib.parse.urlencode(
|
|
98
|
+
{
|
|
99
|
+
"message": self.format(record),
|
|
100
|
+
"level": record.levelname,
|
|
101
|
+
"program": record.name,
|
|
102
|
+
"timestamp": ts,
|
|
103
|
+
}
|
|
104
|
+
).encode()
|
|
105
|
+
req = urllib.request.Request(
|
|
106
|
+
self._url,
|
|
107
|
+
data=data,
|
|
108
|
+
method="POST",
|
|
109
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
110
|
+
)
|
|
111
|
+
urllib.request.urlopen(req, timeout=self._timeout)
|
|
112
|
+
except Exception:
|
|
113
|
+
self.handleError(record)
|