verica-observability 0.1.1__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,40 @@
1
+ # deps
2
+ node_modules/
3
+ .pnpm-store/
4
+
5
+ # python sdk
6
+ sdks/python/.venv/
7
+ sdks/**/__pycache__/
8
+
9
+ # test coverage
10
+ coverage/
11
+
12
+ # build output
13
+ dist/
14
+ .next/
15
+ out/
16
+ *.tsbuildinfo
17
+
18
+ # turbo
19
+ .turbo/
20
+
21
+ # env / secrets
22
+ .env
23
+ .env.*
24
+ !.env.example
25
+ # key material (RSA credential keypair, etc.) — must never be committed; the
26
+ # real values live base64-encoded in .env (and prod secret stores).
27
+ *.pem
28
+
29
+ # logs
30
+ *.log
31
+ npm-debug.log*
32
+
33
+ # os / editor
34
+ .DS_Store
35
+ .idea/
36
+ .vscode/*
37
+ !.vscode/extensions.json
38
+
39
+ # drizzle local
40
+ packages/db/drizzle/meta/_journal.bak
@@ -0,0 +1,57 @@
1
+ Metadata-Version: 2.4
2
+ Name: verica-observability
3
+ Version: 0.1.1
4
+ Summary: Two-line LLM tracing for Verica: init(token) and your OpenAI/Anthropic calls land as evaluable traces.
5
+ Project-URL: Homepage, https://verica.app
6
+ License-Expression: MIT
7
+ Keywords: evals,llm,observability,opentelemetry,tracing,verica
8
+ Requires-Python: >=3.9
9
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.27
10
+ Requires-Dist: opentelemetry-instrumentation-anthropic>=0.33
11
+ Requires-Dist: opentelemetry-instrumentation-openai>=0.33
12
+ Requires-Dist: opentelemetry-sdk>=1.27
13
+ Provides-Extra: dev
14
+ Requires-Dist: pytest>=8; extra == 'dev'
15
+ Requires-Dist: ruff>=0.6; extra == 'dev'
16
+ Description-Content-Type: text/markdown
17
+
18
+ # verica-observability
19
+
20
+ Two-line LLM tracing for [Verica](https://verica.app): your OpenAI/Anthropic
21
+ calls land as evaluable traces.
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ pip install verica-observability
27
+ ```
28
+
29
+ ## Use
30
+
31
+ ```python
32
+ import verica
33
+
34
+ verica.init(token=os.environ["VERICA_TOKEN"])
35
+
36
+ # Import AFTER init so the client is patched.
37
+ from openai import OpenAI
38
+ ```
39
+
40
+ ## Serverless
41
+
42
+ Call `verica.flush()` (or `verica.shutdown()`) before the runtime freezes so
43
+ the span batch is exported.
44
+
45
+ ## Options
46
+
47
+ | Option / env var | Default | Notes |
48
+ | -------------------------------------------- | ---------- | ------------------------------- |
49
+ | `token` / `VERICA_TOKEN` | (required) | ingest-scoped API token |
50
+ | `capture_content` / `VERICA_CAPTURE_CONTENT` | `true` | send prompt/response content |
51
+ | `conversation_id` | (none) | stamps `gen_ai.conversation.id` |
52
+ | `service_name` / `OTEL_SERVICE_NAME` | `app` | resource service.name |
53
+ | `debug` / `VERICA_DEBUG` | `false` | log export errors |
54
+
55
+ Fail-open by design: if Verica is unreachable or the token is invalid, spans are
56
+ dropped and your app is never affected. Export errors are silent unless `debug`
57
+ is on.
@@ -0,0 +1,40 @@
1
+ # verica-observability
2
+
3
+ Two-line LLM tracing for [Verica](https://verica.app): your OpenAI/Anthropic
4
+ calls land as evaluable traces.
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ pip install verica-observability
10
+ ```
11
+
12
+ ## Use
13
+
14
+ ```python
15
+ import verica
16
+
17
+ verica.init(token=os.environ["VERICA_TOKEN"])
18
+
19
+ # Import AFTER init so the client is patched.
20
+ from openai import OpenAI
21
+ ```
22
+
23
+ ## Serverless
24
+
25
+ Call `verica.flush()` (or `verica.shutdown()`) before the runtime freezes so
26
+ the span batch is exported.
27
+
28
+ ## Options
29
+
30
+ | Option / env var | Default | Notes |
31
+ | -------------------------------------------- | ---------- | ------------------------------- |
32
+ | `token` / `VERICA_TOKEN` | (required) | ingest-scoped API token |
33
+ | `capture_content` / `VERICA_CAPTURE_CONTENT` | `true` | send prompt/response content |
34
+ | `conversation_id` | (none) | stamps `gen_ai.conversation.id` |
35
+ | `service_name` / `OTEL_SERVICE_NAME` | `app` | resource service.name |
36
+ | `debug` / `VERICA_DEBUG` | `false` | log export errors |
37
+
38
+ Fail-open by design: if Verica is unreachable or the token is invalid, spans are
39
+ dropped and your app is never affected. Export errors are silent unless `debug`
40
+ is on.
@@ -0,0 +1,26 @@
1
+ # Run from the repo (worker/web running, OPENAI_API_KEY + VERICA_TOKEN set):
2
+ # ./sdks/python/.venv/bin/pip install openai
3
+ # ./sdks/python/.venv/bin/python sdks/python/examples/basic.py
4
+ import os
5
+
6
+ import verica
7
+
8
+ ok = verica.init(
9
+ token=os.environ.get("VERICA_TOKEN"),
10
+ endpoint=os.environ.get("VERICA_ENDPOINT", "http://localhost:3000"),
11
+ service_name="verica-example-python",
12
+ conversation_id="verica-sdk-check-python",
13
+ )
14
+ if not ok:
15
+ raise SystemExit(1)
16
+
17
+ # Import AFTER init so the instrumentation patches the client.
18
+ from openai import OpenAI # noqa: E402
19
+
20
+ client = OpenAI()
21
+ res = client.chat.completions.create(
22
+ model="gpt-4o-mini",
23
+ messages=[{"role": "user", "content": 'Say "verica sdk check python" and nothing else.'}],
24
+ )
25
+ print(res.choices[0].message.content)
26
+ verica.shutdown() # drain the batch before the process exits
@@ -0,0 +1,30 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "verica-observability"
7
+ version = "0.1.1"
8
+ description = "Two-line LLM tracing for Verica: init(token) and your OpenAI/Anthropic calls land as evaluable traces."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.9"
12
+ keywords = ["verica", "observability", "opentelemetry", "llm", "tracing", "evals"]
13
+ dependencies = [
14
+ "opentelemetry-sdk>=1.27",
15
+ "opentelemetry-exporter-otlp-proto-http>=1.27",
16
+ "opentelemetry-instrumentation-openai>=0.33",
17
+ "opentelemetry-instrumentation-anthropic>=0.33",
18
+ ]
19
+
20
+ [project.urls]
21
+ Homepage = "https://verica.app"
22
+
23
+ [project.optional-dependencies]
24
+ dev = ["pytest>=8", "ruff>=0.6"]
25
+
26
+ [tool.hatch.build.targets.wheel]
27
+ packages = ["verica"]
28
+
29
+ [tool.ruff]
30
+ line-length = 100
@@ -0,0 +1,63 @@
1
+ from verica._config import resolve_config
2
+
3
+
4
+ def test_resolves_from_options():
5
+ cfg, missing = resolve_config({"token": "vk_1", "endpoint": "https://x.app"}, {})
6
+ assert missing == []
7
+ assert cfg.token == "vk_1"
8
+ assert cfg.endpoint == "https://x.app"
9
+ assert cfg.capture_content is True
10
+ assert cfg.service_name == "app"
11
+ assert cfg.debug is False
12
+
13
+
14
+ def test_env_fallback_and_trailing_slash():
15
+ cfg, missing = resolve_config(
16
+ {}, {"VERICA_TOKEN": "vk_2", "VERICA_ENDPOINT": "https://x.app/"}
17
+ )
18
+ assert missing == []
19
+ assert cfg.endpoint == "https://x.app"
20
+
21
+
22
+ def test_options_win_over_env():
23
+ cfg, _ = resolve_config(
24
+ {"token": "opt", "endpoint": "https://opt.app"},
25
+ {"VERICA_TOKEN": "env", "VERICA_ENDPOINT": "https://env.app"},
26
+ )
27
+ assert cfg.token == "opt"
28
+
29
+
30
+ def test_missing_token_reported_not_raised():
31
+ cfg, missing = resolve_config({}, {})
32
+ assert cfg is None
33
+ assert missing == ["token"]
34
+
35
+
36
+ def test_endpoint_defaults_to_hosted_ingest():
37
+ cfg, missing = resolve_config({"token": "t"}, {})
38
+ assert missing == []
39
+ assert cfg.endpoint == "https://ingest.verica.app"
40
+
41
+
42
+ def test_endpoint_option_and_env_override_the_default():
43
+ cfg, _ = resolve_config({"token": "t", "endpoint": "http://localhost:3001"}, {})
44
+ assert cfg.endpoint == "http://localhost:3001"
45
+ cfg, _ = resolve_config({"token": "t"}, {"VERICA_ENDPOINT": "http://localhost:3001"})
46
+ assert cfg.endpoint == "http://localhost:3001"
47
+
48
+
49
+ def test_capture_content_env_off_and_option_override():
50
+ base = {"token": "t", "endpoint": "https://x"}
51
+ cfg, _ = resolve_config(base, {"VERICA_CAPTURE_CONTENT": "false"})
52
+ assert cfg.capture_content is False
53
+ cfg, _ = resolve_config({**base, "capture_content": True}, {"VERICA_CAPTURE_CONTENT": "false"})
54
+ assert cfg.capture_content is True
55
+
56
+
57
+ def test_debug_and_service_name_fallbacks():
58
+ cfg, _ = resolve_config(
59
+ {"token": "t", "endpoint": "https://x"},
60
+ {"VERICA_DEBUG": "1", "OTEL_SERVICE_NAME": "my-svc"},
61
+ )
62
+ assert cfg.debug is True
63
+ assert cfg.service_name == "my-svc"
@@ -0,0 +1,25 @@
1
+ import verica
2
+
3
+
4
+ def teardown_function():
5
+ verica.shutdown()
6
+
7
+
8
+ def test_init_without_config_is_noop_false(monkeypatch):
9
+ monkeypatch.delenv("VERICA_TOKEN", raising=False)
10
+ monkeypatch.delenv("VERICA_ENDPOINT", raising=False)
11
+ assert verica.init() is False
12
+
13
+
14
+ def test_init_is_idempotent_and_fail_open():
15
+ # Unreachable endpoint: init must still succeed (batch export is async).
16
+ assert verica.init(token="vk_t", endpoint="http://127.0.0.1:9") is True
17
+ assert verica.init(token="vk_t", endpoint="http://127.0.0.1:9") is True
18
+
19
+
20
+ def test_flush_and_shutdown_never_raise():
21
+ verica.flush()
22
+ verica.shutdown()
23
+ verica.init(token="vk_t", endpoint="http://127.0.0.1:9")
24
+ verica.flush()
25
+ verica.shutdown()
@@ -0,0 +1,6 @@
1
+ """Two-line LLM tracing for Verica."""
2
+
3
+ from verica._sdk import flush, init, shutdown
4
+
5
+ __version__ = "0.1.1"
6
+ __all__ = ["init", "flush", "shutdown"]
@@ -0,0 +1,50 @@
1
+ """Config resolution: options > env vars > defaults. Pure and import-safe."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Mapping, Optional
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class Config:
9
+ token: str
10
+ endpoint: str
11
+ capture_content: bool
12
+ conversation_id: Optional[str]
13
+ service_name: str
14
+ debug: bool
15
+
16
+
17
+ # Verica's hosted ingest endpoint. Override via the `endpoint` option or the
18
+ # VERICA_ENDPOINT env var for self-host or local dev. Only `token` is required.
19
+ DEFAULT_ENDPOINT = "https://ingest.verica.app"
20
+
21
+
22
+ def _truthy(value: Optional[str]) -> bool:
23
+ return value in ("1", "true")
24
+
25
+
26
+ def resolve_config(options: dict, env: Mapping[str, str]) -> "tuple[Optional[Config], list[str]]":
27
+ token = options.get("token") or env.get("VERICA_TOKEN") or ""
28
+ if not token:
29
+ return None, ["token"]
30
+
31
+ raw_endpoint = options.get("endpoint") or env.get("VERICA_ENDPOINT") or DEFAULT_ENDPOINT
32
+
33
+ capture = options.get("capture_content")
34
+ if capture is None:
35
+ raw = env.get("VERICA_CAPTURE_CONTENT")
36
+ capture = _truthy(raw) if raw is not None else True
37
+
38
+ return (
39
+ Config(
40
+ token=token,
41
+ endpoint=raw_endpoint.rstrip("/"),
42
+ capture_content=capture,
43
+ conversation_id=options.get("conversation_id"),
44
+ service_name=options.get("service_name") or env.get("OTEL_SERVICE_NAME") or "app",
45
+ debug=options.get("debug")
46
+ if options.get("debug") is not None
47
+ else _truthy(env.get("VERICA_DEBUG")),
48
+ ),
49
+ [],
50
+ )
@@ -0,0 +1,116 @@
1
+ """Fail-open OTel setup: never raise into the host app."""
2
+
3
+ import logging
4
+ import os
5
+
6
+ from verica._config import Config, resolve_config
7
+
8
+ logger = logging.getLogger("verica")
9
+
10
+ _provider = None # type: ignore[var-annotated]
11
+
12
+
13
+ class _ConversationIdProcessor:
14
+ """Stamps gen_ai.conversation.id on every span (Phase 4 session join)."""
15
+
16
+ def __init__(self, conversation_id: str):
17
+ self._id = conversation_id
18
+
19
+ def on_start(self, span, parent_context=None):
20
+ span.set_attribute("gen_ai.conversation.id", self._id)
21
+
22
+ def on_end(self, span):
23
+ pass
24
+
25
+ def shutdown(self):
26
+ pass
27
+
28
+ def force_flush(self, timeout_millis: int = 30000):
29
+ return True
30
+
31
+
32
+ def init(**options) -> bool:
33
+ global _provider
34
+ if _provider is not None:
35
+ logger.warning("verica.init() called twice; ignoring the second call.")
36
+ return True
37
+
38
+ cfg, missing = resolve_config(options, os.environ)
39
+ if cfg is None:
40
+ logger.warning("verica: missing %s; tracing is disabled.", " and ".join(missing))
41
+ return False
42
+
43
+ try:
44
+ from opentelemetry import trace
45
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
46
+ from opentelemetry.sdk.resources import SERVICE_NAME, Resource
47
+ from opentelemetry.sdk.trace import TracerProvider
48
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor
49
+
50
+ # openllmetry instrumentations gate content capture on this env var.
51
+ os.environ.setdefault("TRACELOOP_TRACE_CONTENT", str(cfg.capture_content).lower())
52
+
53
+ # Spec §5: export errors (401, network) must not spam the host app's
54
+ # logs; the OTLP exporter logs through this logger on every failure.
55
+ if not cfg.debug:
56
+ logging.getLogger("opentelemetry.exporter.otlp.proto.http.trace_exporter").setLevel(
57
+ logging.CRITICAL
58
+ )
59
+ logging.getLogger("opentelemetry.sdk.trace.export").setLevel(logging.CRITICAL)
60
+
61
+ provider = TracerProvider(resource=Resource.create({SERVICE_NAME: cfg.service_name}))
62
+ if cfg.conversation_id:
63
+ provider.add_span_processor(_ConversationIdProcessor(cfg.conversation_id))
64
+ provider.add_span_processor(
65
+ BatchSpanProcessor(
66
+ OTLPSpanExporter(
67
+ endpoint=f"{cfg.endpoint}/v1/traces",
68
+ headers={"Authorization": f"Bearer {cfg.token}"},
69
+ )
70
+ )
71
+ )
72
+ trace.set_tracer_provider(provider)
73
+ _instrument(provider, cfg)
74
+ _provider = provider
75
+ return True
76
+ except Exception as err: # noqa: BLE001 - fail-open by contract
77
+ logger.warning("verica: init failed; tracing is disabled. %s", err if cfg.debug else "")
78
+ _provider = None
79
+ return False
80
+
81
+
82
+ def _instrument(provider, cfg: Config) -> None:
83
+ """Each instrumentor is optional: a missing/broken one never blocks init."""
84
+ try:
85
+ from opentelemetry.instrumentation.openai import OpenAIInstrumentor
86
+
87
+ OpenAIInstrumentor().instrument(tracer_provider=provider)
88
+ except Exception as err: # noqa: BLE001
89
+ if cfg.debug:
90
+ logger.warning("verica: openai instrumentation not active: %s", err)
91
+ try:
92
+ from opentelemetry.instrumentation.anthropic import AnthropicInstrumentor
93
+
94
+ AnthropicInstrumentor().instrument(tracer_provider=provider)
95
+ except Exception as err: # noqa: BLE001
96
+ if cfg.debug:
97
+ logger.warning("verica: anthropic instrumentation not active: %s", err)
98
+
99
+
100
+ def flush() -> None:
101
+ try:
102
+ if _provider is not None:
103
+ _provider.force_flush()
104
+ except Exception: # noqa: BLE001
105
+ logger.debug("verica: flush failed", exc_info=True)
106
+
107
+
108
+ def shutdown() -> None:
109
+ global _provider
110
+ try:
111
+ if _provider is not None:
112
+ _provider.shutdown()
113
+ except Exception: # noqa: BLE001
114
+ logger.debug("verica: shutdown failed", exc_info=True)
115
+ finally:
116
+ _provider = None