kensa-sdk 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,26 @@
1
+ node_modules
2
+ .next
3
+ next-env.d.ts
4
+ out
5
+ coverage
6
+ .env
7
+ .env.local
8
+ .env.*.local
9
+ *.tsbuildinfo
10
+ .DS_Store
11
+ .pnpm-store
12
+ .coverage
13
+ .pytest_cache
14
+ .ruff_cache
15
+ .venv
16
+ __pycache__
17
+ .vercel
18
+ .wrangler/
19
+ .zed
20
+ .specs
21
+ .worktrees
22
+ .codex/
23
+ .claude/
24
+ .agents/
25
+ .env*
26
+ !.env.example
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Borgware
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,92 @@
1
+ Metadata-Version: 2.5
2
+ Name: kensa-sdk
3
+ Version: 0.1.0
4
+ Summary: Thin OpenTelemetry wrapper that exports agent traces to Kensa
5
+ Project-URL: Homepage, https://kensa.sh
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.12
9
+ Requires-Dist: opentelemetry-api>=1.42.0
10
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.42.0
11
+ Requires-Dist: opentelemetry-sdk>=1.42.0
12
+ Description-Content-Type: text/markdown
13
+
14
+ # kensa-sdk
15
+
16
+ Send your agent's OpenTelemetry traces to [Kensa](https://kensa.sh).
17
+
18
+ ```bash
19
+ pip install kensa-sdk
20
+ ```
21
+
22
+ The package installs as `kensa-sdk` and imports as `kensa`.
23
+
24
+ ## Quick start
25
+
26
+ ```python
27
+ import os
28
+
29
+ import kensa
30
+
31
+ kensa.init(api_key=os.environ.get("KENSA_API_KEY"))
32
+ ```
33
+
34
+ Spans are sent in the background. Automatic process-exit cleanup uses OpenTelemetry's
35
+ standard shutdown, which currently waits up to 30 seconds to drain its queue. Slow exports
36
+ can therefore leave queued spans unsent, even though each export allows 120 seconds.
37
+
38
+ For a finite run, stop producing spans and call `kensa.flush()` before exiting. In a
39
+ serverless function, flush before the response ends. The installed OpenTelemetry processor
40
+ does not enforce an overall flush deadline, so multiple batches can take longer than
41
+ 120 seconds to drain.
42
+
43
+ ```python
44
+ kensa.flush()
45
+ ```
46
+
47
+ `kensa.init()` sends spans; your existing instrumentation produces them. Without an API
48
+ key it does nothing and logs a warning, and nothing the SDK does raises into your
49
+ application.
50
+
51
+ ## Already using OpenTelemetry?
52
+
53
+ `kensa.init()` attaches to the tracer provider your application installed instead of
54
+ replacing it, so call it after that provider is set up. Pass `provider=` if you have
55
+ not registered yours globally:
56
+
57
+ ```python
58
+ kensa.init(api_key=os.environ.get("KENSA_API_KEY"), provider=my_provider)
59
+ ```
60
+
61
+ Your provider's resource is fixed when it is created, so set the service name,
62
+ `service.version`, and `deployment.environment.name` on it yourself, from your
63
+ platform's variables (table below); Kensa logs a warning when the deployment is
64
+ missing. `service_name` only applies when Kensa installs its own provider.
65
+
66
+ ## Configuration
67
+
68
+ | Setting | Set with | Default |
69
+ | ------------ | ------------------------------------------------ | --------------------------- |
70
+ | API key | `init(api_key=...)` or `KENSA_API_KEY` | Required |
71
+ | Service name | `init(service_name=...)` or `OTEL_SERVICE_NAME` | `unknown_service` |
72
+ | Deployment | `OTEL_RESOURCE_ATTRIBUTES` | Detected from your platform |
73
+
74
+ Kensa checks a fix against the traces from the deployment that shipped it, so it needs
75
+ the deployed commit and the environment. `kensa.init()` detects them:
76
+
77
+ | Platform | Commit | Environment |
78
+ | ---------------- | ------------------------ | -------------------------- |
79
+ | Vercel | `VERCEL_GIT_COMMIT_SHA` | `VERCEL_ENV` |
80
+ | Railway | `RAILWAY_GIT_COMMIT_SHA` | `RAILWAY_ENVIRONMENT_NAME` |
81
+ | Render | `RENDER_GIT_COMMIT` | |
82
+
83
+ On Render, set the environment yourself (below). CI and local runs are never detected
84
+ as a deployment.
85
+
86
+ Anywhere else, or to override detection, set the standard OpenTelemetry variable:
87
+
88
+ ```bash
89
+ OTEL_RESOURCE_ATTRIBUTES="service.version=<commit sha>,deployment.environment.name=production"
90
+ ```
91
+
92
+ Requires Python 3.12 or later.
@@ -0,0 +1,79 @@
1
+ # kensa-sdk
2
+
3
+ Send your agent's OpenTelemetry traces to [Kensa](https://kensa.sh).
4
+
5
+ ```bash
6
+ pip install kensa-sdk
7
+ ```
8
+
9
+ The package installs as `kensa-sdk` and imports as `kensa`.
10
+
11
+ ## Quick start
12
+
13
+ ```python
14
+ import os
15
+
16
+ import kensa
17
+
18
+ kensa.init(api_key=os.environ.get("KENSA_API_KEY"))
19
+ ```
20
+
21
+ Spans are sent in the background. Automatic process-exit cleanup uses OpenTelemetry's
22
+ standard shutdown, which currently waits up to 30 seconds to drain its queue. Slow exports
23
+ can therefore leave queued spans unsent, even though each export allows 120 seconds.
24
+
25
+ For a finite run, stop producing spans and call `kensa.flush()` before exiting. In a
26
+ serverless function, flush before the response ends. The installed OpenTelemetry processor
27
+ does not enforce an overall flush deadline, so multiple batches can take longer than
28
+ 120 seconds to drain.
29
+
30
+ ```python
31
+ kensa.flush()
32
+ ```
33
+
34
+ `kensa.init()` sends spans; your existing instrumentation produces them. Without an API
35
+ key it does nothing and logs a warning, and nothing the SDK does raises into your
36
+ application.
37
+
38
+ ## Already using OpenTelemetry?
39
+
40
+ `kensa.init()` attaches to the tracer provider your application installed instead of
41
+ replacing it, so call it after that provider is set up. Pass `provider=` if you have
42
+ not registered yours globally:
43
+
44
+ ```python
45
+ kensa.init(api_key=os.environ.get("KENSA_API_KEY"), provider=my_provider)
46
+ ```
47
+
48
+ Your provider's resource is fixed when it is created, so set the service name,
49
+ `service.version`, and `deployment.environment.name` on it yourself, from your
50
+ platform's variables (table below); Kensa logs a warning when the deployment is
51
+ missing. `service_name` only applies when Kensa installs its own provider.
52
+
53
+ ## Configuration
54
+
55
+ | Setting | Set with | Default |
56
+ | ------------ | ------------------------------------------------ | --------------------------- |
57
+ | API key | `init(api_key=...)` or `KENSA_API_KEY` | Required |
58
+ | Service name | `init(service_name=...)` or `OTEL_SERVICE_NAME` | `unknown_service` |
59
+ | Deployment | `OTEL_RESOURCE_ATTRIBUTES` | Detected from your platform |
60
+
61
+ Kensa checks a fix against the traces from the deployment that shipped it, so it needs
62
+ the deployed commit and the environment. `kensa.init()` detects them:
63
+
64
+ | Platform | Commit | Environment |
65
+ | ---------------- | ------------------------ | -------------------------- |
66
+ | Vercel | `VERCEL_GIT_COMMIT_SHA` | `VERCEL_ENV` |
67
+ | Railway | `RAILWAY_GIT_COMMIT_SHA` | `RAILWAY_ENVIRONMENT_NAME` |
68
+ | Render | `RENDER_GIT_COMMIT` | |
69
+
70
+ On Render, set the environment yourself (below). CI and local runs are never detected
71
+ as a deployment.
72
+
73
+ Anywhere else, or to override detection, set the standard OpenTelemetry variable:
74
+
75
+ ```bash
76
+ OTEL_RESOURCE_ATTRIBUTES="service.version=<commit sha>,deployment.environment.name=production"
77
+ ```
78
+
79
+ Requires Python 3.12 or later.
@@ -0,0 +1,37 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "kensa-sdk"
7
+ version = "0.1.0"
8
+ description = "Thin OpenTelemetry wrapper that exports agent traces to Kensa"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.12"
13
+ dependencies = [
14
+ "opentelemetry-api>=1.42.0",
15
+ "opentelemetry-sdk>=1.42.0",
16
+ "opentelemetry-exporter-otlp-proto-http>=1.42.0",
17
+ ]
18
+
19
+ [project.urls]
20
+ Homepage = "https://kensa.sh"
21
+
22
+ [dependency-groups]
23
+ dev = ["pytest>=8.0", "ruff>=0.16.4", "ty>=0.0.1a14"]
24
+
25
+ [tool.hatch.build.targets.wheel]
26
+ packages = ["src/kensa"]
27
+
28
+ [tool.ruff]
29
+ line-length = 100
30
+ target-version = "py312"
31
+
32
+ [tool.ruff.lint]
33
+ select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
34
+
35
+ [tool.pytest.ini_options]
36
+ addopts = "--strict-markers"
37
+ testpaths = ["tests"]
@@ -0,0 +1,6 @@
1
+ """Send OpenTelemetry traces to Kensa."""
2
+
3
+ from kensa.tracing import _VERSION, flush, init
4
+
5
+ __all__ = ["flush", "init"]
6
+ __version__: str = _VERSION or "unknown"
File without changes
@@ -0,0 +1,197 @@
1
+ """Send OpenTelemetry spans to Kensa."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import os
7
+ from collections.abc import Iterator, Mapping
8
+ from contextlib import contextmanager
9
+ from importlib.metadata import PackageNotFoundError, version
10
+ from typing import Final
11
+
12
+ from opentelemetry import trace
13
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
14
+ from opentelemetry.sdk.resources import Resource
15
+ from opentelemetry.sdk.trace import SpanProcessor, TracerProvider
16
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor
17
+
18
+ __all__ = ["flush", "init"]
19
+
20
+ _LOGGER = logging.getLogger("kensa")
21
+ _DEFAULT_ENDPOINT: Final = "https://ingest.kensa.sh/v1/traces"
22
+ # Ingest accepts at most 4 MiB per request and GenAI spans carry whole prompts, so
23
+ # batches stay far below the exporter's default of 512 spans.
24
+ _MAX_EXPORT_BATCH_SIZE: Final = 24
25
+ _IDENTITY_KEYS: Final = ("service.version", "deployment.environment.name")
26
+
27
+ # Full commit SHAs that hosting platforms expose at runtime. Kensa matches a reported
28
+ # fix's commit against the service.version of later traces. Deploy platforms only: CI
29
+ # and local runs must never pass for production traffic.
30
+ _COMMIT_VARIABLES: Final = (
31
+ "VERCEL_GIT_COMMIT_SHA",
32
+ "RENDER_GIT_COMMIT",
33
+ "RAILWAY_GIT_COMMIT_SHA",
34
+ )
35
+ _ENVIRONMENT_VARIABLES: Final = ("VERCEL_ENV", "RAILWAY_ENVIRONMENT_NAME")
36
+ _ENVIRONMENT_KEYS: Final = ("deployment.environment.name", "deployment.environment")
37
+ _FOREIGN_EXPORTER_PREFIXES: Final = ("OTEL_EXPORTER_OTLP_", "OTEL_PYTHON_EXPORTER_OTLP_")
38
+ _DEFAULT_MAX_QUEUE_SIZE: Final = 2048
39
+
40
+
41
+ def _package_version() -> str | None:
42
+ # pyproject.toml is the only place the version is written; installing stamps it into
43
+ # the package metadata. Frozen or vendored copies may lack that metadata.
44
+ try:
45
+ return version("kensa-sdk")
46
+ except PackageNotFoundError:
47
+ return None
48
+
49
+
50
+ _VERSION: Final = _package_version()
51
+
52
+ _processor: SpanProcessor | None = None
53
+ _warned_missing_key = False
54
+
55
+
56
+ def _first(env: Mapping[str, str], names: tuple[str, ...]) -> str | None:
57
+ for name in names:
58
+ value = env.get(name, "").strip()
59
+ if value:
60
+ return value
61
+ return None
62
+
63
+
64
+ def _detected_attributes(env: Mapping[str, str]) -> dict[str, str]:
65
+ attributes: dict[str, str] = {}
66
+ if commit := _first(env, _COMMIT_VARIABLES):
67
+ attributes["service.version"] = commit
68
+ if environment := _first(env, _ENVIRONMENT_VARIABLES):
69
+ # Both spellings: semantic conventions renamed the attribute, and Kensa prefers
70
+ # the new one.
71
+ attributes["deployment.environment.name"] = environment
72
+ attributes["deployment.environment"] = environment
73
+ return attributes
74
+
75
+
76
+ def _resource(service_name: str | None, env: Mapping[str, str]) -> Resource:
77
+ # Detected values, overridden by OTEL_SERVICE_NAME and OTEL_RESOURCE_ATTRIBUTES
78
+ # (which Resource.create reads), overridden by an explicit service name.
79
+ explicit: dict[str, str] = {"service.name": service_name} if service_name else {}
80
+ configured = Resource.create(explicit)
81
+ detected = _detected_attributes(env)
82
+ # An environment set through OTEL_RESOURCE_ATTRIBUTES, in either spelling, replaces
83
+ # the detected one entirely rather than leaving the two spellings disagreeing.
84
+ if any(key in configured.attributes for key in _ENVIRONMENT_KEYS):
85
+ for key in _ENVIRONMENT_KEYS:
86
+ detected.pop(key, None)
87
+ resource = Resource(detected).merge(configured)
88
+ if _VERSION:
89
+ resource = resource.merge(Resource({"kensa.instrumentation.version": _VERSION}))
90
+ return resource
91
+
92
+
93
+ @contextmanager
94
+ def _without_otlp_environment() -> Iterator[None]:
95
+ # OTLPSpanExporter reads OTEL_EXPORTER_OTLP_* (certificates, client keys, timeout) and
96
+ # a credential-provider session when constructed. Those belong to whatever other
97
+ # backend the application exports to, so hide them while Kensa's exporter is built.
98
+ hidden = {k: v for k, v in os.environ.items() if k.startswith(_FOREIGN_EXPORTER_PREFIXES)}
99
+ for name in hidden:
100
+ del os.environ[name]
101
+ try:
102
+ yield
103
+ finally:
104
+ os.environ.update(hidden)
105
+
106
+
107
+ def _batch_size() -> int:
108
+ # BatchSpanProcessor rejects a batch larger than its queue, so honour a smaller one.
109
+ try:
110
+ queue = int(os.environ.get("OTEL_BSP_MAX_QUEUE_SIZE", _DEFAULT_MAX_QUEUE_SIZE))
111
+ except ValueError:
112
+ queue = _DEFAULT_MAX_QUEUE_SIZE
113
+ return max(1, min(_MAX_EXPORT_BATCH_SIZE, queue))
114
+
115
+
116
+ def _warn_missing_key() -> None:
117
+ global _warned_missing_key
118
+ if not _warned_missing_key:
119
+ _warned_missing_key = True
120
+ _LOGGER.warning("KENSA_API_KEY is not set, so no traces are sent to Kensa.")
121
+
122
+
123
+ def _warn_missing_identity(provider: TracerProvider) -> None:
124
+ attributes = provider.resource.attributes
125
+ missing = [key for key in _IDENTITY_KEYS if not attributes.get(key)]
126
+ if missing:
127
+ _LOGGER.warning(
128
+ "Kensa attached to this application's tracer provider and cannot set %s on "
129
+ "it. Add them to your own Resource so Kensa can tell which deployment a fix "
130
+ "shipped in.",
131
+ ", ".join(missing),
132
+ )
133
+
134
+
135
+ def init(
136
+ *,
137
+ api_key: str | None = None,
138
+ service_name: str | None = None,
139
+ provider: TracerProvider | None = None,
140
+ ) -> None:
141
+ """Start sending this process's traces to Kensa.
142
+
143
+ Only the first call with an API key (``api_key`` or ``KENSA_API_KEY``) takes
144
+ effect. Attaches to the application's tracer provider if it has one, or to
145
+ ``provider`` if given; otherwise installs its own. Call it after any code that sets
146
+ up a tracer provider, or that provider cannot be installed.
147
+ """
148
+ global _processor
149
+ if _processor is not None:
150
+ return
151
+ key = api_key or os.environ.get("KENSA_API_KEY")
152
+ if not key:
153
+ _warn_missing_key()
154
+ return
155
+
156
+ try:
157
+ with _without_otlp_environment():
158
+ exporter = OTLPSpanExporter(
159
+ endpoint=os.environ.get("KENSA_ENDPOINT") or _DEFAULT_ENDPOINT,
160
+ headers={"authorization": f"Bearer {key}"},
161
+ # Ingest redacts prompt-rich batches before acknowledging them.
162
+ timeout=120,
163
+ )
164
+ processor = BatchSpanProcessor(exporter, max_export_batch_size=_batch_size())
165
+ existing = provider if provider is not None else trace.get_tracer_provider()
166
+ if isinstance(existing, TracerProvider):
167
+ existing.add_span_processor(processor)
168
+ _warn_missing_identity(existing)
169
+ else:
170
+ # Keep OpenTelemetry's default automatic shutdown and its drain deadline.
171
+ installed = TracerProvider(resource=_resource(service_name, os.environ))
172
+ installed.add_span_processor(processor)
173
+ trace.set_tracer_provider(installed)
174
+ if trace.get_tracer_provider() is not installed:
175
+ # Another vendor's non-SDK provider holds the global slot and cannot take
176
+ # Kensa's processor.
177
+ processor.shutdown()
178
+ _LOGGER.error(
179
+ "Another tracer provider is registered and Kensa cannot attach to it. "
180
+ "Pass your OpenTelemetry SDK TracerProvider as kensa.init(provider=...)."
181
+ )
182
+ return
183
+ except Exception as error: # Instrumentation must never break the application.
184
+ _LOGGER.warning("Not sending traces to Kensa: %s", error)
185
+ return
186
+ _processor = processor
187
+
188
+
189
+ def flush() -> None:
190
+ """Send any buffered spans now.
191
+
192
+ Call after span production stops, including before a serverless response. Automatic
193
+ shutdown uses OpenTelemetry's default deadline and may drop slow batches. The installed
194
+ processor does not enforce an overall flush deadline. A failed send is logged.
195
+ """
196
+ if _processor is not None and not _processor.force_flush(timeout_millis=120_000):
197
+ _LOGGER.warning("Some spans could not be sent to Kensa.")