Skip to content
Open
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
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.10
55 changes: 28 additions & 27 deletions logtide_sdk/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
"""LogTide SDK - Official Python SDK for LogTide."""

from logtide_sdk.client import LogTideClient, serialize_exception
from logtide_sdk.client import LogTideClient
from logtide_sdk.dsn import DsnParseError, DsnParts, parse_dsn
from logtide_sdk.enums import CircuitState, LogLevel
from logtide_sdk.exceptions import BufferFullError, CircuitBreakerOpenError, LogTideError
from logtide_sdk.handler import LogTideHandler
from logtide_sdk.models import (
AggregatedStatsOptions,
AggregatedStatsResponse,
ClientMetrics,
ClientOptions,
LogEntry,
LogsResponse,
PayloadLimitsOptions,
QueryOptions,
)
from logtide_sdk.scope import (
Breadcrumb,
Scope,
Expand All @@ -14,28 +27,16 @@
set_tag,
set_user,
)
from logtide_sdk.serialization import serialize_exception
from logtide_sdk.tracecontext import (
TraceContext,
format_traceparent,
generate_span_id,
inject_traceparent,
generate_trace_id,
inject_traceparent,
parse_traceparent,
resolve_trace_id,
)
from logtide_sdk.enums import CircuitState, LogLevel
from logtide_sdk.exceptions import BufferFullError, CircuitBreakerOpenError, LogTideError
from logtide_sdk.handler import LogTideHandler
from logtide_sdk.models import (
AggregatedStatsOptions,
AggregatedStatsResponse,
ClientMetrics,
ClientOptions,
LogEntry,
LogsResponse,
PayloadLimitsOptions,
QueryOptions,
)

_has_async = False
try:
Expand All @@ -49,41 +50,41 @@

__all__ = [
"AggregatedStatsOptions",
"Breadcrumb",
"Scope",
"User",
"add_breadcrumb",
"get_current_scope",
"push_scope",
"set_extra",
"set_session_id",
"set_tag",
"set_user",
"AggregatedStatsResponse",
"Breadcrumb",
"BufferFullError",
"CircuitBreakerOpenError",
"CircuitState",
"ClientMetrics",
"ClientOptions",
"DsnParseError",
"DsnParts",
"LogEntry",
"LogLevel",
"LogTideClient",
"LogTideError",
"LogTideHandler",
"LogsResponse",
"PayloadLimitsOptions",
"DsnParseError",
"DsnParts",
"QueryOptions",
"Scope",
"TraceContext",
"User",
"add_breadcrumb",
"format_traceparent",
"generate_span_id",
"generate_trace_id",
"get_current_scope",
"inject_traceparent",
"parse_dsn",
"parse_traceparent",
"push_scope",
"resolve_trace_id",
"serialize_exception",
"set_extra",
"set_session_id",
"set_tag",
"set_user",
]

if _has_async:
Expand Down
65 changes: 65 additions & 0 deletions logtide_sdk/_base_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from typing import Any

from logtide_sdk.json_encoder import logtide_json_dumps
from logtide_sdk.models import ClientOptions, LogEntry, PayloadLimitsOptions
from logtide_sdk.payload_limits import apply_payload_limits
from logtide_sdk.serialization import serialize_exception


class BaseClient:
"""
Base LogTide SDK Client. (more like helper to be DRY for now)
"""

def __init__(self, options: ClientOptions) -> None:
self.options = options
self._payload_limits = options.payload_limits or PayloadLimitsOptions()

def set_trace_id(self, trace_id: str | None) -> None:
"""Set trace ID for subsequent logs."""
self._trace_id = trace_id

def get_trace_id(self) -> str | None:
"""Return the current trace ID."""
return self._trace_id

def _get_headers(self) -> dict[str, str]:
"""Return HTTP headers for all API requests."""
assert self.options.api_key, "Get headers somehow with unset API Key"

return {
"X-API-Key": self.options.api_key,
"Content-Type": "application/json",
}

def _apply_payload_limits(self, entry: LogEntry) -> None:
"""Enforce payload limits on entry.metadata in-place."""
if not entry.metadata:
return

lim = self._payload_limits
entry.metadata = apply_payload_limits(entry.metadata, "root", lim)

raw = logtide_json_dumps(entry)
if len(raw.encode()) > lim.max_log_size:
if self.options.debug:
# TODO: replace all prints with logging
print(f"[LogTide] Log entry too large ({len(raw)} bytes), truncating metadata")

entry.metadata = {
"_truncated": True,
"_original_size": len(raw.encode()),
}

def _process_metadata_or_error(
self, metadata_or_error: dict[str, Any] | Exception | None
) -> dict[str, Any]:
"""
Normalise the metadata_or_error parameter used by error() and critical().
Exceptions are serialized to a structured 'exception' key.
"""
if metadata_or_error is None:
return {}
if isinstance(metadata_or_error, dict):
return metadata_or_error
return {"exception": serialize_exception(metadata_or_error)}
64 changes: 13 additions & 51 deletions logtide_sdk/async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,27 @@
"Install it with: pip install logtide-sdk[async]"
)

