traceiq-capture 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,9 @@
1
+ sdk/.venv/
2
+ sdk/.pytest_cache/
3
+ sdk/traces/
4
+ backend/.venv/
5
+ backend/.env
6
+ **/__pycache__/
7
+ *.sqlite3
8
+ node_modules/
9
+ .env
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 TraceIQ
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,129 @@
1
+ Metadata-Version: 2.5
2
+ Name: traceiq-capture
3
+ Version: 0.1.0
4
+ Summary: TraceIQ Capture — OpenTelemetry instrumentation with a locked attribute contract
5
+ Author: TraceIQ
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Keywords: ai,cost,llm,observability,opentelemetry,otlp
9
+ Requires-Python: >=3.10
10
+ Requires-Dist: opentelemetry-api>=1.27.0
11
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.27.0
12
+ Requires-Dist: opentelemetry-sdk>=1.27.0
13
+ Provides-Extra: all
14
+ Requires-Dist: openai; extra == 'all'
15
+ Requires-Dist: opentelemetry-instrumentation-anthropic; extra == 'all'
16
+ Requires-Dist: opentelemetry-instrumentation-langchain; extra == 'all'
17
+ Requires-Dist: opentelemetry-instrumentation-llamaindex; extra == 'all'
18
+ Requires-Dist: opentelemetry-instrumentation-openai; extra == 'all'
19
+ Provides-Extra: anthropic
20
+ Requires-Dist: opentelemetry-instrumentation-anthropic; extra == 'anthropic'
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
23
+ Requires-Dist: pytest>=8.0; extra == 'dev'
24
+ Provides-Extra: langchain
25
+ Requires-Dist: opentelemetry-instrumentation-langchain; extra == 'langchain'
26
+ Provides-Extra: langgraph
27
+ Requires-Dist: langgraph; extra == 'langgraph'
28
+ Requires-Dist: opentelemetry-instrumentation-langchain; extra == 'langgraph'
29
+ Provides-Extra: llamaindex
30
+ Requires-Dist: opentelemetry-instrumentation-llamaindex; extra == 'llamaindex'
31
+ Provides-Extra: openai
32
+ Requires-Dist: openai; extra == 'openai'
33
+ Requires-Dist: opentelemetry-instrumentation-openai; extra == 'openai'
34
+ Description-Content-Type: text/markdown
35
+
36
+ # TraceIQ Capture (`traceiq-capture`)
37
+
38
+ OpenTelemetry-native Python SDK for AI agent / LLM cost observability. Locked attribute contract (no field sprawl). Framework coverage via optional OTel instrumentations — not Traceloop.
39
+
40
+ Install name: **`traceiq-capture`**. Import: **`import traceiq`**.
41
+
42
+ ## Install
43
+
44
+ ```bash
45
+ cd sdk
46
+ pip install -e . # installs package traceiq-capture
47
+ pip install -e ".[openai]" # OpenAI auto-instrumentation
48
+ pip install -e ".[langchain]" # LangChain / LangGraph
49
+ pip install -e ".[anthropic]"
50
+ pip install -e ".[llamaindex]"
51
+ pip install -e ".[all]"
52
+ pip install -e ".[dev]" # pytest
53
+ ```
54
+
55
+ ## Quickstart
56
+
57
+ Set env (optional for local file export):
58
+
59
+ ```bash
60
+ export TRACEIQ_API_KEY=...
61
+ export TRACEIQ_ENDPOINT=https://api.example.com # omit → ./traces/
62
+ export TRACEIQ_APP_NAME=support-agent-api # optional
63
+ export TRACEIQ_ENVIRONMENT=production # optional
64
+ ```
65
+
66
+ ```python
67
+ import traceiq
68
+
69
+ traceiq.init()
70
+
71
+ # Existing OpenAI / LangChain / Anthropic / LlamaIndex code unchanged.
72
+ # Installed extras are auto-instrumented on init — no decorators required.
73
+ ```
74
+
75
+ No `TRACEIQ_ENDPOINT` → spans written under `./traces/` for local debug. Batch export every **5s**; process exit flushes automatically (`flush()` is only needed in short scripts/tests).
76
+
77
+ ## Optional: agent trees and attribution
78
+
79
+ Use decorators when you want a run root and step types. Use `set_context` when you want product / project / agent rollups.
80
+
81
+ ```python
82
+ from traceiq import workflow, set_context
83
+
84
+ @workflow(name="triage_ticket")
85
+ def handle(ticket_id: str, user_msg: str):
86
+ set_context(agent_name="triage", product_name="Pro")
87
+ return run_agent(user_msg)
88
+ ```
89
+
90
+ Also available: `@task`, `@tool`, `@agent`, `clear_context`.
91
+
92
+ ## Frameworks
93
+
94
+ Install the matching extra; call `init()` once. LLM library calls emit child spans automatically.
95
+
96
+ | Stack | Extra |
97
+ |-------|-------|
98
+ | OpenAI | `[openai]` |
99
+ | Anthropic | `[anthropic]` |
100
+ | LangChain / LangGraph | `[langchain]` / `[langgraph]` |
101
+ | LlamaIndex | `[llamaindex]` |
102
+ | CrewAI | `[openai]` and/or `[langchain]` |
103
+
104
+ Decorators are optional — add `@workflow` on your entrypoint only if you want TraceIQ run identity around the call tree.
105
+
106
+ ## Public API
107
+
108
+ `init`, `flush`, `set_context`, `clear_context`, `@workflow`, `@task`, `@tool`, `@agent`
109
+
110
+ ## Export
111
+
112
+ - Default: OTLP/HTTP JSON → `{endpoint}/v1/otlp/v1/traces` (Bearer)
113
+ - Fallback: `export_mode="spans"` → `/v1/spans/batch`
114
+ - Batch every **5s** (`disable_batch=True` for sync scripts)
115
+
116
+ ## Attributes (locked)
117
+
118
+ See build spec §3. Only allowlisted `traceiq.*` plus `gen_ai.system` / `gen_ai.request.model` / usage tokens. No prompt/completion content capture. No client-side pricing.
119
+
120
+ ## Develop
121
+
122
+ ```bash
123
+ pip install -e ".[dev]"
124
+ pytest
125
+ ```
126
+
127
+ ## License
128
+
129
+ MIT
@@ -0,0 +1,94 @@
1
+ # TraceIQ Capture (`traceiq-capture`)
2
+
3
+ OpenTelemetry-native Python SDK for AI agent / LLM cost observability. Locked attribute contract (no field sprawl). Framework coverage via optional OTel instrumentations — not Traceloop.
4
+
5
+ Install name: **`traceiq-capture`**. Import: **`import traceiq`**.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ cd sdk
11
+ pip install -e . # installs package traceiq-capture
12
+ pip install -e ".[openai]" # OpenAI auto-instrumentation
13
+ pip install -e ".[langchain]" # LangChain / LangGraph
14
+ pip install -e ".[anthropic]"
15
+ pip install -e ".[llamaindex]"
16
+ pip install -e ".[all]"
17
+ pip install -e ".[dev]" # pytest
18
+ ```
19
+
20
+ ## Quickstart
21
+
22
+ Set env (optional for local file export):
23
+
24
+ ```bash
25
+ export TRACEIQ_API_KEY=...
26
+ export TRACEIQ_ENDPOINT=https://api.example.com # omit → ./traces/
27
+ export TRACEIQ_APP_NAME=support-agent-api # optional
28
+ export TRACEIQ_ENVIRONMENT=production # optional
29
+ ```
30
+
31
+ ```python
32
+ import traceiq
33
+
34
+ traceiq.init()
35
+
36
+ # Existing OpenAI / LangChain / Anthropic / LlamaIndex code unchanged.
37
+ # Installed extras are auto-instrumented on init — no decorators required.
38
+ ```
39
+
40
+ No `TRACEIQ_ENDPOINT` → spans written under `./traces/` for local debug. Batch export every **5s**; process exit flushes automatically (`flush()` is only needed in short scripts/tests).
41
+
42
+ ## Optional: agent trees and attribution
43
+
44
+ Use decorators when you want a run root and step types. Use `set_context` when you want product / project / agent rollups.
45
+
46
+ ```python
47
+ from traceiq import workflow, set_context
48
+
49
+ @workflow(name="triage_ticket")
50
+ def handle(ticket_id: str, user_msg: str):
51
+ set_context(agent_name="triage", product_name="Pro")
52
+ return run_agent(user_msg)
53
+ ```
54
+
55
+ Also available: `@task`, `@tool`, `@agent`, `clear_context`.
56
+
57
+ ## Frameworks
58
+
59
+ Install the matching extra; call `init()` once. LLM library calls emit child spans automatically.
60
+
61
+ | Stack | Extra |
62
+ |-------|-------|
63
+ | OpenAI | `[openai]` |
64
+ | Anthropic | `[anthropic]` |
65
+ | LangChain / LangGraph | `[langchain]` / `[langgraph]` |
66
+ | LlamaIndex | `[llamaindex]` |
67
+ | CrewAI | `[openai]` and/or `[langchain]` |
68
+
69
+ Decorators are optional — add `@workflow` on your entrypoint only if you want TraceIQ run identity around the call tree.
70
+
71
+ ## Public API
72
+
73
+ `init`, `flush`, `set_context`, `clear_context`, `@workflow`, `@task`, `@tool`, `@agent`
74
+
75
+ ## Export
76
+
77
+ - Default: OTLP/HTTP JSON → `{endpoint}/v1/otlp/v1/traces` (Bearer)
78
+ - Fallback: `export_mode="spans"` → `/v1/spans/batch`
79
+ - Batch every **5s** (`disable_batch=True` for sync scripts)
80
+
81
+ ## Attributes (locked)
82
+
83
+ See build spec §3. Only allowlisted `traceiq.*` plus `gen_ai.system` / `gen_ai.request.model` / usage tokens. No prompt/completion content capture. No client-side pricing.
84
+
85
+ ## Develop
86
+
87
+ ```bash
88
+ pip install -e ".[dev]"
89
+ pytest
90
+ ```
91
+
92
+ ## License
93
+
94
+ MIT
@@ -0,0 +1,45 @@
1
+ """Manual example — init-only happy path (no decorators required).
2
+
3
+ Uses OpenAI if OPENAI_API_KEY is set; otherwise emits a fake LLM span.
4
+ Unset TRACEIQ_ENDPOINT → spans land in ./traces/
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+
11
+ import traceiq
12
+
13
+
14
+ def main() -> None:
15
+ api_key = os.environ.get("OPENAI_API_KEY")
16
+ if api_key:
17
+ from openai import OpenAI
18
+
19
+ client = OpenAI(api_key=api_key)
20
+ resp = client.chat.completions.create(
21
+ model="gpt-4o-mini",
22
+ messages=[{"role": "user", "content": "Say hi in one word."}],
23
+ )
24
+ print("openai:", resp.choices[0].message.content)
25
+ else:
26
+ from opentelemetry import trace
27
+
28
+ tracer = trace.get_tracer("demo")
29
+ with tracer.start_as_current_span("fake_llm") as span:
30
+ span.set_attribute("gen_ai.request.model", "gpt-4o-mini")
31
+ span.set_attribute("gen_ai.usage.input_tokens", 5)
32
+ span.set_attribute("gen_ai.usage.output_tokens", 1)
33
+ span.set_attribute("gen_ai.system", "openai")
34
+ print("no OPENAI_API_KEY — emitted fake_llm span")
35
+
36
+
37
+ if __name__ == "__main__":
38
+ # Zero-arg init: env TRACEIQ_* or local ./traces/ when endpoint unset.
39
+ # disable_batch so this short script writes spans before exit.
40
+ os.environ.setdefault("TRACEIQ_APP_NAME", "sdk-example")
41
+ os.environ.setdefault("TRACEIQ_ENVIRONMENT", "dev")
42
+ traceiq.init(disable_batch=True)
43
+ main()
44
+ # Optional upgrades: @workflow / set_context for run trees & attribution.
45
+ print("done — check ./traces/ if TRACEIQ_ENDPOINT is unset")
@@ -0,0 +1,44 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "traceiq-capture"
7
+ version = "0.1.0"
8
+ description = "TraceIQ Capture — OpenTelemetry instrumentation with a locked attribute contract"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [{ name = "TraceIQ" }]
13
+ keywords = ["ai", "observability", "opentelemetry", "otlp", "llm", "cost"]
14
+ dependencies = [
15
+ "opentelemetry-api>=1.27.0",
16
+ "opentelemetry-sdk>=1.27.0",
17
+ "opentelemetry-exporter-otlp-proto-http>=1.27.0",
18
+ ]
19
+
20
+ [project.optional-dependencies]
21
+ openai = ["opentelemetry-instrumentation-openai", "openai"]
22
+ anthropic = ["opentelemetry-instrumentation-anthropic"]
23
+ langchain = ["opentelemetry-instrumentation-langchain"]
24
+ langgraph = ["opentelemetry-instrumentation-langchain", "langgraph"]
25
+ llamaindex = ["opentelemetry-instrumentation-llamaindex"]
26
+ all = [
27
+ "opentelemetry-instrumentation-openai",
28
+ "opentelemetry-instrumentation-anthropic",
29
+ "opentelemetry-instrumentation-langchain",
30
+ "opentelemetry-instrumentation-llamaindex",
31
+ "openai",
32
+ ]
33
+ dev = ["pytest>=8.0", "pytest-asyncio>=0.23"]
34
+
35
+ [tool.hatch.build.targets.wheel]
36
+ packages = ["src/traceiq"]
37
+
38
+ [tool.hatch.build.targets.sdist]
39
+ include = ["/src", "/tests", "/README.md", "/LICENSE", "/example.py"]
40
+
41
+ [tool.pytest.ini_options]
42
+ testpaths = ["tests"]
43
+ pythonpath = ["src"]
44
+ asyncio_mode = "auto"
@@ -0,0 +1,17 @@
1
+ """TraceIQ Capture — OpenTelemetry instrumentation with a locked attribute contract."""
2
+
3
+ from traceiq._context import clear_context, set_context
4
+ from traceiq._init import flush, init
5
+ from traceiq.decorators import agent, task, tool, workflow
6
+
7
+ __all__ = [
8
+ "init",
9
+ "flush",
10
+ "set_context",
11
+ "clear_context",
12
+ "workflow",
13
+ "task",
14
+ "tool",
15
+ "agent",
16
+ ]
17
+ __version__ = "0.1.0"
@@ -0,0 +1,113 @@
1
+ """Normalize gen_ai.* token/model/provider attrs (read-only fallbacks)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Mapping, MutableMapping
6
+
7
+ ATTR_SYSTEM = "gen_ai.system"
8
+ ATTR_REQUEST_MODEL = "gen_ai.request.model"
9
+ ATTR_RESPONSE_MODEL = "gen_ai.response.model"
10
+ ATTR_INPUT_TOKENS = "gen_ai.usage.input_tokens"
11
+ ATTR_OUTPUT_TOKENS = "gen_ai.usage.output_tokens"
12
+ ATTR_PROMPT_TOKENS = "gen_ai.usage.prompt_tokens"
13
+ ATTR_COMPLETION_TOKENS = "gen_ai.usage.completion_tokens"
14
+ ATTR_PROVIDER_NAME = "gen_ai.provider.name"
15
+
16
+ # Cache key variants seen in the wild
17
+ CACHE_READ_CANDIDATES = (
18
+ "gen_ai.usage.cache_read_input_tokens",
19
+ "gen_ai.usage.cache_read_tokens",
20
+ "llm.usage.cache_read_input_tokens",
21
+ )
22
+ CACHE_WRITE_CANDIDATES = (
23
+ "gen_ai.usage.cache_creation_input_tokens",
24
+ "gen_ai.usage.cache_write_tokens",
25
+ "llm.usage.cache_creation_input_tokens",
26
+ )
27
+
28
+ _MODEL_PREFIX_TO_SYSTEM = (
29
+ (("gpt-", "o1", "o3", "text-embedding", "whisper", "tts-"), "openai"),
30
+ (("claude-",), "anthropic"),
31
+ (("gemini-",), "google"),
32
+ )
33
+
34
+
35
+ def _get(attrs: Mapping[str, Any], *keys: str) -> Any:
36
+ for key in keys:
37
+ if key in attrs and attrs[key] is not None:
38
+ return attrs[key]
39
+ return None
40
+
41
+
42
+ def normalize_tokens(attrs: MutableMapping[str, Any]) -> None:
43
+ """prompt/completion → input/output tokens."""
44
+ if ATTR_INPUT_TOKENS not in attrs:
45
+ legacy = _get(attrs, ATTR_PROMPT_TOKENS, "llm.usage.prompt_tokens")
46
+ if legacy is not None:
47
+ attrs[ATTR_INPUT_TOKENS] = int(legacy)
48
+ if ATTR_OUTPUT_TOKENS not in attrs:
49
+ legacy = _get(attrs, ATTR_COMPLETION_TOKENS, "llm.usage.completion_tokens")
50
+ if legacy is not None:
51
+ attrs[ATTR_OUTPUT_TOKENS] = int(legacy)
52
+
53
+
54
+ def ensure_model(attrs: MutableMapping[str, Any]) -> None:
55
+ if ATTR_REQUEST_MODEL not in attrs or not attrs[ATTR_REQUEST_MODEL]:
56
+ resp = _get(attrs, ATTR_RESPONSE_MODEL, "llm.request.model", "llm.model_name")
57
+ if resp:
58
+ attrs[ATTR_REQUEST_MODEL] = str(resp)
59
+
60
+
61
+ def ensure_provider(attrs: MutableMapping[str, Any]) -> None:
62
+ if attrs.get(ATTR_SYSTEM):
63
+ return
64
+ provider = _get(attrs, ATTR_PROVIDER_NAME)
65
+ if provider:
66
+ attrs[ATTR_SYSTEM] = str(provider)
67
+ return
68
+ model = str(attrs.get(ATTR_REQUEST_MODEL) or "")
69
+ lower = model.lower()
70
+ for prefixes, system in _MODEL_PREFIX_TO_SYSTEM:
71
+ if any(lower.startswith(p) for p in prefixes):
72
+ attrs[ATTR_SYSTEM] = system
73
+ return
74
+
75
+
76
+ def extract_cache_usage(attrs: Mapping[str, Any]) -> dict[str, int]:
77
+ out: dict[str, int] = {}
78
+ for key in CACHE_READ_CANDIDATES:
79
+ if key in attrs and attrs[key] is not None:
80
+ out["traceiq.usage.cache_read_tokens"] = int(attrs[key])
81
+ break
82
+ for key in CACHE_WRITE_CANDIDATES:
83
+ if key in attrs and attrs[key] is not None:
84
+ out["traceiq.usage.cache_write_tokens"] = int(attrs[key])
85
+ break
86
+ return out
87
+
88
+
89
+ def extract_audio_vision_usage(attrs: Mapping[str, Any]) -> dict[str, int]:
90
+ out: dict[str, int] = {}
91
+ audio_in = _get(
92
+ attrs,
93
+ "gen_ai.usage.input_audio_tokens",
94
+ "traceiq.usage.audio_input_tokens",
95
+ )
96
+ audio_out = _get(
97
+ attrs,
98
+ "gen_ai.usage.output_audio_tokens",
99
+ "traceiq.usage.audio_output_tokens",
100
+ )
101
+ vision = _get(
102
+ attrs,
103
+ "gen_ai.usage.image_tokens",
104
+ "llm.usage.image_tokens",
105
+ "traceiq.usage.vision_tokens",
106
+ )
107
+ if audio_in is not None:
108
+ out["traceiq.usage.audio_input_tokens"] = int(audio_in)
109
+ if audio_out is not None:
110
+ out["traceiq.usage.audio_output_tokens"] = int(audio_out)
111
+ if vision is not None:
112
+ out["traceiq.usage.vision_tokens"] = int(vision)
113
+ return out
@@ -0,0 +1,120 @@
1
+ """Process globals + per-request ContextVar (locked §3.1 fields only)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from contextvars import ContextVar
6
+ from dataclasses import dataclass, fields
7
+ from typing import Any
8
+
9
+ INGEST_SOURCE = "sdk"
10
+
11
+ # Allowlisted ContextVar keys (set_context)
12
+ _CONTEXT_KEYS = frozenset(
13
+ {
14
+ "workflow_id",
15
+ "agent_name",
16
+ "product_id",
17
+ "product_name",
18
+ "project_id",
19
+ "project_name",
20
+ }
21
+ )
22
+
23
+
24
+ @dataclass
25
+ class _Globals:
26
+ app_name: str = ""
27
+ environment: str = ""
28
+ ingest_source: str = INGEST_SOURCE
29
+ api_key: str = ""
30
+ endpoint: str = ""
31
+ export_mode: str = "otlp"
32
+ disable_batch: bool = False
33
+ initialized: bool = False
34
+
35
+
36
+ @dataclass
37
+ class _RequestContext:
38
+ workflow_id: str = ""
39
+ agent_name: str = ""
40
+ product_id: str = ""
41
+ product_name: str = ""
42
+ project_id: str = ""
43
+ project_name: str = ""
44
+
45
+
46
+ _globals = _Globals()
47
+ _request_ctx: ContextVar[_RequestContext] = ContextVar(
48
+ "traceiq_request_ctx", default=_RequestContext()
49
+ )
50
+
51
+
52
+ def configure_globals(
53
+ *,
54
+ app_name: str = "",
55
+ environment: str = "",
56
+ api_key: str = "",
57
+ endpoint: str = "",
58
+ export_mode: str = "otlp",
59
+ disable_batch: bool = False,
60
+ ) -> None:
61
+ _globals.app_name = app_name or ""
62
+ _globals.environment = environment or ""
63
+ _globals.ingest_source = INGEST_SOURCE
64
+ _globals.api_key = api_key or ""
65
+ _globals.endpoint = (endpoint or "").rstrip("/")
66
+ _globals.export_mode = export_mode or "otlp"
67
+ _globals.disable_batch = bool(disable_batch)
68
+ _globals.initialized = True
69
+
70
+
71
+ def get_globals() -> _Globals:
72
+ return _globals
73
+
74
+
75
+ def set_context(**kwargs: Any) -> None:
76
+ """Per-request dims. Allowed: workflow_id, agent_name, product_*, project_*."""
77
+ unknown = set(kwargs) - _CONTEXT_KEYS
78
+ if unknown:
79
+ raise TypeError(f"Unknown set_context fields: {sorted(unknown)}")
80
+ current = _request_ctx.get()
81
+ data = {f.name: getattr(current, f.name) for f in fields(current)}
82
+ for key, value in kwargs.items():
83
+ if value is None:
84
+ continue
85
+ data[key] = str(value)
86
+ _request_ctx.set(_RequestContext(**data))
87
+
88
+
89
+ def clear_context() -> None:
90
+ _request_ctx.set(_RequestContext())
91
+
92
+
93
+ def get_request_context() -> _RequestContext:
94
+ return _request_ctx.get()
95
+
96
+
97
+ def merged_trace_attrs() -> dict[str, str]:
98
+ """Globals + request context for stamping on every span (§3.1)."""
99
+ g = _globals
100
+ r = _request_ctx.get()
101
+ out: dict[str, str] = {
102
+ "traceiq.ingest_source": INGEST_SOURCE,
103
+ }
104
+ if g.app_name:
105
+ out["traceiq.app_name"] = g.app_name
106
+ if g.environment:
107
+ out["traceiq.environment"] = g.environment
108
+ if r.workflow_id:
109
+ out["traceiq.workflow_id"] = r.workflow_id
110
+ if r.agent_name:
111
+ out["traceiq.agent_name"] = r.agent_name
112
+ if r.product_id:
113
+ out["traceiq.product.id"] = r.product_id
114
+ if r.product_name:
115
+ out["traceiq.product.name"] = r.product_name
116
+ if r.project_id:
117
+ out["traceiq.project.id"] = r.project_id
118
+ if r.project_name:
119
+ out["traceiq.project.name"] = r.project_name
120
+ return out