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
@@ -0,0 +1,7 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
7
+ *.egg
@@ -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,4 @@
1
+ from .handler import BunnyLogsHandler
2
+
3
+ __all__ = ["BunnyLogsHandler"]
4
+ __version__ = "0.1.0"
@@ -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)