statoss 0.1.0__tar.gz → 0.2.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.
@@ -1,8 +1,31 @@
1
+ Metadata-Version: 2.4
2
+ Name: statoss
3
+ Version: 0.2.0
4
+ Summary: Error tracking and logs for StatOSS: sentry-sdk with your app's own levels, categories and fields.
5
+ Keywords: statoss,errors,error-tracking,logs,sentry,monitoring
6
+ Author: StatOSS
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Topic :: System :: Monitoring
15
+ Classifier: Typing :: Typed
16
+ Requires-Dist: sentry-sdk>=2.35.0,<3
17
+ Requires-Dist: typing-extensions>=4.12
18
+ Requires-Python: >=3.9
19
+ Project-URL: Homepage, https://statoss.com
20
+ Project-URL: Source, https://github.com/kroqdotdev/statoss/tree/main/sdk-python
21
+ Description-Content-Type: text/markdown
22
+
1
23
  # statoss
2
24
 
3
- Error tracking for [StatOSS](https://statoss.com) in Python: a thin layer
4
- over [`sentry-sdk`](https://pypi.org/project/sentry-sdk/) that adds your
5
- app's own levels, categories and fields. Python 3.9 or later, sentry-sdk 2.
25
+ Error tracking and logs for [StatOSS](https://statoss.com) in Python: a
26
+ thin layer over [`sentry-sdk`](https://pypi.org/project/sentry-sdk/) that
27
+ adds your app's own levels, categories and fields. Python 3.9 or later,
28
+ sentry-sdk 2.35 or later.
6
29
 
7
30
  ## Install
8
31
 
@@ -84,6 +107,39 @@ statoss.set_category("billing") # every later event in this scope
84
107
  `sentry_sdk.capture_message` may leave it, is sent the same way, after your
85
108
  `before_send`.
86
109
 
110
+ ## Logs
111
+
112
+ With `enable_logs=True`, `statoss.logger` sends log lines through
113
+ sentry-sdk's logger, which StatOSS's Logs area searches. Without it they
114
+ are dropped.
115
+
116
+ ```python
117
+ statoss.init(dsn=os.environ["STATOSS_DSN"], schema=schema, enable_logs=True)
118
+
119
+ statoss.logger.info("Order {order_id} paid", order_id=7, attributes={"amount": 12.5})
120
+ statoss.logger.log("payment-failed", "Card declined", attributes={"order_id": 7})
121
+ # level "error", attributes statoss.level=payment-failed, order_id=7
122
+ ```
123
+
124
+ - `trace`, `debug`, `info`, `warning`, `error` and `fatal` take a template
125
+ and its values as sentry-sdk's do: `"Paid {amount}"` with `amount=12`
126
+ sends "Paid 12", the template and each value as attributes.
127
+ `attributes=` adds more.
128
+ - `log(level, template, ...)` takes a level by name: one of the six (`warn`
129
+ too), a Sentry level, or one of the schema's. A schema level is sent at
130
+ its range's level (`trace` stays `trace`), with its severity as
131
+ `sentry.severity_number` when the schema gives one, and its name as the
132
+ attribute `statoss.level`. A name the schema does not hold is sent as
133
+ `error` and, with `debug=True`, logged.
134
+ - `client.logger` from `init` takes only the schema's levels when they are
135
+ typed (see [Typing](#typing)).
136
+ - Lines are sent in batches by sentry-sdk; `statoss.flush()` sends what
137
+ waits. sentry-sdk's `before_send_log` sees each line, `statoss.level`
138
+ included.
139
+ - Records of Python's `logging` module at INFO and up go as log lines too
140
+ with `enable_logs=True`, except on sentry-sdk 2.68.0, where they need
141
+ `LoggingIntegration(capture_sentry_logs=True)`.
142
+
87
143
  ## The schema
88
144
 
89
145
  `define_schema` checks everything as StatOSS does and raises `SchemaError`
@@ -139,11 +195,14 @@ client = statoss.init(dsn=os.environ["STATOSS_DSN"], schema=schema)
139
195
 
140
196
  client.capture_exception(error, level="payment-failed", category="billing")
141
197
  client.capture_exception(error, level="payment-faild") # error: not a LevelName
198
+ client.logger.log("slow-query", "Search took {ms} ms", ms=2400)
199
+ client.logger.log("slow-qeury", "Search took {ms} ms", ms=2400) # error: not a LevelName
142
200
  ```
143
201
 
144
202
  `init` returns a `statoss.Client[LevelName, CategoryName]` whose
145
- `capture_exception`, `capture_message` and `set_category` take only those
146
- names and Sentry's levels. The module functions take any name.
203
+ `capture_exception`, `capture_message`, `set_category` and `logger.log`
204
+ take only those names and Sentry's levels. The module functions and
205
+ `statoss.logger` take any name.
147
206
 
148
207
  ## Developing
149
208
 
@@ -1,30 +1,9 @@
1
- Metadata-Version: 2.4
2
- Name: statoss
3
- Version: 0.1.0
4
- Summary: Error tracking for StatOSS: sentry-sdk with your app's own levels, categories and fields.
5
- Keywords: statoss,errors,error-tracking,sentry,monitoring
6
- Author: StatOSS
7
- License-Expression: MIT
8
- License-File: LICENSE
9
- Classifier: Development Status :: 4 - Beta
10
- Classifier: Intended Audience :: Developers
11
- Classifier: Operating System :: OS Independent
12
- Classifier: Programming Language :: Python :: 3
13
- Classifier: Programming Language :: Python :: 3 :: Only
14
- Classifier: Topic :: System :: Monitoring
15
- Classifier: Typing :: Typed
16
- Requires-Dist: sentry-sdk>=2.0.0,<3
17
- Requires-Dist: typing-extensions>=4.12
18
- Requires-Python: >=3.9
19
- Project-URL: Homepage, https://statoss.com
20
- Project-URL: Source, https://github.com/kroqdotdev/statoss/tree/main/sdk-python
21
- Description-Content-Type: text/markdown
22
-
23
1
  # statoss
24
2
 
25
- Error tracking for [StatOSS](https://statoss.com) in Python: a thin layer
26
- over [`sentry-sdk`](https://pypi.org/project/sentry-sdk/) that adds your
27
- app's own levels, categories and fields. Python 3.9 or later, sentry-sdk 2.
3
+ Error tracking and logs for [StatOSS](https://statoss.com) in Python: a
4
+ thin layer over [`sentry-sdk`](https://pypi.org/project/sentry-sdk/) that
5
+ adds your app's own levels, categories and fields. Python 3.9 or later,
6
+ sentry-sdk 2.35 or later.
28
7
 
29
8
  ## Install
30
9
 
@@ -106,6 +85,39 @@ statoss.set_category("billing") # every later event in this scope
106
85
  `sentry_sdk.capture_message` may leave it, is sent the same way, after your
107
86
  `before_send`.
108
87
 
88
+ ## Logs
89
+
90
+ With `enable_logs=True`, `statoss.logger` sends log lines through
91
+ sentry-sdk's logger, which StatOSS's Logs area searches. Without it they
92
+ are dropped.
93
+
94
+ ```python
95
+ statoss.init(dsn=os.environ["STATOSS_DSN"], schema=schema, enable_logs=True)
96
+
97
+ statoss.logger.info("Order {order_id} paid", order_id=7, attributes={"amount": 12.5})
98
+ statoss.logger.log("payment-failed", "Card declined", attributes={"order_id": 7})
99
+ # level "error", attributes statoss.level=payment-failed, order_id=7
100
+ ```
101
+
102
+ - `trace`, `debug`, `info`, `warning`, `error` and `fatal` take a template
103
+ and its values as sentry-sdk's do: `"Paid {amount}"` with `amount=12`
104
+ sends "Paid 12", the template and each value as attributes.
105
+ `attributes=` adds more.
106
+ - `log(level, template, ...)` takes a level by name: one of the six (`warn`
107
+ too), a Sentry level, or one of the schema's. A schema level is sent at
108
+ its range's level (`trace` stays `trace`), with its severity as
109
+ `sentry.severity_number` when the schema gives one, and its name as the
110
+ attribute `statoss.level`. A name the schema does not hold is sent as
111
+ `error` and, with `debug=True`, logged.
112
+ - `client.logger` from `init` takes only the schema's levels when they are
113
+ typed (see [Typing](#typing)).
114
+ - Lines are sent in batches by sentry-sdk; `statoss.flush()` sends what
115
+ waits. sentry-sdk's `before_send_log` sees each line, `statoss.level`
116
+ included.
117
+ - Records of Python's `logging` module at INFO and up go as log lines too
118
+ with `enable_logs=True`, except on sentry-sdk 2.68.0, where they need
119
+ `LoggingIntegration(capture_sentry_logs=True)`.
120
+
109
121
  ## The schema
110
122
 
111
123
  `define_schema` checks everything as StatOSS does and raises `SchemaError`
@@ -161,11 +173,14 @@ client = statoss.init(dsn=os.environ["STATOSS_DSN"], schema=schema)
161
173
 
162
174
  client.capture_exception(error, level="payment-failed", category="billing")
163
175
  client.capture_exception(error, level="payment-faild") # error: not a LevelName
176
+ client.logger.log("slow-query", "Search took {ms} ms", ms=2400)
177
+ client.logger.log("slow-qeury", "Search took {ms} ms", ms=2400) # error: not a LevelName
164
178
  ```
165
179
 
166
180
  `init` returns a `statoss.Client[LevelName, CategoryName]` whose
167
- `capture_exception`, `capture_message` and `set_category` take only those
168
- names and Sentry's levels. The module functions take any name.
181
+ `capture_exception`, `capture_message`, `set_category` and `logger.log`
182
+ take only those names and Sentry's levels. The module functions and
183
+ `statoss.logger` take any name.
169
184
 
170
185
  ## Developing
171
186
 
@@ -1,19 +1,20 @@
1
1
  [project]
2
2
  name = "statoss"
3
- version = "0.1.0"
4
- description = "Error tracking for StatOSS: sentry-sdk with your app's own levels, categories and fields."
3
+ version = "0.2.0"
4
+ description = "Error tracking and logs for StatOSS: sentry-sdk with your app's own levels, categories and fields."
5
5
  readme = "README.md"
6
6
  license = "MIT"
7
7
  license-files = ["LICENSE"]
8
8
  requires-python = ">=3.9"
9
9
  dependencies = [
10
- "sentry-sdk>=2.0.0,<3",
10
+ "sentry-sdk>=2.35.0,<3",
11
11
  "typing-extensions>=4.12",
12
12
  ]
13
13
  keywords = [
14
14
  "statoss",
15
15
  "errors",
16
16
  "error-tracking",
17
+ "logs",
17
18
  "sentry",
18
19
  "monitoring",
19
20
  ]
@@ -1,14 +1,14 @@
1
1
  [project]
2
2
  name = "statoss"
3
- version = "0.1.0"
4
- description = "Error tracking for StatOSS: sentry-sdk with your app's own levels, categories and fields."
3
+ version = "0.2.0"
4
+ description = "Error tracking and logs for StatOSS: sentry-sdk with your app's own levels, categories and fields."
5
5
  readme = "README.md"
6
6
  license = "MIT"
7
7
  license-files = ["LICENSE"]
8
8
  authors = [{ name = "StatOSS" }]
9
9
  requires-python = ">=3.9"
10
- dependencies = ["sentry-sdk>=2.0.0,<3", "typing-extensions>=4.12"]
11
- keywords = ["statoss", "errors", "error-tracking", "sentry", "monitoring"]
10
+ dependencies = ["sentry-sdk>=2.35.0,<3", "typing-extensions>=4.12"]
11
+ keywords = ["statoss", "errors", "error-tracking", "logs", "sentry", "monitoring"]
12
12
  classifiers = [
13
13
  "Development Status :: 4 - Beta",
14
14
  "Intended Audience :: Developers",
@@ -1,4 +1,4 @@
1
- """Error tracking for StatOSS: sentry-sdk with the app's own levels, categories and fields."""
1
+ """Error tracking and logs for StatOSS: sentry-sdk with the app's own levels, categories, fields."""
2
2
 
3
3
  from sentry_sdk import add_breadcrumb, flush, set_context, set_tag, set_user
4
4
 
@@ -10,6 +10,8 @@ from ._client import (
10
10
  init,
11
11
  set_category,
12
12
  )
13
+ from ._client import default_logger as logger
14
+ from ._logs import Logger, LogLevel
13
15
  from ._schema import (
14
16
  CATEGORY_TAG,
15
17
  LEVEL_TAG,
@@ -28,7 +30,7 @@ from ._schema import (
28
30
  define_schema,
29
31
  )
30
32
 
31
- __version__ = "0.1.0"
33
+ __version__ = "0.2.0"
32
34
 
33
35
  __all__ = [
34
36
  "CATEGORY_TAG",
@@ -40,6 +42,8 @@ __all__ = [
40
42
  "FieldValue",
41
43
  "Level",
42
44
  "Like",
45
+ "LogLevel",
46
+ "Logger",
43
47
  "Rule",
44
48
  "RuleMatch",
45
49
  "RuleSubject",
@@ -53,6 +57,7 @@ __all__ = [
53
57
  "define_schema",
54
58
  "flush",
55
59
  "init",
60
+ "logger",
56
61
  "set_category",
57
62
  "set_context",
58
63
  "set_tag",
@@ -22,6 +22,7 @@ from typing import (
22
22
 
23
23
  import sentry_sdk
24
24
 
25
+ from ._logs import Logger
25
26
  from ._schema import (
26
27
  BUILTIN_LEVELS,
27
28
  CATEGORY_TAG,
@@ -71,6 +72,8 @@ class Client(Generic[L, C]):
71
72
 
72
73
  def __init__(self, schema: Optional[Schema[L, C]] = None) -> None:
73
74
  self.schema = schema
75
+ #: Log lines at the schema's levels; sent when init has enable_logs=True.
76
+ self.logger: Logger[L] = Logger(lambda: self)
74
77
 
75
78
  def _level(self, level: Optional[str]) -> Tuple[Optional[str], Dict[str, str]]:
76
79
  """The Sentry level to send, and the tag naming the app's own level."""
@@ -229,8 +232,9 @@ def init(
229
232
  ) -> Client[Any, Any]:
230
233
  """sentry_sdk.init, and the capture functions for the schema's levels.
231
234
 
232
- The options are sentry-sdk's. The DSN is on the app's setup page in
233
- StatOSS: https://<key>@ingest.statoss.com/<app number>.
235
+ The options are sentry-sdk's; enable_logs=True sends log lines. The DSN
236
+ is on the app's setup page in StatOSS:
237
+ https://<key>@ingest.statoss.com/<app number>.
234
238
  """
235
239
  global _current
236
240
  client: Client[Any, Any] = Client(schema)
@@ -282,3 +286,7 @@ def capture_message(
282
286
  def set_category(category: Optional[str], scope: Optional[sentry_sdk.Scope] = None) -> None:
283
287
  """Gives later events a category; None takes it off."""
284
288
  _current.set_category(category, scope)
289
+
290
+
291
+ default_logger: Logger[str] = Logger(lambda: _current)
292
+ """Log lines at any level, read against the schema init was given."""
@@ -0,0 +1,174 @@
1
+ """Log lines: sentry-sdk's logger, with the app's own levels.
2
+
3
+ A line at one of the app's own levels is sent at its range's level, with
4
+ its severity when the schema gives one, and its name as the attribute
5
+ statoss.level, as @statoss/sdk sends it (sdk/src/logs.ts).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import (
11
+ TYPE_CHECKING,
12
+ Any,
13
+ Callable,
14
+ Dict,
15
+ Generic,
16
+ Literal,
17
+ Mapping,
18
+ Optional,
19
+ Tuple,
20
+ Union,
21
+ get_args,
22
+ )
23
+
24
+ import sentry_sdk
25
+ from sentry_sdk import logger as sentry_logger
26
+
27
+ from ._schema import LEVEL_TAG, SEVERITY_RANGES, L, SentryLevel, is_level_name
28
+
29
+ if TYPE_CHECKING:
30
+ from ._client import Client
31
+
32
+ LogLevel = Literal["trace", "debug", "info", "warn", "warning", "error", "fatal"]
33
+ """The levels of a log line: OpenTelemetry's six ranges, warn also written warning."""
34
+
35
+ LOG_LEVELS: Tuple[str, ...] = get_args(LogLevel)
36
+
37
+ #: The range a level name or a schema's `like` is logged at.
38
+ _RANGE: Dict[str, str] = {
39
+ "trace": "trace",
40
+ "debug": "debug",
41
+ "info": "info",
42
+ "warn": "warning",
43
+ "warning": "warning",
44
+ "error": "error",
45
+ "fatal": "fatal",
46
+ "critical": "fatal",
47
+ }
48
+
49
+ #: sentry-sdk's logger function for each range.
50
+ _SEND: Dict[str, Callable[..., None]] = {
51
+ "trace": sentry_logger.trace,
52
+ "debug": sentry_logger.debug,
53
+ "info": sentry_logger.info,
54
+ "warning": sentry_logger.warning,
55
+ "error": sentry_logger.error,
56
+ "fatal": sentry_logger.fatal,
57
+ }
58
+
59
+ SEVERITY_ATTRIBUTE = "sentry.severity_number"
60
+
61
+
62
+ def _first(like: str) -> int:
63
+ return SEVERITY_RANGES[like][0]
64
+
65
+
66
+ def logs_enabled() -> bool:
67
+ """Whether init turned logs on (enable_logs=True, or in _experiments)."""
68
+ try:
69
+ options = sentry_sdk.get_client().options
70
+ except Exception:
71
+ return False
72
+ experiments = options.get("_experiments") or {}
73
+ return bool(options.get("enable_logs") or experiments.get("enable_logs"))
74
+
75
+
76
+ class Logger(Generic[L]):
77
+ """sentry-sdk's logger, with a schema's levels.
78
+
79
+ Templates take named values as sentry-sdk's do:
80
+ `logger.info("Paid {amount}", amount=12)` sends "Paid 12" with the
81
+ template and each value as attributes. `attributes=` adds more.
82
+ Nothing is sent unless init has enable_logs=True.
83
+ """
84
+
85
+ def __init__(self, client: Callable[[], Client[Any, Any]]) -> None:
86
+ self._client = client
87
+
88
+ def trace(
89
+ self, template: str, /, *, attributes: Optional[Mapping[str, Any]] = None, **params: Any
90
+ ) -> None:
91
+ self._send("trace", _first("trace"), None, template, attributes, params)
92
+
93
+ def debug(
94
+ self, template: str, /, *, attributes: Optional[Mapping[str, Any]] = None, **params: Any
95
+ ) -> None:
96
+ self._send("debug", _first("debug"), None, template, attributes, params)
97
+
98
+ def info(
99
+ self, template: str, /, *, attributes: Optional[Mapping[str, Any]] = None, **params: Any
100
+ ) -> None:
101
+ self._send("info", _first("info"), None, template, attributes, params)
102
+
103
+ def warning(
104
+ self, template: str, /, *, attributes: Optional[Mapping[str, Any]] = None, **params: Any
105
+ ) -> None:
106
+ self._send("warning", _first("warning"), None, template, attributes, params)
107
+
108
+ def error(
109
+ self, template: str, /, *, attributes: Optional[Mapping[str, Any]] = None, **params: Any
110
+ ) -> None:
111
+ self._send("error", _first("error"), None, template, attributes, params)
112
+
113
+ def fatal(
114
+ self, template: str, /, *, attributes: Optional[Mapping[str, Any]] = None, **params: Any
115
+ ) -> None:
116
+ self._send("fatal", _first("fatal"), None, template, attributes, params)
117
+
118
+ def log(
119
+ self,
120
+ level: Union[L, LogLevel, SentryLevel],
121
+ template: str,
122
+ /,
123
+ *,
124
+ attributes: Optional[Mapping[str, Any]] = None,
125
+ **params: Any,
126
+ ) -> None:
127
+ """A line at a level by name: one of the six, a Sentry level, or one
128
+ of the app's own, sent at its range's level with statoss.level."""
129
+ like, severity, own = self._level(level)
130
+ self._send(like, severity, own, template, attributes, params)
131
+
132
+ def _level(self, level: object) -> Tuple[str, int, Optional[str]]:
133
+ """The range a line goes at, its severity, and the app's own level."""
134
+ from ._client import _warn
135
+
136
+ if isinstance(level, str) and level in _RANGE:
137
+ return _RANGE[level], _first(_RANGE[level]), None
138
+ if not isinstance(level, str) or not is_level_name(level):
139
+ _warn(
140
+ "Logged %r as info: use trace, debug, info, warning, error, fatal or a "
141
+ "level name of 1 to 32 lowercase letters, digits, dots, dashes or underscores.",
142
+ level,
143
+ )
144
+ return "info", _first("info"), None
145
+ schema = self._client().schema
146
+ found = schema.levels.get(level) if schema is not None else None
147
+ like = found.ranks_like if found is not None else None
148
+ if found is None or like is None:
149
+ if schema is not None:
150
+ _warn("Level %r is not in the schema, so the line is logged as error.", level)
151
+ return "error", _first("error"), level
152
+ severity = found.severity
153
+ low, high = SEVERITY_RANGES[like]
154
+ if severity is None or not low <= severity <= high:
155
+ severity = low
156
+ return like, severity, level
157
+
158
+ def _send(
159
+ self,
160
+ like: str,
161
+ severity: int,
162
+ own: Optional[str],
163
+ template: str,
164
+ attributes: Optional[Mapping[str, Any]],
165
+ params: Dict[str, Any],
166
+ ) -> None:
167
+ if not logs_enabled():
168
+ return
169
+ attrs: Dict[str, Any] = dict(attributes or {})
170
+ if severity != _first(like):
171
+ attrs[SEVERITY_ATTRIBUTE] = severity
172
+ if own is not None:
173
+ attrs[LEVEL_TAG] = own
174
+ _SEND[like](template, attributes=attrs, **params)
File without changes
File without changes
File without changes