from logtide_sdk._base_client import BaseClient
from logtide_sdk._retry import classify_failure
from logtide_sdk._version import SDK_NAME, VERSION
from logtide_sdk.circuit_breaker import CircuitBreaker
from logtide_sdk.client import _process_value, serialize_exception
from logtide_sdk.enums import CircuitState, LogLevel
from logtide_sdk.exceptions import CircuitBreakerOpenError
from logtide_sdk.json_encoder import logtide_json_dumps
from logtide_sdk._retry import classify_failure
from logtide_sdk._version import SDK_NAME, VERSION
from logtide_sdk.scope import get_current_scope
from logtide_sdk.tracecontext import active_trace_context, generate_trace_id
from logtide_sdk.models import (
AggregatedStatsOptions,
AggregatedStatsResponse,
ClientMetrics,
ClientOptions,
LogEntry,
LogsResponse,
PayloadLimitsOptions,
QueryOptions,
)
from logtide_sdk.scope import get_current_scope
from logtide_sdk.tracecontext import active_trace_context, generate_trace_id


class AsyncLogTideClient:
class AsyncLogTideClient(BaseClient):
"""
Async LogTide SDK Client.

Expand All @@ -65,7 +64,8 @@ def __init__(self, options: ClientOptions) -> None:
Args:
options: Client configuration options (same as LogTideClient)
"""
self.options = options
super().__init__(options=options)

self._buffer: list[LogEntry] = []
self._trace_id: str | None = None
self._buffer_lock: asyncio.Lock | None = None # created lazily in first async call
Expand All @@ -76,7 +76,6 @@ def __init__(self, options: ClientOptions) -> None:
reset_timeout_ms=options.circuit_breaker_reset_ms,
)
self._latency_window: list[float] = []
self._payload_limits = options.payload_limits or PayloadLimitsOptions()
self._session: aiohttp.ClientSession | None = None
self._flush_task: Any | None = None # asyncio.Task[None]
self._closed = False
Expand Down Expand Up @@ -130,18 +129,6 @@ async def close(self) -> None:
if self.options.debug:
print("[LogTide] Async client closed")

# -----------------------------------------------------------------------
# Trace ID helpers
# -----------------------------------------------------------------------

def set_trace_id(self, trace_id: str | None) -> None:
"""Set trace ID for subsequent logs."""
self._trace_id = trace_id

def get_trace_id(self) -> str | None:
"""Return the current trace ID."""
return self._trace_id

# -----------------------------------------------------------------------
# Logging methods
# -----------------------------------------------------------------------
Expand All @@ -153,7 +140,10 @@ async def log(self, entry: LogEntry) -> None:
Args:
entry: Pre-built log entry
"""
if self._closed:
if self._closed or self.options.local_mode is True:
return

if self.options.local_mode == "if_unset_api_key" and not self.options.api_key:
return

if entry.metadata is None:
Expand Down Expand Up @@ -507,12 +497,6 @@ def _get_session(self) -> aiohttp.ClientSession:
self._session = aiohttp.ClientSession()
return self._session

def _get_headers(self) -> dict[str, str]:
return {
"X-API-Key": self.options.api_key,
"Content-Type": "application/json",
}

async def _flush_loop(self) -> None:
"""Background coroutine: flush on a fixed interval until closed."""
interval = self.options.flush_interval / 1000.0
Expand Down Expand Up @@ -602,29 +586,7 @@ async def _send_logs(self, logs: list[LogEntry]) -> None:
) as response:
response.raise_for_status()

def _process_metadata_or_error(
self, metadata_or_error: dict[str, Any] | Exception | None
) -> dict[str, Any]:
if metadata_or_error is None:
return {}
if isinstance(metadata_or_error, dict):
return metadata_or_error
return {"exception": serialize_exception(metadata_or_error)}

# NOTE: this is twice. (both in async and regular clients, maybe need base class)
def _apply_payload_limits(self, entry: LogEntry) -> None:
"""Enforce payload limits on entry.metadata in-place."""
if not entry.metadata:
return
lim = self._payload_limits
entry.metadata = _process_value(entry.metadata, "root", lim)

raw = logtide_json_dumps(entry)
if len(raw.encode()) > lim.max_log_size:
if self.options.debug:
print(f"[LogTide] Log entry too large ({len(raw)} bytes), truncating metadata")
entry.metadata = {"_truncated": True, "_original_size": len(raw.encode())}

# TODO: refactor update latency code repeat
def _update_latency(self, latency: float) -> None:
with self._metrics_lock:
self._latency_window.append(latency)
Expand Down
Loading
Loading