tailcove 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,59 @@
1
+ name: Bug report
2
+ description: Something in tailcove isn't working as expected.
3
+ labels: [bug]
4
+ body:
5
+ - type: markdown
6
+ attributes:
7
+ value: |
8
+ Thanks for taking the time to report this. The more detail you give, the faster we can fix it.
9
+
10
+ **Never paste your ingest token or browser key.** Replace it with `***`.
11
+ - type: textarea
12
+ id: what-happened
13
+ attributes:
14
+ label: What happened?
15
+ description: Tell us what you saw, and what you expected to see instead.
16
+ validations:
17
+ required: true
18
+ - type: textarea
19
+ id: reproduce
20
+ attributes:
21
+ label: How can we reproduce it?
22
+ description: The smallest code or steps that show the problem.
23
+ render: python
24
+ validations:
25
+ required: true
26
+ - type: input
27
+ id: version
28
+ attributes:
29
+ label: tailcove version
30
+ placeholder: "0.1.1"
31
+ validations:
32
+ required: true
33
+ - type: input
34
+ id: runtime
35
+ attributes:
36
+ label: Python version
37
+ placeholder: "3.12.4"
38
+ validations:
39
+ required: true
40
+ - type: input
41
+ id: framework
42
+ attributes:
43
+ label: Framework (optional)
44
+ placeholder: "Django 5.1, Flask 3.0"
45
+ - type: textarea
46
+ id: logs
47
+ attributes:
48
+ label: Errors or output
49
+ description: Any error message, stack trace or console output. This is formatted as code automatically.
50
+ render: shell
51
+ - type: checkboxes
52
+ id: checks
53
+ attributes:
54
+ label: Before submitting
55
+ options:
56
+ - label: I searched existing issues and didn't find a duplicate.
57
+ required: true
58
+ - label: I removed any tokens, keys and private data from this report.
59
+ required: true
@@ -0,0 +1,8 @@
1
+ blank_issues_enabled: false
2
+ contact_links:
3
+ - name: Documentation
4
+ url: https://tailcove.dev/docs/sending-logs/python
5
+ about: Setup guides and answers to common questions.
6
+ - name: Security vulnerability
7
+ url: mailto:security@tailcove.dev
8
+ about: Please email us privately instead of opening a public issue.
@@ -0,0 +1,30 @@
1
+ name: Feature request
2
+ description: Suggest an improvement to tailcove.
3
+ labels: [enhancement]
4
+ body:
5
+ - type: textarea
6
+ id: problem
7
+ attributes:
8
+ label: What problem are you trying to solve?
9
+ description: Describe what you're trying to do and where tailcove gets in the way.
10
+ validations:
11
+ required: true
12
+ - type: textarea
13
+ id: solution
14
+ attributes:
15
+ label: What would you like to see?
16
+ description: How you imagine it working. Example code is welcome.
17
+ validations:
18
+ required: true
19
+ - type: textarea
20
+ id: alternatives
21
+ attributes:
22
+ label: Alternatives you've considered
23
+ description: Other approaches or workarounds you've tried.
24
+ - type: checkboxes
25
+ id: checks
26
+ attributes:
27
+ label: Before submitting
28
+ options:
29
+ - label: I searched existing issues and didn't find a duplicate.
30
+ required: true
@@ -0,0 +1,58 @@
1
+ name: publish
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ build:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+
17
+ - uses: actions/setup-python@v5
18
+ with:
19
+ python-version: '3.12'
20
+
21
+ - name: Check tag matches package version
22
+ run: |
23
+ version="$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")"
24
+ if [ "v$version" != "$GITHUB_REF_NAME" ]; then
25
+ echo "Tag $GITHUB_REF_NAME does not match pyproject.toml version $version" >&2
26
+ exit 1
27
+ fi
28
+
29
+ - name: Test
30
+ run: |
31
+ python -m pip install --upgrade pip
32
+ python -m pip install -e '.[dev]'
33
+ python -m pytest
34
+
35
+ - name: Build
36
+ run: |
37
+ python -m pip install build twine
38
+ python -m build
39
+ python -m twine check dist/*
40
+
41
+ - uses: actions/upload-artifact@v4
42
+ with:
43
+ name: dist
44
+ path: dist/
45
+
46
+ publish:
47
+ needs: build
48
+ runs-on: ubuntu-latest
49
+ environment: pypi
50
+ permissions:
51
+ id-token: write
52
+ steps:
53
+ - uses: actions/download-artifact@v4
54
+ with:
55
+ name: dist
56
+ path: dist/
57
+
58
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,6 @@
1
+ __pycache__/
2
+ *.egg-info/
3
+ .pytest_cache/
4
+ .venv/
5
+ dist/
6
+ build/
tailcove-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Akcybex
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.
@@ -0,0 +1,201 @@
1
+ Metadata-Version: 2.5
2
+ Name: tailcove
3
+ Version: 0.1.0
4
+ Summary: Python client for shipping application logs to Tailcove — a stdlib logging.Handler that batches and ships NDJSON.
5
+ Project-URL: Homepage, https://tailcove.dev
6
+ Project-URL: Documentation, https://tailcove.dev/docs/sending-logs/python
7
+ Project-URL: Source, https://github.com/tailcove/python
8
+ Project-URL: Issues, https://github.com/tailcove/python/issues
9
+ Author-email: Akcybex <developers@akcybex.com>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: logging,monolog,ndjson,tailcove
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: System :: Logging
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.9
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=7; extra == 'dev'
25
+ Description-Content-Type: text/markdown
26
+
27
+ <h1 align="center">tailcove</h1>
28
+
29
+ <p align="center">
30
+ Send Python logs to <a href="https://tailcove.dev">Tailcove</a> with the standard <code>logging</code> module.<br>
31
+ One handler, no dependencies, and it never slows down your code.
32
+ </p>
33
+
34
+ <p align="center">
35
+ <a href="https://pypi.org/project/tailcove/"><img alt="PyPI version" src="https://img.shields.io/pypi/v/tailcove?color=2DD4BF&labelColor=0A0A0A"></a>
36
+ <a href="https://pypi.org/project/tailcove/"><img alt="Python versions" src="https://img.shields.io/pypi/pyversions/tailcove?color=2DD4BF&labelColor=0A0A0A"></a>
37
+ <a href="https://pypi.org/project/tailcove/"><img alt="PyPI downloads" src="https://img.shields.io/pypi/dm/tailcove?color=2DD4BF&labelColor=0A0A0A"></a>
38
+ <a href="LICENSE"><img alt="MIT license" src="https://img.shields.io/badge/license-MIT-2DD4BF?labelColor=0A0A0A"></a>
39
+ </p>
40
+
41
+ <p align="center">
42
+ <a href="https://tailcove.dev/docs/sending-logs/python">Documentation</a> ·
43
+ <a href="https://github.com/tailcove/python/issues">Report a bug</a> ·
44
+ <a href="https://app.tailcove.dev/register">Get started free</a>
45
+ </p>
46
+
47
+ ---
48
+
49
+ ## Why use it
50
+
51
+ - **Plain `logging`.** `TailcoveHandler` is a normal `logging.Handler`. Keep the loggers you already have.
52
+ - **Never blocks.** Records are batched and sent from a background thread, so a request or a hot loop never waits on the network.
53
+ - **No dependencies.** Delivery uses `urllib` from the standard library.
54
+ - **Safe.** A failed delivery is caught and never raised into your code.
55
+ - **Structured exceptions.** Log an exception and it arrives as data (class, message, location, trace), so identical errors group together.
56
+
57
+ ## Install
58
+
59
+ ```bash
60
+ pip install tailcove
61
+ ```
62
+
63
+ Requires Python 3.9 or newer.
64
+
65
+ ## Quick start
66
+
67
+ **1. Get a token.** Create a source in your [Tailcove dashboard](https://app.tailcove.dev) and copy its ingest token. It is shown only once, so save it somewhere safe (an environment variable is ideal).
68
+
69
+ **2. Add the handler.**
70
+
71
+ ```python
72
+ import logging
73
+ import os
74
+
75
+ from tailcove import TailcoveHandler
76
+
77
+ logger = logging.getLogger("myapp")
78
+ logger.addHandler(TailcoveHandler(token=os.environ["TAILCOVE_TOKEN"]))
79
+ logger.setLevel(logging.INFO)
80
+
81
+ logger.info("user signed up", extra={"user_id": 42})
82
+ logger.error("payment failed", extra={"order_id": 99, "reason": "card_declined"})
83
+ ```
84
+
85
+ **3. Open Tailcove.** Your records appear in search and live tail within a couple of seconds.
86
+
87
+ Buffered records are flushed when your program exits normally, because Python's own `logging.shutdown()` closes every handler.
88
+
89
+ ## Options
90
+
91
+ | Argument | Default | What it does |
92
+ |---|---|---|
93
+ | `token` | — | Your source's ingest token. **Required.** |
94
+ | `endpoint` | `https://ingest.tailcove.dev/api/v1/logs` | The ingest URL. Only change this if you self-host. |
95
+ | `batch_size` | `50` | Send once this many records are buffered. |
96
+ | `batch_seconds` | `2.0` | Send once this many seconds have passed since the first buffered record. |
97
+ | `timeout` | `2.0` | Seconds a delivery attempt may take before giving up. |
98
+ | `trace_limit` | `10` | Most stack frames kept when a logged exception is rendered. |
99
+ | `level` | `logging.NOTSET` | Lowest level this handler processes, on top of the logger's own level. |
100
+
101
+ Records are sent once `batch_size` is reached or `batch_seconds` has passed, whichever comes first. They are never sent one HTTP request per log call. To send whatever is buffered right now, call `handler.flush()`. It blocks until the background thread confirms.
102
+
103
+ ## Structured context
104
+
105
+ Anything you pass in `extra={...}` is shipped as the line's context, with the same keys:
106
+
107
+ ```python
108
+ logger.info("order placed", extra={"order_id": 99, "total_cents": 4200})
109
+ ```
110
+
111
+ ## Exceptions
112
+
113
+ Log with `exc_info=True`, or use `logger.exception(...)` inside an `except` block, and the exception is sent as structured data and not as a flattened string:
114
+
115
+ ```python
116
+ try:
117
+ charge_card(order)
118
+ except CardDeclined:
119
+ logger.error("payment failed", exc_info=True)
120
+ ```
121
+
122
+ ```json
123
+ "exception": {
124
+ "class": "billing.errors.CardDeclined",
125
+ "message": "insufficient funds",
126
+ "location": "/app/billing/charge.py:42",
127
+ "trace": [
128
+ "/app/billing/charge.py:42 in charge_card",
129
+ "/app/orders/service.py:17 in place_order"
130
+ ]
131
+ }
132
+ ```
133
+
134
+ `location` is where the exception was raised, and `trace` is limited to `trace_limit` frames. It's the same shape the Laravel, Node and browser libraries use, so the same error groups into one issue whichever app reported it.
135
+
136
+ ## Frameworks
137
+
138
+ ### Django
139
+
140
+ Add the handler to `LOGGING` in `settings.py`:
141
+
142
+ ```python
143
+ LOGGING = {
144
+ "version": 1,
145
+ "disable_existing_loggers": False,
146
+ "handlers": {
147
+ "tailcove": {
148
+ "class": "tailcove.TailcoveHandler",
149
+ "token": env("TAILCOVE_TOKEN"),
150
+ },
151
+ },
152
+ "root": {
153
+ "handlers": ["console", "tailcove"],
154
+ "level": "INFO",
155
+ },
156
+ "loggers": {
157
+ "django.request": {
158
+ "handlers": ["tailcove"],
159
+ "level": "ERROR",
160
+ "propagate": True,
161
+ },
162
+ },
163
+ }
164
+ ```
165
+
166
+ ### Flask
167
+
168
+ Attach the handler to the app logger, or to the root logger if you want every `logging.getLogger(__name__)` in the app to ship too:
169
+
170
+ ```python
171
+ from flask import Flask
172
+ from tailcove import TailcoveHandler
173
+
174
+ app = Flask(__name__)
175
+ app.logger.addHandler(TailcoveHandler(token=app.config["TAILCOVE_TOKEN"]))
176
+ app.logger.setLevel("INFO")
177
+
178
+
179
+ @app.errorhandler(Exception)
180
+ def handle_exception(exc):
181
+ app.logger.error("unhandled exception", exc_info=exc)
182
+ raise
183
+ ```
184
+
185
+ ## When delivery fails
186
+
187
+ Network errors and non-2xx responses never raise into your code. `emit()` follows the standard `Handler` convention and routes its own failures through `handleError()`. Errors on the background thread are caught and printed to stderr. Logging is best-effort by design.
188
+
189
+ ## Help and feedback
190
+
191
+ - **Docs:** [tailcove.dev/docs/sending-logs/python](https://tailcove.dev/docs/sending-logs/python)
192
+ - **Bugs and feature requests:** [open an issue](https://github.com/tailcove/python/issues/new/choose)
193
+ - **Security problems:** please email **security@tailcove.dev** instead of opening a public issue.
194
+
195
+ ## Other Tailcove libraries
196
+
197
+ [Laravel](https://github.com/tailcove/laravel) · [Node.js](https://github.com/tailcove/node) · [Browser](https://github.com/tailcove/browser)
198
+
199
+ ## License
200
+
201
+ [MIT](LICENSE) © Akcybex
@@ -0,0 +1,175 @@
1
+ <h1 align="center">tailcove</h1>
2
+
3
+ <p align="center">
4
+ Send Python logs to <a href="https://tailcove.dev">Tailcove</a> with the standard <code>logging</code> module.<br>
5
+ One handler, no dependencies, and it never slows down your code.
6
+ </p>
7
+
8
+ <p align="center">
9
+ <a href="https://pypi.org/project/tailcove/"><img alt="PyPI version" src="https://img.shields.io/pypi/v/tailcove?color=2DD4BF&labelColor=0A0A0A"></a>
10
+ <a href="https://pypi.org/project/tailcove/"><img alt="Python versions" src="https://img.shields.io/pypi/pyversions/tailcove?color=2DD4BF&labelColor=0A0A0A"></a>
11
+ <a href="https://pypi.org/project/tailcove/"><img alt="PyPI downloads" src="https://img.shields.io/pypi/dm/tailcove?color=2DD4BF&labelColor=0A0A0A"></a>
12
+ <a href="LICENSE"><img alt="MIT license" src="https://img.shields.io/badge/license-MIT-2DD4BF?labelColor=0A0A0A"></a>
13
+ </p>
14
+
15
+ <p align="center">
16
+ <a href="https://tailcove.dev/docs/sending-logs/python">Documentation</a> ·
17
+ <a href="https://github.com/tailcove/python/issues">Report a bug</a> ·
18
+ <a href="https://app.tailcove.dev/register">Get started free</a>
19
+ </p>
20
+
21
+ ---
22
+
23
+ ## Why use it
24
+
25
+ - **Plain `logging`.** `TailcoveHandler` is a normal `logging.Handler`. Keep the loggers you already have.
26
+ - **Never blocks.** Records are batched and sent from a background thread, so a request or a hot loop never waits on the network.
27
+ - **No dependencies.** Delivery uses `urllib` from the standard library.
28
+ - **Safe.** A failed delivery is caught and never raised into your code.
29
+ - **Structured exceptions.** Log an exception and it arrives as data (class, message, location, trace), so identical errors group together.
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ pip install tailcove
35
+ ```
36
+
37
+ Requires Python 3.9 or newer.
38
+
39
+ ## Quick start
40
+
41
+ **1. Get a token.** Create a source in your [Tailcove dashboard](https://app.tailcove.dev) and copy its ingest token. It is shown only once, so save it somewhere safe (an environment variable is ideal).
42
+
43
+ **2. Add the handler.**
44
+
45
+ ```python
46
+ import logging
47
+ import os
48
+
49
+ from tailcove import TailcoveHandler
50
+
51
+ logger = logging.getLogger("myapp")
52
+ logger.addHandler(TailcoveHandler(token=os.environ["TAILCOVE_TOKEN"]))
53
+ logger.setLevel(logging.INFO)
54
+
55
+ logger.info("user signed up", extra={"user_id": 42})
56
+ logger.error("payment failed", extra={"order_id": 99, "reason": "card_declined"})
57
+ ```
58
+
59
+ **3. Open Tailcove.** Your records appear in search and live tail within a couple of seconds.
60
+
61
+ Buffered records are flushed when your program exits normally, because Python's own `logging.shutdown()` closes every handler.
62
+
63
+ ## Options
64
+
65
+ | Argument | Default | What it does |
66
+ |---|---|---|
67
+ | `token` | — | Your source's ingest token. **Required.** |
68
+ | `endpoint` | `https://ingest.tailcove.dev/api/v1/logs` | The ingest URL. Only change this if you self-host. |
69
+ | `batch_size` | `50` | Send once this many records are buffered. |
70
+ | `batch_seconds` | `2.0` | Send once this many seconds have passed since the first buffered record. |
71
+ | `timeout` | `2.0` | Seconds a delivery attempt may take before giving up. |
72
+ | `trace_limit` | `10` | Most stack frames kept when a logged exception is rendered. |
73
+ | `level` | `logging.NOTSET` | Lowest level this handler processes, on top of the logger's own level. |
74
+
75
+ Records are sent once `batch_size` is reached or `batch_seconds` has passed, whichever comes first. They are never sent one HTTP request per log call. To send whatever is buffered right now, call `handler.flush()`. It blocks until the background thread confirms.
76
+
77
+ ## Structured context
78
+
79
+ Anything you pass in `extra={...}` is shipped as the line's context, with the same keys:
80
+
81
+ ```python
82
+ logger.info("order placed", extra={"order_id": 99, "total_cents": 4200})
83
+ ```
84
+
85
+ ## Exceptions
86
+
87
+ Log with `exc_info=True`, or use `logger.exception(...)` inside an `except` block, and the exception is sent as structured data and not as a flattened string:
88
+
89
+ ```python
90
+ try:
91
+ charge_card(order)
92
+ except CardDeclined:
93
+ logger.error("payment failed", exc_info=True)
94
+ ```
95
+
96
+ ```json
97
+ "exception": {
98
+ "class": "billing.errors.CardDeclined",
99
+ "message": "insufficient funds",
100
+ "location": "/app/billing/charge.py:42",
101
+ "trace": [
102
+ "/app/billing/charge.py:42 in charge_card",
103
+ "/app/orders/service.py:17 in place_order"
104
+ ]
105
+ }
106
+ ```
107
+
108
+ `location` is where the exception was raised, and `trace` is limited to `trace_limit` frames. It's the same shape the Laravel, Node and browser libraries use, so the same error groups into one issue whichever app reported it.
109
+
110
+ ## Frameworks
111
+
112
+ ### Django
113
+
114
+ Add the handler to `LOGGING` in `settings.py`:
115
+
116
+ ```python
117
+ LOGGING = {
118
+ "version": 1,
119
+ "disable_existing_loggers": False,
120
+ "handlers": {
121
+ "tailcove": {
122
+ "class": "tailcove.TailcoveHandler",
123
+ "token": env("TAILCOVE_TOKEN"),
124
+ },
125
+ },
126
+ "root": {
127
+ "handlers": ["console", "tailcove"],
128
+ "level": "INFO",
129
+ },
130
+ "loggers": {
131
+ "django.request": {
132
+ "handlers": ["tailcove"],
133
+ "level": "ERROR",
134
+ "propagate": True,
135
+ },
136
+ },
137
+ }
138
+ ```
139
+
140
+ ### Flask
141
+
142
+ Attach the handler to the app logger, or to the root logger if you want every `logging.getLogger(__name__)` in the app to ship too:
143
+
144
+ ```python
145
+ from flask import Flask
146
+ from tailcove import TailcoveHandler
147
+
148
+ app = Flask(__name__)
149
+ app.logger.addHandler(TailcoveHandler(token=app.config["TAILCOVE_TOKEN"]))
150
+ app.logger.setLevel("INFO")
151
+
152
+
153
+ @app.errorhandler(Exception)
154
+ def handle_exception(exc):
155
+ app.logger.error("unhandled exception", exc_info=exc)
156
+ raise
157
+ ```
158
+
159
+ ## When delivery fails
160
+
161
+ Network errors and non-2xx responses never raise into your code. `emit()` follows the standard `Handler` convention and routes its own failures through `handleError()`. Errors on the background thread are caught and printed to stderr. Logging is best-effort by design.
162
+
163
+ ## Help and feedback
164
+
165
+ - **Docs:** [tailcove.dev/docs/sending-logs/python](https://tailcove.dev/docs/sending-logs/python)
166
+ - **Bugs and feature requests:** [open an issue](https://github.com/tailcove/python/issues/new/choose)
167
+ - **Security problems:** please email **security@tailcove.dev** instead of opening a public issue.
168
+
169
+ ## Other Tailcove libraries
170
+
171
+ [Laravel](https://github.com/tailcove/laravel) · [Node.js](https://github.com/tailcove/node) · [Browser](https://github.com/tailcove/browser)
172
+
173
+ ## License
174
+
175
+ [MIT](LICENSE) © Akcybex
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "tailcove"
7
+ version = "0.1.0"
8
+ description = "Python client for shipping application logs to Tailcove — a stdlib logging.Handler that batches and ships NDJSON."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.9"
13
+ keywords = ["logging", "tailcove", "monolog", "ndjson"]
14
+ authors = [{ name = "Akcybex", email = "developers@akcybex.com" }]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Developers",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.9",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Topic :: System :: Logging",
24
+ "Typing :: Typed",
25
+ ]
26
+ dependencies = []
27
+
28
+ [project.optional-dependencies]
29
+ dev = ["pytest>=7"]
30
+
31
+ [project.urls]
32
+ Homepage = "https://tailcove.dev"
33
+ Documentation = "https://tailcove.dev/docs/sending-logs/python"
34
+ Source = "https://github.com/tailcove/python"
35
+ Issues = "https://github.com/tailcove/python/issues"
36
+
37
+ [tool.hatch.build.targets.wheel]
38
+ packages = ["src/tailcove"]
39
+
40
+ [tool.pytest.ini_options]
41
+ testpaths = ["tests"]
@@ -0,0 +1,5 @@
1
+ from .handler import TailcoveHandler
2
+ from .transport import DeliveryError
3
+
4
+ __all__ = ["TailcoveHandler", "DeliveryError"]
5
+ __version__ = "0.1.0"
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from typing import Any, Dict, FrozenSet
5
+
6
+ # The stdlib `logging` module doesn't nest `extra={...}` under a key — each
7
+ # entry lands as a top-level attribute directly on the LogRecord. To recover
8
+ # "the extra dict" we diff a record's __dict__ against the attributes a
9
+ # vanilla LogRecord always carries; whatever's left over is caller-supplied
10
+ # context. Computed once from a throwaway record rather than hardcoded, so
11
+ # it stays correct if a future Python version adds/removes a built-in field.
12
+ _DUMMY_RECORD = logging.LogRecord(
13
+ name="tailcove",
14
+ level=logging.INFO,
15
+ pathname=__file__,
16
+ lineno=0,
17
+ msg="",
18
+ args=(),
19
+ exc_info=None,
20
+ )
21
+ RESERVED_ATTRS: FrozenSet[str] = frozenset(vars(_DUMMY_RECORD).keys()) | {"message", "asctime"}
22
+
23
+
24
+ def extra_context(record: logging.LogRecord) -> Dict[str, Any]:
25
+ return {key: value for key, value in vars(record).items() if key not in RESERVED_ATTRS}
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ import traceback
4
+ from types import TracebackType
5
+ from typing import Any, Dict, List, Optional, Tuple, Type
6
+
7
+ ExcInfo = Tuple[Optional[Type[BaseException]], Optional[BaseException], Optional[TracebackType]]
8
+
9
+
10
+ def format_exception(exc_info: ExcInfo, trace_limit: int = 10) -> Dict[str, Any]:
11
+ """
12
+ Renders a caught exception as structured data instead of a flat string,
13
+ matching the tailcove/laravel package's `{class, message, location,
14
+ trace}` shape so fingerprinting (Milestone 11) works identically
15
+ regardless of which client sent the line. `location` is the file:line
16
+ of the frame where the exception was actually raised (the last frame in
17
+ the traceback); `trace` is trimmed to `trace_limit` frames the same way
18
+ the Laravel package trims its stringified trace.
19
+ """
20
+ exc_type, exc_value, exc_tb = exc_info
21
+
22
+ frames = traceback.extract_tb(exc_tb)
23
+ location = f"{frames[-1].filename}:{frames[-1].lineno}" if frames else ""
24
+
25
+ trace: List[str] = [
26
+ f"{frame.filename}:{frame.lineno} in {frame.name}" for frame in frames
27
+ ][:trace_limit]
28
+
29
+ class_name = "UnknownException"
30
+ if exc_type is not None:
31
+ class_name = exc_type.__qualname__
32
+ if exc_type.__module__ not in ("builtins", "__main__"):
33
+ class_name = f"{exc_type.__module__}.{class_name}"
34
+
35
+ return {
36
+ "class": class_name,
37
+ "message": str(exc_value) if exc_value is not None else "",
38
+ "location": location,
39
+ "trace": trace,
40
+ }