Skip to content
This repository was archived by the owner on Apr 30, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions overmind_sdk/filexporter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from __future__ import annotations

import json
import threading
from pathlib import Path
from typing import IO, Sequence

from google.protobuf.json_format import MessageToDict
from opentelemetry.exporter.otlp.proto.common.trace_encoder import encode_spans
from opentelemetry.sdk.trace import ReadableSpan
from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult


class FileSpanExporter(SpanExporter):
def __init__(
self,
file_path: str | Path | IO[str],
) -> None:
self.file_path = Path(file_path) if isinstance(file_path, str) else file_path
self._lock = threading.Lock()
self._file: IO[str] | None = None

def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult:
with self._lock:
if not self._file:
if isinstance(self.file_path, Path):
self._file = self.file_path.open('a', encoding='utf-8')
else:
self._file = self.file_path
encoded = encode_spans(spans)
data = MessageToDict(encoded, preserving_proto_field_name=True)
self._file.write(json.dumps(data) + '\n')
self._file.flush()
return SpanExportResult.SUCCESS

def force_flush(self, timeout_millis: int = 30000) -> bool:
return True

def shutdown(self) -> None:
with self._lock:
if self._file:
self._file.flush()
if self._file is not self.file_path:
self._file.close()
10 changes: 9 additions & 1 deletion overmind_sdk/tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,15 @@ def init(
# Configure OTLP Exporter
headers = {"X-API-Token": overmind_api_key}

otlp_exporter = OTLPSpanExporter(endpoint=endpoint, headers=headers)
if overmind_base_url:
otlp_exporter = OTLPSpanExporter(endpoint=endpoint, headers=headers)
else:
# No URL provided: fallback to writing spans to disk for local debugging
from overmind_sdk.filexporter import FileSpanExporter
otlp_exporter = FileSpanExporter(
file_path=os.environ.get("OVERMIND_TRACE_FILE", "overmind-traces.jsonl"),
)

span_processor = BatchSpanProcessor(otlp_exporter)
provider.add_span_processor(span_processor)
span_processor.on_start = _span_processor_on_start
Expand Down
6 changes: 1 addition & 5 deletions overmind_sdk/utils/api_settings.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import os
from overmind_sdk.client import OvermindError

LOCAL_API_KEY_PREFIX = "ovr_core_"
LOCAL_BASE_URL = "http://localhost:8000"
Expand All @@ -12,10 +11,7 @@ def get_api_settings(
) -> tuple[str, str]:
overmind_api_key = overmind_api_key or os.getenv("OVERMIND_API_KEY")
if not overmind_api_key:
raise OvermindError(
"No Overmind API key provided. Either pass 'overmind_api_key' parameter "
"or set OVERMIND_API_KEY environment variable."
)
return None, None

is_local = overmind_api_key.startswith(LOCAL_API_KEY_PREFIX)
default_url = LOCAL_BASE_URL if is_local else DEFAULT_BASE_URL
Expand Down
13 changes: 12 additions & 1 deletion overmind_sdk/utils/serializers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
import json


def _default_serializer(obj):
if hasattr(obj, "model_dump"):
try:
return obj.model_dump()
except Exception:
pass
if hasattr(obj, "__dict__"):
return str(obj)
return str(obj)


def serialize(obj):
return json.dumps(obj, default=lambda x: x.model_dump())
return json.dumps(obj, default=_default_serializer)
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "overmind-sdk"
version = "0.1.36"
version = "0.1.37"
description = "Python client for Overmind API"
authors = ["Overmind Ltd"]
readme = "README.md"
Expand Down
Loading