botanu 0.1.dev63__tar.gz → 0.1.dev68__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.
- {botanu-0.1.dev63 → botanu-0.1.dev68}/PKG-INFO +3 -1
- {botanu-0.1.dev63 → botanu-0.1.dev68}/README.md +2 -0
- {botanu-0.1.dev63 → botanu-0.1.dev68}/src/botanu/__init__.py +6 -0
- {botanu-0.1.dev63 → botanu-0.1.dev68}/src/botanu/models/run_context.py +5 -1
- {botanu-0.1.dev63 → botanu-0.1.dev68}/src/botanu/processors/__init__.py +3 -1
- botanu-0.1.dev68/src/botanu/processors/resource_enricher.py +179 -0
- botanu-0.1.dev68/src/botanu/processors/sampled.py +86 -0
- botanu-0.1.dev68/src/botanu/register.py +50 -0
- botanu-0.1.dev68/src/botanu/sampling/__init__.py +8 -0
- botanu-0.1.dev68/src/botanu/sampling/content_sampler.py +39 -0
- {botanu-0.1.dev63 → botanu-0.1.dev68}/src/botanu/sdk/__init__.py +6 -1
- {botanu-0.1.dev63 → botanu-0.1.dev68}/src/botanu/sdk/bootstrap.py +194 -17
- {botanu-0.1.dev63 → botanu-0.1.dev68}/src/botanu/sdk/config.py +145 -7
- {botanu-0.1.dev63 → botanu-0.1.dev68}/src/botanu/sdk/decorators.py +88 -4
- botanu-0.1.dev68/src/botanu/sdk/span_helpers.py +217 -0
- {botanu-0.1.dev63 → botanu-0.1.dev68}/src/botanu/tracking/data.py +51 -0
- {botanu-0.1.dev63 → botanu-0.1.dev68}/src/botanu/tracking/llm.py +45 -0
- botanu-0.1.dev63/src/botanu/sdk/span_helpers.py +0 -143
- {botanu-0.1.dev63 → botanu-0.1.dev68}/.gitignore +0 -0
- {botanu-0.1.dev63 → botanu-0.1.dev68}/LICENSE +0 -0
- {botanu-0.1.dev63 → botanu-0.1.dev68}/NOTICE +0 -0
- {botanu-0.1.dev63 → botanu-0.1.dev68}/pyproject.toml +0 -0
- {botanu-0.1.dev63 → botanu-0.1.dev68}/src/botanu/_version.py +0 -0
- {botanu-0.1.dev63 → botanu-0.1.dev68}/src/botanu/integrations/__init__.py +0 -0
- {botanu-0.1.dev63 → botanu-0.1.dev68}/src/botanu/integrations/tenacity.py +0 -0
- {botanu-0.1.dev63 → botanu-0.1.dev68}/src/botanu/models/__init__.py +0 -0
- {botanu-0.1.dev63 → botanu-0.1.dev68}/src/botanu/processors/enricher.py +0 -0
- {botanu-0.1.dev63 → botanu-0.1.dev68}/src/botanu/py.typed +0 -0
- {botanu-0.1.dev63 → botanu-0.1.dev68}/src/botanu/resources/__init__.py +0 -0
- {botanu-0.1.dev63 → botanu-0.1.dev68}/src/botanu/sdk/context.py +0 -0
- {botanu-0.1.dev63 → botanu-0.1.dev68}/src/botanu/sdk/middleware.py +0 -0
- {botanu-0.1.dev63 → botanu-0.1.dev68}/src/botanu/tracking/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: botanu
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.dev68
|
|
4
4
|
Summary: OpenTelemetry-native run-level cost attribution for AI workflows
|
|
5
5
|
Project-URL: Homepage, https://github.com/botanu-ai/botanu-sdk-python
|
|
6
6
|
Project-URL: Documentation, https://docs.botanu.ai
|
|
@@ -110,6 +110,8 @@ This SDK is built on [OpenTelemetry](https://opentelemetry.io/) for event-level
|
|
|
110
110
|
|
|
111
111
|
## Getting Started
|
|
112
112
|
|
|
113
|
+
|
|
114
|
+
|
|
113
115
|
An **event** is one business transaction — resolving a support ticket, processing
|
|
114
116
|
an order, generating a report. Each event may involve multiple **runs** (LLM calls,
|
|
115
117
|
retries, sub-workflows) across multiple services. By correlating every run to a
|
|
@@ -8,6 +8,8 @@ This SDK is built on [OpenTelemetry](https://opentelemetry.io/) for event-level
|
|
|
8
8
|
|
|
9
9
|
## Getting Started
|
|
10
10
|
|
|
11
|
+
|
|
12
|
+
|
|
11
13
|
An **event** is one business transaction — resolving a support ticket, processing
|
|
12
14
|
an order, generating a report. Each event may involve multiple **runs** (LLM calls,
|
|
13
15
|
retries, sub-workflows) across multiple services. By correlating every run to a
|
|
@@ -23,6 +23,9 @@ from botanu._version import __version__
|
|
|
23
23
|
# Run context model
|
|
24
24
|
from botanu.models.run_context import RunContext, RunOutcome, RunStatus
|
|
25
25
|
|
|
26
|
+
# Processors
|
|
27
|
+
from botanu.processors import RunContextEnricher, SampledSpanProcessor
|
|
28
|
+
|
|
26
29
|
# Bootstrap
|
|
27
30
|
from botanu.sdk.bootstrap import (
|
|
28
31
|
disable,
|
|
@@ -73,4 +76,7 @@ __all__ = [
|
|
|
73
76
|
"RunContext",
|
|
74
77
|
"RunStatus",
|
|
75
78
|
"RunOutcome",
|
|
79
|
+
# Processors
|
|
80
|
+
"RunContextEnricher",
|
|
81
|
+
"SampledSpanProcessor",
|
|
76
82
|
]
|
|
@@ -89,6 +89,7 @@ class RunContext:
|
|
|
89
89
|
event_id: str
|
|
90
90
|
customer_id: str
|
|
91
91
|
environment: str
|
|
92
|
+
step: Optional[str] = None
|
|
92
93
|
workflow_version: Optional[str] = None
|
|
93
94
|
tenant_id: Optional[str] = None
|
|
94
95
|
parent_run_id: Optional[str] = None
|
|
@@ -270,7 +271,10 @@ class RunContext:
|
|
|
270
271
|
if self.cancelled_at:
|
|
271
272
|
attrs["botanu.run.cancelled_at"] = self.cancelled_at
|
|
272
273
|
if self.outcome:
|
|
273
|
-
|
|
274
|
+
# `botanu.outcome.status` is NOT emitted (removed 2026-04-16):
|
|
275
|
+
# customer-reported outcome is trivially fakeable. Event outcome
|
|
276
|
+
# is derived from eval verdict rollup / HITL / SoR instead.
|
|
277
|
+
# Remaining fields are diagnostic only and stay for debugging.
|
|
274
278
|
if self.outcome.reason_code:
|
|
275
279
|
attrs["botanu.outcome.reason_code"] = self.outcome.reason_code
|
|
276
280
|
if self.outcome.error_class:
|
|
@@ -8,5 +8,7 @@ All other processing should happen in the OTel Collector.
|
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
from botanu.processors.enricher import RunContextEnricher
|
|
11
|
+
from botanu.processors.resource_enricher import ResourceEnricher
|
|
12
|
+
from botanu.processors.sampled import SampledSpanProcessor
|
|
11
13
|
|
|
12
|
-
__all__ = ["RunContextEnricher"]
|
|
14
|
+
__all__ = ["RunContextEnricher", "ResourceEnricher", "SampledSpanProcessor"]
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 The Botanu Authors
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""ResourceEnricher — infer `botanu.cloud_provider` + `botanu.bytes_transferred`
|
|
5
|
+
from OTel semantic-convention attributes set by auto-instrumentation.
|
|
6
|
+
|
|
7
|
+
Why this exists: the cost worker (botanu-cost-engine-workflow) prices non-LLM
|
|
8
|
+
spans via `rate × bytes_transferred` and looks up rate cards keyed by
|
|
9
|
+
`cloud_provider + system_name`. OTel auto-instrumentation emits the raw
|
|
10
|
+
attributes (`db.system`, `http.request.body.size`, `aws.service`, etc.) but
|
|
11
|
+
does NOT emit botanu-namespaced attributes in the shape the cost worker
|
|
12
|
+
reads. Without this enricher, S3 PUTs, DynamoDB ops, and egress all price to
|
|
13
|
+
$0 — see the `pricing.md` problem statement.
|
|
14
|
+
|
|
15
|
+
Attributes written:
|
|
16
|
+
|
|
17
|
+
- `botanu.cloud_provider` ("aws" | "gcp" | "azure" | …)
|
|
18
|
+
- `botanu.bytes_transferred` (int, sent + received combined)
|
|
19
|
+
|
|
20
|
+
The enricher is purely additive. It leaves all original OTel attributes
|
|
21
|
+
intact — no customer observability breaks.
|
|
22
|
+
|
|
23
|
+
Explicit values set by `set_bytes_transferred()` / `cloud_provider=` kwarg on
|
|
24
|
+
trackers take precedence: this enricher only writes if the target attribute
|
|
25
|
+
is not already present (checked at `on_end` time via the span's attribute
|
|
26
|
+
dict).
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import logging
|
|
32
|
+
from typing import Mapping, Optional
|
|
33
|
+
|
|
34
|
+
from opentelemetry import context
|
|
35
|
+
from opentelemetry.sdk.trace import ReadableSpan, Span, SpanProcessor
|
|
36
|
+
|
|
37
|
+
logger = logging.getLogger(__name__)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# System/service → cloud provider. Used when the semconv `cloud.provider`
|
|
41
|
+
# attribute is absent (most auto-instrumentations don't set it, so we infer
|
|
42
|
+
# from the db/messaging system name or the AWS/Azure/GCP service name).
|
|
43
|
+
_SYSTEM_TO_CLOUD_PROVIDER: dict[str, str] = {
|
|
44
|
+
# AWS
|
|
45
|
+
"dynamodb": "aws",
|
|
46
|
+
"s3": "aws",
|
|
47
|
+
"sqs": "aws",
|
|
48
|
+
"sns": "aws",
|
|
49
|
+
"kinesis": "aws",
|
|
50
|
+
"eventbridge": "aws",
|
|
51
|
+
"lambda": "aws",
|
|
52
|
+
"elasticache": "aws",
|
|
53
|
+
"redshift": "aws",
|
|
54
|
+
"athena": "aws",
|
|
55
|
+
"neptune": "aws",
|
|
56
|
+
"efs": "aws",
|
|
57
|
+
# GCP
|
|
58
|
+
"firestore": "gcp",
|
|
59
|
+
"bigquery": "gcp",
|
|
60
|
+
"gcs": "gcp",
|
|
61
|
+
"pubsub": "gcp",
|
|
62
|
+
# Azure
|
|
63
|
+
"cosmosdb": "azure",
|
|
64
|
+
"azure_blob": "azure",
|
|
65
|
+
"servicebus": "azure",
|
|
66
|
+
"eventhub": "azure",
|
|
67
|
+
"synapse": "azure",
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
_BOTANU_CLOUD_PROVIDER = "botanu.cloud_provider"
|
|
71
|
+
_BOTANU_BYTES_TRANSFERRED = "botanu.bytes_transferred"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ResourceEnricher(SpanProcessor):
|
|
75
|
+
"""Write botanu-namespaced resource attributes from OTel semconv data.
|
|
76
|
+
|
|
77
|
+
Runs at `on_end` (not `on_start`) — auto-instrumentation populates the
|
|
78
|
+
source attributes on span start, but some (notably http.*.body.size) are
|
|
79
|
+
only known when the response completes.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
def on_start(self, span: Span, parent_context: Optional[context.Context] = None) -> None:
|
|
83
|
+
# Cheap path: no work at start. Waiting until on_end lets us read
|
|
84
|
+
# response-time attributes that auto-instrumentation sets after the
|
|
85
|
+
# wrapped call returns (bytes, status codes, etc.).
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
def on_end(self, span: ReadableSpan) -> None:
|
|
89
|
+
attrs = span.attributes or {}
|
|
90
|
+
|
|
91
|
+
# Skip LLM spans entirely — LLM pricing goes through pricing_model_tokens
|
|
92
|
+
# (prompt/completion tokens), not bytes_transferred. Writing bytes here
|
|
93
|
+
# would double-count into cost_infra_usd.
|
|
94
|
+
if _is_llm_span(attrs):
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
cloud_provider = _infer_cloud_provider(attrs)
|
|
98
|
+
bytes_transferred = _infer_bytes_transferred(attrs)
|
|
99
|
+
|
|
100
|
+
if cloud_provider is None and bytes_transferred is None:
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
# Writing to a ReadableSpan: OTel SDK's ReadableSpan is read-only by
|
|
104
|
+
# contract, but the concrete _Span class exposes set_attribute. If
|
|
105
|
+
# the attribute is already set (explicit API or customer), skip —
|
|
106
|
+
# explicit beats inferred.
|
|
107
|
+
setter = getattr(span, "set_attribute", None)
|
|
108
|
+
if setter is None:
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
if cloud_provider is not None and _BOTANU_CLOUD_PROVIDER not in attrs:
|
|
112
|
+
setter(_BOTANU_CLOUD_PROVIDER, cloud_provider)
|
|
113
|
+
if bytes_transferred is not None and _BOTANU_BYTES_TRANSFERRED not in attrs:
|
|
114
|
+
setter(_BOTANU_BYTES_TRANSFERRED, bytes_transferred)
|
|
115
|
+
|
|
116
|
+
def shutdown(self) -> None:
|
|
117
|
+
pass
|
|
118
|
+
|
|
119
|
+
def force_flush(self, timeout_millis: int = 30000) -> bool:
|
|
120
|
+
return True
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _is_llm_span(attrs: Mapping[str, object]) -> bool:
|
|
124
|
+
return (
|
|
125
|
+
"gen_ai.request.model" in attrs
|
|
126
|
+
or "gen_ai.system" in attrs
|
|
127
|
+
or "llm.request.model" in attrs
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _infer_cloud_provider(attrs: Mapping[str, object]) -> Optional[str]:
|
|
132
|
+
# 1. Explicit semconv `cloud.provider` (if set, trust it)
|
|
133
|
+
explicit = attrs.get("cloud.provider")
|
|
134
|
+
if isinstance(explicit, str) and explicit:
|
|
135
|
+
return explicit.lower()
|
|
136
|
+
|
|
137
|
+
# 2. AWS auto-instrumentation sets `aws.service` or `rpc.system="aws-api"`
|
|
138
|
+
if attrs.get("rpc.system") == "aws-api" or "aws.service" in attrs or "aws.region" in attrs:
|
|
139
|
+
return "aws"
|
|
140
|
+
if "gcp.service" in attrs or "gcp.project_id" in attrs:
|
|
141
|
+
return "gcp"
|
|
142
|
+
if "azure.resource" in attrs or "azure.namespace" in attrs:
|
|
143
|
+
return "azure"
|
|
144
|
+
|
|
145
|
+
# 3. Infer from system name (db.system, messaging.system, botanu.storage.system)
|
|
146
|
+
for key in ("db.system", "messaging.system", "botanu.storage.system"):
|
|
147
|
+
val = attrs.get(key)
|
|
148
|
+
if isinstance(val, str):
|
|
149
|
+
provider = _SYSTEM_TO_CLOUD_PROVIDER.get(val.lower())
|
|
150
|
+
if provider:
|
|
151
|
+
return provider
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _infer_bytes_transferred(attrs: Mapping[str, object]) -> Optional[int]:
|
|
156
|
+
total = 0
|
|
157
|
+
saw_any = False
|
|
158
|
+
|
|
159
|
+
# OTel HTTP semconv (stable)
|
|
160
|
+
for key in ("http.request.body.size", "http.response.body.size"):
|
|
161
|
+
val = attrs.get(key)
|
|
162
|
+
if isinstance(val, int) and val >= 0:
|
|
163
|
+
total += val
|
|
164
|
+
saw_any = True
|
|
165
|
+
|
|
166
|
+
# botanu tracker attrs (fallback — populated by DBTracker.set_result etc.)
|
|
167
|
+
if not saw_any:
|
|
168
|
+
for key in (
|
|
169
|
+
"botanu.data.bytes_read",
|
|
170
|
+
"botanu.data.bytes_written",
|
|
171
|
+
"botanu.messaging.bytes_transferred",
|
|
172
|
+
"botanu.warehouse.bytes_scanned",
|
|
173
|
+
):
|
|
174
|
+
val = attrs.get(key)
|
|
175
|
+
if isinstance(val, int) and val >= 0:
|
|
176
|
+
total += val
|
|
177
|
+
saw_any = True
|
|
178
|
+
|
|
179
|
+
return total if saw_any else None
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 The Botanu Authors
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""SampledSpanProcessor — preserves the customer's sampling ratio.
|
|
5
|
+
|
|
6
|
+
When botanu changes the TracerProvider sampler to AlwaysOn (to capture 100%),
|
|
7
|
+
existing customer processors (Datadog exporter, Jaeger exporter, etc.) would
|
|
8
|
+
suddenly see 10x the span volume if the customer had ratio-based sampling.
|
|
9
|
+
|
|
10
|
+
This processor wraps an existing processor and applies the customer's original
|
|
11
|
+
ratio at the export level. Result: the customer's exporter sees the same volume
|
|
12
|
+
as before, their bill is unchanged, their dashboards are unchanged.
|
|
13
|
+
|
|
14
|
+
botanu's own processor is NOT wrapped — it sees 100%.
|
|
15
|
+
|
|
16
|
+
Sampling is deterministic: the same trace_id always gets the same decision.
|
|
17
|
+
This matches OTel's ``TraceIdRatioBasedSampler`` algorithm.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import logging
|
|
23
|
+
from typing import Optional
|
|
24
|
+
|
|
25
|
+
from opentelemetry import context
|
|
26
|
+
from opentelemetry.sdk.trace import ReadableSpan, SpanProcessor
|
|
27
|
+
from opentelemetry.trace import Span
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SampledSpanProcessor(SpanProcessor):
|
|
33
|
+
"""Wraps a SpanProcessor with deterministic ratio sampling.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
wrapped: The original processor to wrap (e.g., BatchSpanProcessor
|
|
37
|
+
sending to Datadog).
|
|
38
|
+
ratio: Sampling ratio (0.0 to 1.0). 0.1 means 10% of spans are
|
|
39
|
+
forwarded to the wrapped processor.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, wrapped: SpanProcessor, ratio: float) -> None:
|
|
43
|
+
if not 0.0 <= ratio <= 1.0:
|
|
44
|
+
raise ValueError(f"ratio must be between 0.0 and 1.0, got {ratio}")
|
|
45
|
+
self._wrapped = wrapped
|
|
46
|
+
self._ratio = ratio
|
|
47
|
+
# Pre-compute bound for comparison (avoids per-span float math)
|
|
48
|
+
self._bound = int(ratio * (2**64 - 1))
|
|
49
|
+
|
|
50
|
+
def _should_sample(self, trace_id: int) -> bool:
|
|
51
|
+
"""Deterministic sampling decision based on trace_id.
|
|
52
|
+
|
|
53
|
+
Uses the upper 64 bits of the 128-bit trace_id, matching OTel's
|
|
54
|
+
TraceIdRatioBasedSampler algorithm. Same trace_id always produces
|
|
55
|
+
the same decision.
|
|
56
|
+
"""
|
|
57
|
+
if self._ratio >= 1.0:
|
|
58
|
+
return True
|
|
59
|
+
if self._ratio <= 0.0:
|
|
60
|
+
return False
|
|
61
|
+
# Upper 64 bits of trace_id for deterministic comparison
|
|
62
|
+
upper = trace_id >> 64 if trace_id.bit_length() > 64 else trace_id
|
|
63
|
+
return upper <= self._bound
|
|
64
|
+
|
|
65
|
+
def on_start(
|
|
66
|
+
self,
|
|
67
|
+
span: Span,
|
|
68
|
+
parent_context: Optional[context.Context] = None,
|
|
69
|
+
) -> None:
|
|
70
|
+
# Gate on_start with the same decision as on_end. Forwarding on_start
|
|
71
|
+
# unconditionally while gating on_end orphans spans inside wrapped
|
|
72
|
+
# processors (BatchSpanProcessor, Datadog exporter, etc.) — they hold
|
|
73
|
+
# start-time bookkeeping for spans whose on_end never fires. Over time
|
|
74
|
+
# this leaks memory in the customer's process.
|
|
75
|
+
if self._should_sample(span.context.trace_id):
|
|
76
|
+
self._wrapped.on_start(span, parent_context)
|
|
77
|
+
|
|
78
|
+
def on_end(self, span: ReadableSpan) -> None:
|
|
79
|
+
if self._should_sample(span.context.trace_id):
|
|
80
|
+
self._wrapped.on_end(span)
|
|
81
|
+
|
|
82
|
+
def shutdown(self) -> None:
|
|
83
|
+
self._wrapped.shutdown()
|
|
84
|
+
|
|
85
|
+
def force_flush(self, timeout_millis: int = 30000) -> bool:
|
|
86
|
+
return self._wrapped.force_flush(timeout_millis)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 The Botanu Authors
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Zero-code initialization entry point.
|
|
5
|
+
|
|
6
|
+
Import this module to auto-initialize Botanu SDK with no code changes.
|
|
7
|
+
All configuration is read from environment variables or botanu.yaml.
|
|
8
|
+
|
|
9
|
+
Usage::
|
|
10
|
+
|
|
11
|
+
# As a Python module flag
|
|
12
|
+
python -m botanu.register && python app.py
|
|
13
|
+
|
|
14
|
+
# Or via PYTHONPATH preload (works with gunicorn, uvicorn, etc.)
|
|
15
|
+
python -c "import botanu.register" && python app.py
|
|
16
|
+
|
|
17
|
+
# Or in gunicorn config
|
|
18
|
+
# gunicorn.conf.py:
|
|
19
|
+
def on_starting(server):
|
|
20
|
+
import botanu.register # noqa: F401
|
|
21
|
+
|
|
22
|
+
# Or in uvicorn
|
|
23
|
+
uvicorn app:app --env-file .env
|
|
24
|
+
|
|
25
|
+
# Or in Dockerfile
|
|
26
|
+
ENV BOTANU_API_KEY=btnu_live_...
|
|
27
|
+
ENV BOTANU_SERVICE_NAME=my-service
|
|
28
|
+
CMD ["python", "-c", "import botanu.register; import uvicorn; uvicorn.run('app:app')"]
|
|
29
|
+
|
|
30
|
+
Configuration (env vars or botanu.yaml):
|
|
31
|
+
|
|
32
|
+
BOTANU_API_KEY - API key (required for Botanu Cloud)
|
|
33
|
+
BOTANU_SERVICE_NAME - Service name (recommended)
|
|
34
|
+
BOTANU_ENVIRONMENT - Environment (default: production)
|
|
35
|
+
|
|
36
|
+
See docs/getting-started/configuration.md for full options.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
import logging
|
|
42
|
+
|
|
43
|
+
from botanu.sdk.bootstrap import enable
|
|
44
|
+
|
|
45
|
+
logger = logging.getLogger(__name__)
|
|
46
|
+
|
|
47
|
+
result = enable()
|
|
48
|
+
|
|
49
|
+
if result:
|
|
50
|
+
logger.info("Botanu SDK auto-initialized via botanu.register")
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 The Botanu Authors
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Sampling primitives — content capture gate, future trace samplers."""
|
|
5
|
+
|
|
6
|
+
from botanu.sampling.content_sampler import should_capture_content
|
|
7
|
+
|
|
8
|
+
__all__ = ["should_capture_content"]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 The Botanu Authors
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Content capture sampling gate for eval.
|
|
5
|
+
|
|
6
|
+
MVP: simple ``random.random() < rate`` check. The ``event_id`` parameter is
|
|
7
|
+
accepted now so that a Month 2+ upgrade to hash-based deterministic sampling
|
|
8
|
+
(SHA-256 of ``tenant_id || event_id``) won't break callers. Deterministic
|
|
9
|
+
sampling matters for replays and backfills; simple random is sufficient for
|
|
10
|
+
MVP volume.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import random
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def should_capture_content(rate: float, event_id: Optional[str] = None) -> bool:
|
|
20
|
+
"""Return True if this call's content should be captured.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
rate: Capture rate in [0.0, 1.0]. 0.0 disables capture (default,
|
|
24
|
+
privacy-safe). 1.0 captures everything (sandbox/shadow).
|
|
25
|
+
Production typically uses 0.10–0.20.
|
|
26
|
+
event_id: Currently unused. Present so a future deterministic-hash
|
|
27
|
+
implementation can be swapped in without API churn.
|
|
28
|
+
|
|
29
|
+
Examples:
|
|
30
|
+
>>> should_capture_content(0.0)
|
|
31
|
+
False
|
|
32
|
+
>>> should_capture_content(1.0)
|
|
33
|
+
True
|
|
34
|
+
"""
|
|
35
|
+
if rate <= 0.0:
|
|
36
|
+
return False
|
|
37
|
+
if rate >= 1.0:
|
|
38
|
+
return True
|
|
39
|
+
return random.random() < rate
|
|
@@ -15,7 +15,11 @@ from botanu.sdk.context import (
|
|
|
15
15
|
set_baggage,
|
|
16
16
|
)
|
|
17
17
|
from botanu.sdk.decorators import botanu_outcome, botanu_workflow, run_botanu, workflow
|
|
18
|
-
from botanu.sdk.span_helpers import
|
|
18
|
+
from botanu.sdk.span_helpers import (
|
|
19
|
+
emit_outcome,
|
|
20
|
+
set_business_context,
|
|
21
|
+
set_correlation,
|
|
22
|
+
)
|
|
19
23
|
|
|
20
24
|
__all__ = [
|
|
21
25
|
"BotanuConfig",
|
|
@@ -33,5 +37,6 @@ __all__ = [
|
|
|
33
37
|
"run_botanu",
|
|
34
38
|
"set_baggage",
|
|
35
39
|
"set_business_context",
|
|
40
|
+
"set_correlation",
|
|
36
41
|
"workflow",
|
|
37
42
|
]
|