sentry-logger 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.
- sentry_logger-0.1.0/PKG-INFO +124 -0
- sentry_logger-0.1.0/README.md +100 -0
- sentry_logger-0.1.0/pyproject.toml +38 -0
- sentry_logger-0.1.0/sentry_logger/__init__.py +70 -0
- sentry_logger-0.1.0/sentry_logger/cli.py +150 -0
- sentry_logger-0.1.0/sentry_logger/config.py +25 -0
- sentry_logger-0.1.0/sentry_logger/handler.py +73 -0
- sentry_logger-0.1.0/sentry_logger/local_config.py +51 -0
- sentry_logger-0.1.0/sentry_logger.egg-info/PKG-INFO +124 -0
- sentry_logger-0.1.0/sentry_logger.egg-info/SOURCES.txt +12 -0
- sentry_logger-0.1.0/sentry_logger.egg-info/dependency_links.txt +1 -0
- sentry_logger-0.1.0/sentry_logger.egg-info/entry_points.txt +2 -0
- sentry_logger-0.1.0/sentry_logger.egg-info/top_level.txt +1 -0
- sentry_logger-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sentry-logger
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Push logs from your Python app to the Sentry dashboard — AI-powered service health monitoring
|
|
5
|
+
Author-email: Sentry <support@sentrylabs.live>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://sentrylabs.live
|
|
8
|
+
Project-URL: Repository, https://github.com/moiz-frost/sentry
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/moiz-frost/sentry/issues
|
|
10
|
+
Keywords: logging,monitoring,observability,sentry,dashboard,devtools
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
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: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
20
|
+
Classifier: Topic :: System :: Logging
|
|
21
|
+
Classifier: Topic :: System :: Monitoring
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# Sentry Logger SDK (Python)
|
|
26
|
+
|
|
27
|
+
Push logs from your Python app to the Sentry dashboard.
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install sentry-logger
|
|
33
|
+
# or from the repo:
|
|
34
|
+
pip install ./sdk/python
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Quick Start — CLI + Browser OAuth
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
sentry-logger init --app-name "my-service" --dsn "http://localhost:9000"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
What happens:
|
|
44
|
+
|
|
45
|
+
1. CLI requests a device session from the backend
|
|
46
|
+
2. Browser opens your sign-in page
|
|
47
|
+
3. Sign in with Google — your app is registered and an API key is created
|
|
48
|
+
4. CLI polls and stores credentials in `~/.sentry_logger/config.json`
|
|
49
|
+
|
|
50
|
+
Check the currently linked app:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
sentry-logger status
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## SDK Usage
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from sentry_logger import init
|
|
60
|
+
|
|
61
|
+
# Reads api_key and dsn from ~/.sentry_logger/config.json
|
|
62
|
+
init()
|
|
63
|
+
|
|
64
|
+
import logging
|
|
65
|
+
logging.info("Hello from my app")
|
|
66
|
+
logging.error("Something went wrong")
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Explicit configuration:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from sentry_logger import init
|
|
73
|
+
|
|
74
|
+
init(
|
|
75
|
+
api_key="sk_...",
|
|
76
|
+
dsn="http://localhost:9000", # your backend URL
|
|
77
|
+
)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Environment Variables
|
|
81
|
+
|
|
82
|
+
| Variable | Default | Description |
|
|
83
|
+
|--------------------|--------------------------|--------------------------------|
|
|
84
|
+
| `LOGSENTRY_URL` | `http://localhost:9000` | Backend ingest URL base |
|
|
85
|
+
|
|
86
|
+
## FastAPI Example
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
import logging
|
|
90
|
+
from fastapi import FastAPI
|
|
91
|
+
from sentry_logger import init
|
|
92
|
+
|
|
93
|
+
app = FastAPI()
|
|
94
|
+
|
|
95
|
+
@app.on_event("startup")
|
|
96
|
+
def setup_logging() -> None:
|
|
97
|
+
init()
|
|
98
|
+
logging.getLogger(__name__).info("Sentry SDK initialized")
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Configuration Reference
|
|
102
|
+
|
|
103
|
+
| Parameter | Default | Description |
|
|
104
|
+
|--------------------------|---------|-------------------------------------------------------|
|
|
105
|
+
| `api_key` | — | API key (`sk_...`). If omitted, loaded from CLI config |
|
|
106
|
+
| `dsn` | — | Ingest URL. Falls back to `LOGSENTRY_URL` env or `http://localhost:8001` |
|
|
107
|
+
| `batch_size` | `50` | Send logs when buffer reaches this size |
|
|
108
|
+
| `flush_interval_seconds` | `5.0` | Auto-flush buffer after this many seconds |
|
|
109
|
+
|
|
110
|
+
## Log Format
|
|
111
|
+
|
|
112
|
+
The SDK formats logs in the standard Python logging format. The backend parses:
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
2024-01-01 10:00:00,123 [INFO] [ServiceName]: Log message here
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The `ServiceName` in brackets is used to group logs by service in the dashboard.
|
|
119
|
+
Use Python logger names to map to service names:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
logger = logging.getLogger("PaymentService")
|
|
123
|
+
logger.info("Payment processed")
|
|
124
|
+
```
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Sentry Logger SDK (Python)
|
|
2
|
+
|
|
3
|
+
Push logs from your Python app to the Sentry dashboard.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install sentry-logger
|
|
9
|
+
# or from the repo:
|
|
10
|
+
pip install ./sdk/python
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start — CLI + Browser OAuth
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
sentry-logger init --app-name "my-service" --dsn "http://localhost:9000"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
What happens:
|
|
20
|
+
|
|
21
|
+
1. CLI requests a device session from the backend
|
|
22
|
+
2. Browser opens your sign-in page
|
|
23
|
+
3. Sign in with Google — your app is registered and an API key is created
|
|
24
|
+
4. CLI polls and stores credentials in `~/.sentry_logger/config.json`
|
|
25
|
+
|
|
26
|
+
Check the currently linked app:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
sentry-logger status
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## SDK Usage
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from sentry_logger import init
|
|
36
|
+
|
|
37
|
+
# Reads api_key and dsn from ~/.sentry_logger/config.json
|
|
38
|
+
init()
|
|
39
|
+
|
|
40
|
+
import logging
|
|
41
|
+
logging.info("Hello from my app")
|
|
42
|
+
logging.error("Something went wrong")
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Explicit configuration:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from sentry_logger import init
|
|
49
|
+
|
|
50
|
+
init(
|
|
51
|
+
api_key="sk_...",
|
|
52
|
+
dsn="http://localhost:9000", # your backend URL
|
|
53
|
+
)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Environment Variables
|
|
57
|
+
|
|
58
|
+
| Variable | Default | Description |
|
|
59
|
+
|--------------------|--------------------------|--------------------------------|
|
|
60
|
+
| `LOGSENTRY_URL` | `http://localhost:9000` | Backend ingest URL base |
|
|
61
|
+
|
|
62
|
+
## FastAPI Example
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
import logging
|
|
66
|
+
from fastapi import FastAPI
|
|
67
|
+
from sentry_logger import init
|
|
68
|
+
|
|
69
|
+
app = FastAPI()
|
|
70
|
+
|
|
71
|
+
@app.on_event("startup")
|
|
72
|
+
def setup_logging() -> None:
|
|
73
|
+
init()
|
|
74
|
+
logging.getLogger(__name__).info("Sentry SDK initialized")
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Configuration Reference
|
|
78
|
+
|
|
79
|
+
| Parameter | Default | Description |
|
|
80
|
+
|--------------------------|---------|-------------------------------------------------------|
|
|
81
|
+
| `api_key` | — | API key (`sk_...`). If omitted, loaded from CLI config |
|
|
82
|
+
| `dsn` | — | Ingest URL. Falls back to `LOGSENTRY_URL` env or `http://localhost:8001` |
|
|
83
|
+
| `batch_size` | `50` | Send logs when buffer reaches this size |
|
|
84
|
+
| `flush_interval_seconds` | `5.0` | Auto-flush buffer after this many seconds |
|
|
85
|
+
|
|
86
|
+
## Log Format
|
|
87
|
+
|
|
88
|
+
The SDK formats logs in the standard Python logging format. The backend parses:
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
2024-01-01 10:00:00,123 [INFO] [ServiceName]: Log message here
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The `ServiceName` in brackets is used to group logs by service in the dashboard.
|
|
95
|
+
Use Python logger names to map to service names:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
logger = logging.getLogger("PaymentService")
|
|
99
|
+
logger.info("Payment processed")
|
|
100
|
+
```
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sentry-logger"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Push logs from your Python app to the Sentry dashboard — AI-powered service health monitoring"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "Sentry", email = "support@sentrylabs.live" }]
|
|
13
|
+
keywords = ["logging", "monitoring", "observability", "sentry", "dashboard", "devtools"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.9",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Topic :: Software Development :: Libraries",
|
|
24
|
+
"Topic :: System :: Logging",
|
|
25
|
+
"Topic :: System :: Monitoring",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://sentrylabs.live"
|
|
30
|
+
Repository = "https://github.com/moiz-frost/sentry"
|
|
31
|
+
"Bug Tracker" = "https://github.com/moiz-frost/sentry/issues"
|
|
32
|
+
|
|
33
|
+
[project.scripts]
|
|
34
|
+
sentry-logger = "sentry_logger.cli:main"
|
|
35
|
+
|
|
36
|
+
[tool.setuptools.packages.find]
|
|
37
|
+
where = ["."]
|
|
38
|
+
include = ["sentry_logger*"]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sentry Logger SDK - Push logs from your Python app to the Sentry dashboard.
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from .handler import SentryLogHandler
|
|
9
|
+
from .config import SentryLoggerConfig
|
|
10
|
+
from .local_config import load_local_config, resolve_dsn
|
|
11
|
+
|
|
12
|
+
__all__ = ["SentryLogHandler", "SentryLoggerConfig", "init"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def init(
|
|
16
|
+
api_key: Optional[str] = None,
|
|
17
|
+
dsn: Optional[str] = None,
|
|
18
|
+
batch_size: int = 50,
|
|
19
|
+
flush_interval_seconds: float = 5.0,
|
|
20
|
+
) -> SentryLogHandler:
|
|
21
|
+
"""
|
|
22
|
+
Initialize Sentry Logger and attach it to the root Python logger.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
api_key: Your Sentry API key. If omitted, reads from the local config
|
|
26
|
+
file written by ``sentry-logger init`` (~/.sentry_logger/config.json).
|
|
27
|
+
dsn: Backend base URL. If omitted, uses the production endpoint
|
|
28
|
+
(https://api.sentrylabs.live) or the value saved by the CLI.
|
|
29
|
+
Can also be overridden with the SENTRY_INGEST_URL env var.
|
|
30
|
+
batch_size: Logs to buffer before flushing (default 50).
|
|
31
|
+
flush_interval_seconds: Seconds between automatic flushes (default 5).
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
SentryLogHandler — attach to specific loggers if needed.
|
|
35
|
+
|
|
36
|
+
Example (CLI-linked app, zero config):
|
|
37
|
+
>>> from sentry_logger import init
|
|
38
|
+
>>> init()
|
|
39
|
+
|
|
40
|
+
Example (explicit key):
|
|
41
|
+
>>> from sentry_logger import init
|
|
42
|
+
>>> init(api_key="sk_...")
|
|
43
|
+
"""
|
|
44
|
+
import logging
|
|
45
|
+
|
|
46
|
+
resolved_dsn = resolve_dsn(dsn)
|
|
47
|
+
|
|
48
|
+
if not api_key:
|
|
49
|
+
cfg = load_local_config()
|
|
50
|
+
api_key = cfg.get("api_key")
|
|
51
|
+
# Also prefer the DSN saved by the CLI if not provided explicitly
|
|
52
|
+
if not dsn and cfg.get("dsn"):
|
|
53
|
+
resolved_dsn = str(cfg["dsn"]).rstrip("/")
|
|
54
|
+
|
|
55
|
+
if not api_key:
|
|
56
|
+
raise ValueError(
|
|
57
|
+
"api_key is required. Either run `sentry-logger init` first, "
|
|
58
|
+
"or pass api_key= to init()."
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
config = SentryLoggerConfig(
|
|
62
|
+
api_key=api_key,
|
|
63
|
+
batch_size=batch_size,
|
|
64
|
+
flush_interval_seconds=flush_interval_seconds,
|
|
65
|
+
_backend_url=resolved_dsn,
|
|
66
|
+
)
|
|
67
|
+
handler = SentryLogHandler(config)
|
|
68
|
+
logging.getLogger().addHandler(handler)
|
|
69
|
+
logging.getLogger().setLevel(logging.INFO)
|
|
70
|
+
return handler
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""CLI for one-command SDK onboarding."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
from typing import Optional
|
|
9
|
+
import urllib.error
|
|
10
|
+
import urllib.parse
|
|
11
|
+
import urllib.request
|
|
12
|
+
import webbrowser
|
|
13
|
+
|
|
14
|
+
from .local_config import DEFAULT_DSN, get_default_config_path, load_local_config, save_local_config
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _http_json(method: str, url: str, payload: Optional[dict] = None) -> dict:
|
|
18
|
+
data = json.dumps(payload).encode("utf-8") if payload is not None else None
|
|
19
|
+
request = urllib.request.Request(
|
|
20
|
+
url,
|
|
21
|
+
method=method,
|
|
22
|
+
data=data,
|
|
23
|
+
headers={"Content-Type": "application/json"},
|
|
24
|
+
)
|
|
25
|
+
with urllib.request.urlopen(request, timeout=15) as response:
|
|
26
|
+
body = response.read().decode("utf-8")
|
|
27
|
+
return json.loads(body) if body else {}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def cmd_init(args: argparse.Namespace) -> int:
|
|
31
|
+
dsn = args.dsn.rstrip("/")
|
|
32
|
+
try:
|
|
33
|
+
start = _http_json(
|
|
34
|
+
"POST",
|
|
35
|
+
f"{dsn}/sdk/device/start",
|
|
36
|
+
{
|
|
37
|
+
"app_name": args.app_name,
|
|
38
|
+
"description": args.description,
|
|
39
|
+
"ttl_seconds": args.ttl_seconds,
|
|
40
|
+
},
|
|
41
|
+
)
|
|
42
|
+
except urllib.error.HTTPError as exc:
|
|
43
|
+
print(f"Failed to start login flow: HTTP {exc.code}", file=sys.stderr)
|
|
44
|
+
return 1
|
|
45
|
+
except Exception as exc:
|
|
46
|
+
print(f"Failed to start login flow: {type(exc).__name__}", file=sys.stderr)
|
|
47
|
+
return 1
|
|
48
|
+
|
|
49
|
+
verification_uri_complete = start["verification_uri_complete"]
|
|
50
|
+
user_code = start["user_code"]
|
|
51
|
+
device_code = start["device_code"]
|
|
52
|
+
interval = int(start.get("interval", 3))
|
|
53
|
+
|
|
54
|
+
print("Open this URL to login and link your app:")
|
|
55
|
+
print(verification_uri_complete)
|
|
56
|
+
print(f"If prompted, enter code: {user_code}")
|
|
57
|
+
|
|
58
|
+
if not args.no_browser:
|
|
59
|
+
webbrowser.open(verification_uri_complete)
|
|
60
|
+
|
|
61
|
+
started = time.monotonic()
|
|
62
|
+
while True:
|
|
63
|
+
if (time.monotonic() - started) > args.timeout_seconds:
|
|
64
|
+
print("Timed out waiting for approval.", file=sys.stderr)
|
|
65
|
+
return 2
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
poll_url = f"{dsn}/sdk/device/poll?{urllib.parse.urlencode({'device_code': device_code})}"
|
|
69
|
+
poll = _http_json("GET", poll_url)
|
|
70
|
+
except urllib.error.HTTPError as exc:
|
|
71
|
+
if exc.code == 400:
|
|
72
|
+
print("Device code expired. Run init again.", file=sys.stderr)
|
|
73
|
+
return 2
|
|
74
|
+
time.sleep(interval)
|
|
75
|
+
continue
|
|
76
|
+
except Exception:
|
|
77
|
+
time.sleep(interval)
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
if poll.get("status") == "approved":
|
|
81
|
+
payload = {
|
|
82
|
+
"app_id": poll["app_id"],
|
|
83
|
+
"app_name": poll.get("app_name", args.app_name),
|
|
84
|
+
"api_key": poll["api_key"],
|
|
85
|
+
"dsn": poll.get("dsn", dsn),
|
|
86
|
+
}
|
|
87
|
+
saved = save_local_config(payload, path=args.config_path)
|
|
88
|
+
print("")
|
|
89
|
+
print(f"Linked app '{payload['app_name']}' ({payload['app_id']}).")
|
|
90
|
+
print(f"Credentials saved to {saved}.")
|
|
91
|
+
print("Use in code:")
|
|
92
|
+
print(" from sentry_logger import init")
|
|
93
|
+
print(" init() # reads local config")
|
|
94
|
+
return 0
|
|
95
|
+
|
|
96
|
+
time.sleep(interval)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def cmd_status(args: argparse.Namespace) -> int:
|
|
100
|
+
cfg = load_local_config(path=args.config_path)
|
|
101
|
+
if not cfg:
|
|
102
|
+
print(f"No config found at {args.config_path or get_default_config_path()}")
|
|
103
|
+
return 1
|
|
104
|
+
print(f"App: {cfg.get('app_name')}")
|
|
105
|
+
print(f"App ID: {cfg.get('app_id')}")
|
|
106
|
+
print(f"DSN: {cfg.get('dsn')}")
|
|
107
|
+
print(f"API key present: {'yes' if cfg.get('api_key') else 'no'}")
|
|
108
|
+
return 0
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
112
|
+
parser = argparse.ArgumentParser(prog="sentry-logger", description="Sentry Logger SDK CLI")
|
|
113
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
114
|
+
|
|
115
|
+
_dsn_help = f"Backend base URL (default: {DEFAULT_DSN})"
|
|
116
|
+
|
|
117
|
+
init_parser = sub.add_parser("init", help="Login in browser and provision app credentials")
|
|
118
|
+
init_parser.add_argument("--app-name", required=True, help="Name for the app to register")
|
|
119
|
+
init_parser.add_argument("--description", default="", help="Optional app description")
|
|
120
|
+
init_parser.add_argument("--dsn", default=DEFAULT_DSN, help=_dsn_help)
|
|
121
|
+
init_parser.add_argument("--ttl-seconds", type=int, default=600, help="Login session TTL")
|
|
122
|
+
init_parser.add_argument("--timeout-seconds", type=int, default=300, help="Polling timeout")
|
|
123
|
+
init_parser.add_argument("--no-browser", action="store_true", help="Do not auto-open browser")
|
|
124
|
+
init_parser.add_argument("--config-path", default=None, help="Override config file path")
|
|
125
|
+
init_parser.set_defaults(func=cmd_init)
|
|
126
|
+
|
|
127
|
+
login_parser = sub.add_parser("login", help="Alias for init")
|
|
128
|
+
login_parser.add_argument("--app-name", required=True, help="Name for the app to register")
|
|
129
|
+
login_parser.add_argument("--description", default="", help="Optional app description")
|
|
130
|
+
login_parser.add_argument("--dsn", default=DEFAULT_DSN, help=_dsn_help)
|
|
131
|
+
login_parser.add_argument("--ttl-seconds", type=int, default=600, help="Login session TTL")
|
|
132
|
+
login_parser.add_argument("--timeout-seconds", type=int, default=300, help="Polling timeout")
|
|
133
|
+
login_parser.add_argument("--no-browser", action="store_true", help="Do not auto-open browser")
|
|
134
|
+
login_parser.add_argument("--config-path", default=None, help="Override config file path")
|
|
135
|
+
login_parser.set_defaults(func=cmd_init)
|
|
136
|
+
|
|
137
|
+
status_parser = sub.add_parser("status", help="Show current local SDK config")
|
|
138
|
+
status_parser.add_argument("--config-path", default=None, help="Override config file path")
|
|
139
|
+
status_parser.set_defaults(func=cmd_status)
|
|
140
|
+
return parser
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def main() -> int:
|
|
144
|
+
parser = build_parser()
|
|
145
|
+
args = parser.parse_args()
|
|
146
|
+
return int(args.func(args))
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
if __name__ == "__main__":
|
|
150
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Configuration for Sentry Logger SDK."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class SentryLoggerConfig:
|
|
11
|
+
api_key: str
|
|
12
|
+
batch_size: int = 50
|
|
13
|
+
flush_interval_seconds: float = 5.0
|
|
14
|
+
|
|
15
|
+
# Internal - URL is set via environment variable or resolved from local config
|
|
16
|
+
_backend_url: Optional[str] = field(default=None, repr=False)
|
|
17
|
+
|
|
18
|
+
def __post_init__(self):
|
|
19
|
+
if not self._backend_url:
|
|
20
|
+
self._backend_url = os.environ.get(
|
|
21
|
+
"SENTRY_INGEST_URL",
|
|
22
|
+
"https://api.sentrylabs.live",
|
|
23
|
+
)
|
|
24
|
+
self._backend_url = self._backend_url.rstrip("/")
|
|
25
|
+
self.ingest_url = f"{self._backend_url}/ingest"
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Logging handler that buffers log records and sends them in batches to the Sentry ingest API.
|
|
3
|
+
"""
|
|
4
|
+
import atexit
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
import urllib.request
|
|
10
|
+
|
|
11
|
+
from .config import SentryLoggerConfig
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SentryLogHandler(logging.Handler):
|
|
15
|
+
"""
|
|
16
|
+
Buffers log records and POSTs them in batches to the Sentry ingest endpoint.
|
|
17
|
+
Uses a background thread for non-blocking sends.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, config: SentryLoggerConfig):
|
|
21
|
+
super().__init__()
|
|
22
|
+
self.config = config
|
|
23
|
+
self._buffer: list[str] = []
|
|
24
|
+
self._lock = threading.Lock()
|
|
25
|
+
self._shutdown = False
|
|
26
|
+
self._last_flush = time.monotonic()
|
|
27
|
+
self.setFormatter(
|
|
28
|
+
logging.Formatter("%(asctime)s [%(levelname)s] [%(name)s]: %(message)s")
|
|
29
|
+
)
|
|
30
|
+
atexit.register(self.flush)
|
|
31
|
+
|
|
32
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
33
|
+
try:
|
|
34
|
+
msg = self.format(record)
|
|
35
|
+
with self._lock:
|
|
36
|
+
self._buffer.append(msg)
|
|
37
|
+
now = time.monotonic()
|
|
38
|
+
if (
|
|
39
|
+
len(self._buffer) >= self.config.batch_size
|
|
40
|
+
or (now - self._last_flush) >= self.config.flush_interval_seconds
|
|
41
|
+
):
|
|
42
|
+
self._flush_locked()
|
|
43
|
+
except Exception:
|
|
44
|
+
self.handleError(record)
|
|
45
|
+
|
|
46
|
+
def _flush_locked(self) -> None:
|
|
47
|
+
if not self._buffer:
|
|
48
|
+
return
|
|
49
|
+
logs = self._buffer[:]
|
|
50
|
+
self._buffer = []
|
|
51
|
+
self._last_flush = time.monotonic()
|
|
52
|
+
threading.Thread(target=self._send, args=(logs,), daemon=True).start()
|
|
53
|
+
|
|
54
|
+
def _send(self, logs: list[str]) -> None:
|
|
55
|
+
try:
|
|
56
|
+
body = json.dumps({"logs": logs}).encode("utf-8")
|
|
57
|
+
req = urllib.request.Request(
|
|
58
|
+
self.config.ingest_url,
|
|
59
|
+
data=body,
|
|
60
|
+
method="POST",
|
|
61
|
+
headers={
|
|
62
|
+
"Content-Type": "application/json",
|
|
63
|
+
"Authorization": f"Bearer {self.config.api_key}",
|
|
64
|
+
"X-API-Key": self.config.api_key,
|
|
65
|
+
},
|
|
66
|
+
)
|
|
67
|
+
urllib.request.urlopen(req, timeout=10)
|
|
68
|
+
except Exception:
|
|
69
|
+
pass # Fail silently to avoid disrupting the app
|
|
70
|
+
|
|
71
|
+
def flush(self) -> None:
|
|
72
|
+
with self._lock:
|
|
73
|
+
self._flush_locked()
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Local CLI config persistence for SDK credentials."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_default_config_path() -> Path:
|
|
11
|
+
home = Path.home()
|
|
12
|
+
return home / ".sentry_logger" / "config.json"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def ensure_parent(path: Path) -> None:
|
|
16
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def load_local_config(path: Optional[str] = None) -> dict[str, Any]:
|
|
20
|
+
config_path = Path(path) if path else get_default_config_path()
|
|
21
|
+
if not config_path.exists():
|
|
22
|
+
return {}
|
|
23
|
+
try:
|
|
24
|
+
with config_path.open("r", encoding="utf-8") as f:
|
|
25
|
+
payload = json.load(f)
|
|
26
|
+
return payload if isinstance(payload, dict) else {}
|
|
27
|
+
except Exception:
|
|
28
|
+
return {}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def save_local_config(payload: dict[str, Any], path: Optional[str] = None) -> Path:
|
|
32
|
+
config_path = Path(path) if path else get_default_config_path()
|
|
33
|
+
ensure_parent(config_path)
|
|
34
|
+
with config_path.open("w", encoding="utf-8") as f:
|
|
35
|
+
json.dump(payload, f, indent=2)
|
|
36
|
+
return config_path
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
DEFAULT_DSN = "https://api.sentrylabs.live"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def resolve_dsn(explicit_dsn: Optional[str] = None) -> str:
|
|
43
|
+
if explicit_dsn:
|
|
44
|
+
return explicit_dsn.rstrip("/")
|
|
45
|
+
env_dsn = os.environ.get("SENTRY_INGEST_URL")
|
|
46
|
+
if env_dsn:
|
|
47
|
+
return env_dsn.rstrip("/")
|
|
48
|
+
cfg = load_local_config()
|
|
49
|
+
if cfg.get("dsn"):
|
|
50
|
+
return str(cfg["dsn"]).rstrip("/")
|
|
51
|
+
return DEFAULT_DSN
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sentry-logger
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Push logs from your Python app to the Sentry dashboard — AI-powered service health monitoring
|
|
5
|
+
Author-email: Sentry <support@sentrylabs.live>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://sentrylabs.live
|
|
8
|
+
Project-URL: Repository, https://github.com/moiz-frost/sentry
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/moiz-frost/sentry/issues
|
|
10
|
+
Keywords: logging,monitoring,observability,sentry,dashboard,devtools
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
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: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
20
|
+
Classifier: Topic :: System :: Logging
|
|
21
|
+
Classifier: Topic :: System :: Monitoring
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# Sentry Logger SDK (Python)
|
|
26
|
+
|
|
27
|
+
Push logs from your Python app to the Sentry dashboard.
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install sentry-logger
|
|
33
|
+
# or from the repo:
|
|
34
|
+
pip install ./sdk/python
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Quick Start — CLI + Browser OAuth
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
sentry-logger init --app-name "my-service" --dsn "http://localhost:9000"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
What happens:
|
|
44
|
+
|
|
45
|
+
1. CLI requests a device session from the backend
|
|
46
|
+
2. Browser opens your sign-in page
|
|
47
|
+
3. Sign in with Google — your app is registered and an API key is created
|
|
48
|
+
4. CLI polls and stores credentials in `~/.sentry_logger/config.json`
|
|
49
|
+
|
|
50
|
+
Check the currently linked app:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
sentry-logger status
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## SDK Usage
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from sentry_logger import init
|
|
60
|
+
|
|
61
|
+
# Reads api_key and dsn from ~/.sentry_logger/config.json
|
|
62
|
+
init()
|
|
63
|
+
|
|
64
|
+
import logging
|
|
65
|
+
logging.info("Hello from my app")
|
|
66
|
+
logging.error("Something went wrong")
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Explicit configuration:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from sentry_logger import init
|
|
73
|
+
|
|
74
|
+
init(
|
|
75
|
+
api_key="sk_...",
|
|
76
|
+
dsn="http://localhost:9000", # your backend URL
|
|
77
|
+
)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Environment Variables
|
|
81
|
+
|
|
82
|
+
| Variable | Default | Description |
|
|
83
|
+
|--------------------|--------------------------|--------------------------------|
|
|
84
|
+
| `LOGSENTRY_URL` | `http://localhost:9000` | Backend ingest URL base |
|
|
85
|
+
|
|
86
|
+
## FastAPI Example
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
import logging
|
|
90
|
+
from fastapi import FastAPI
|
|
91
|
+
from sentry_logger import init
|
|
92
|
+
|
|
93
|
+
app = FastAPI()
|
|
94
|
+
|
|
95
|
+
@app.on_event("startup")
|
|
96
|
+
def setup_logging() -> None:
|
|
97
|
+
init()
|
|
98
|
+
logging.getLogger(__name__).info("Sentry SDK initialized")
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Configuration Reference
|
|
102
|
+
|
|
103
|
+
| Parameter | Default | Description |
|
|
104
|
+
|--------------------------|---------|-------------------------------------------------------|
|
|
105
|
+
| `api_key` | — | API key (`sk_...`). If omitted, loaded from CLI config |
|
|
106
|
+
| `dsn` | — | Ingest URL. Falls back to `LOGSENTRY_URL` env or `http://localhost:8001` |
|
|
107
|
+
| `batch_size` | `50` | Send logs when buffer reaches this size |
|
|
108
|
+
| `flush_interval_seconds` | `5.0` | Auto-flush buffer after this many seconds |
|
|
109
|
+
|
|
110
|
+
## Log Format
|
|
111
|
+
|
|
112
|
+
The SDK formats logs in the standard Python logging format. The backend parses:
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
2024-01-01 10:00:00,123 [INFO] [ServiceName]: Log message here
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The `ServiceName` in brackets is used to group logs by service in the dashboard.
|
|
119
|
+
Use Python logger names to map to service names:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
logger = logging.getLogger("PaymentService")
|
|
123
|
+
logger.info("Payment processed")
|
|
124
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
sentry_logger/__init__.py
|
|
4
|
+
sentry_logger/cli.py
|
|
5
|
+
sentry_logger/config.py
|
|
6
|
+
sentry_logger/handler.py
|
|
7
|
+
sentry_logger/local_config.py
|
|
8
|
+
sentry_logger.egg-info/PKG-INFO
|
|
9
|
+
sentry_logger.egg-info/SOURCES.txt
|
|
10
|
+
sentry_logger.egg-info/dependency_links.txt
|
|
11
|
+
sentry_logger.egg-info/entry_points.txt
|
|
12
|
+
sentry_logger.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sentry_logger
|