Skip to content

Commit c83c5a9

Browse files
committed
feat: json encoder, tests: refactor client, client uses json encoder
Took json encoder with inspiration to logfire, Added pytest-mock package and shortened some tests boilerplate, Custom json encoder (both on size calc and send)
1 parent 0a77962 commit c83c5a9

9 files changed

Lines changed: 480 additions & 58 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ jobs:
2828
- name: Install dependencies
2929
run: |
3030
pip install -e ".[async,starlette,flask,django,fastapi]"
31-
pip install pytest pytest-asyncio pytest-cov freezegun build
31+
pip install pytest pytest-asyncio pytest-cov pytest-mock freezegun build
3232
3333
- name: Run tests
3434
run: pytest tests/ -v --tb=short

logtide_sdk/async_client.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from logtide_sdk.client import _process_value, serialize_exception
2121
from logtide_sdk.enums import CircuitState, LogLevel
2222
from logtide_sdk.exceptions import CircuitBreakerOpenError
23+
from logtide_sdk.json_encoder import logtide_json_dumps
2324
from logtide_sdk.models import (
2425
AggregatedStatsOptions,
2526
AggregatedStatsResponse,
@@ -474,12 +475,12 @@ async def _send_logs_with_retry(self, logs: List[LogEntry]) -> None:
474475
self._metrics.circuit_breaker_trips += 1
475476

476477
async def _send_logs(self, logs: List[LogEntry]) -> None:
477-
"""POST a serialized batch to /api/v1/ingest."""
478-
payload = {"logs": [log.to_dict() for log in logs]}
478+
json_string = logtide_json_dumps({"logs": [log.to_dict() for log in logs]})
479+
479480
async with self._get_session().post(
480481
f"{self.options.api_url}/api/v1/ingest",
481482
headers=self._get_headers(),
482-
json=payload,
483+
data=json_string,
483484
) as response:
484485
response.raise_for_status()
485486

@@ -492,15 +493,15 @@ def _process_metadata_or_error(
492493
return metadata_or_error
493494
return {"exception": serialize_exception(metadata_or_error)}
494495

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

503-
raw = json.dumps(entry.to_dict(), default=lambda o: repr(o))
504+
raw = logtide_json_dumps(entry)
504505
if len(raw.encode()) > lim.max_log_size:
505506
if self.options.debug:
506507
print(f"[LogTide] Log entry too large ({len(raw)} bytes), truncating metadata")

logtide_sdk/client.py

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from logtide_sdk.circuit_breaker import CircuitBreaker
1717
from logtide_sdk.enums import CircuitState, LogLevel
1818
from logtide_sdk.exceptions import CircuitBreakerOpenError
19+
from logtide_sdk.json_encoder import logtide_json_dumps
1920
from logtide_sdk.models import (
2021
AggregatedStatsOptions,
2122
AggregatedStatsResponse,
@@ -78,13 +79,13 @@ def serialize_exception(exc: BaseException) -> Dict[str, Any]:
7879

7980
def _process_value(value: Any, path: str, lim: PayloadLimitsOptions) -> Any:
8081
"""Recursively apply payload limits to a metadata value."""
82+
if value is None:
83+
return
84+
8185
field_name = path.split(".")[-1]
8286
if field_name in lim.exclude_fields:
8387
return "[EXCLUDED]"
8488

85-
if value is None:
86-
return value
87-
8889
if isinstance(value, str):
8990
if len(value) >= 100 and _looks_like_base64(value):
9091
return "[BASE64 DATA REMOVED]"
@@ -571,7 +572,7 @@ def _get_headers(self) -> Dict[str, str]:
571572
"Content-Type": "application/json",
572573
}
573574

574-
def _send_logs_with_retry(self, logs: List[LogEntry]) -> None:
575+
def _send_logs_with_retry(self, log_entries: List[LogEntry]) -> None:
575576
"""Send a batch of logs with exponential backoff and circuit breaker."""
576577
attempt = 0
577578
delay = self.options.retry_delay_ms / 1000.0
@@ -583,21 +584,21 @@ def _send_logs_with_retry(self, logs: List[LogEntry]) -> None:
583584
if self.options.debug:
584585
print("[LogTide] Circuit breaker open, skipping send")
585586
with self._metrics_lock:
586-
self._metrics.logs_dropped += len(logs)
587+
self._metrics.logs_dropped += len(log_entries)
587588
raise CircuitBreakerOpenError("Circuit breaker is open")
588589

589590
start_time = time.time()
590-
self._send_logs(logs)
591+
self._send_logs(log_entries)
591592
latency = (time.time() - start_time) * 1000
592593

593594
self._circuit_breaker.record_success()
594595
self._update_latency(latency)
595596

596597
with self._metrics_lock:
597-
self._metrics.logs_sent += len(logs)
598+
self._metrics.logs_sent += len(log_entries)
598599

599600
if self.options.debug:
600-
print(f"[LogTide] Sent {len(logs)} logs ({latency:.2f}ms)")
601+
print(f"[LogTide] Sent {len(log_entries)} logs ({latency:.2f}ms)")
601602

602603
return
603604

@@ -617,7 +618,7 @@ def _send_logs_with_retry(self, logs: List[LogEntry]) -> None:
617618
if self.options.debug:
618619
print(f"[LogTide] Failed to send logs after {attempt} attempts: {e}")
619620
with self._metrics_lock:
620-
self._metrics.logs_dropped += len(logs)
621+
self._metrics.logs_dropped += len(log_entries)
621622
break
622623

623624
if self.options.debug:
@@ -627,7 +628,7 @@ def _send_logs_with_retry(self, logs: List[LogEntry]) -> None:
627628
# The session is gone — all remaining attempts would fail anyway.
628629
if self._closed:
629630
with self._metrics_lock:
630-
self._metrics.logs_dropped += len(logs)
631+
self._metrics.logs_dropped += len(log_entries)
631632
break
632633

633634
time.sleep(delay)
@@ -639,13 +640,14 @@ def _send_logs_with_retry(self, logs: List[LogEntry]) -> None:
639640
with self._metrics_lock:
640641
self._metrics.circuit_breaker_trips += 1
641642

642-
def _send_logs(self, logs: List[LogEntry]) -> None:
643+
def _send_logs(self, log_entries: List[LogEntry]) -> None:
643644
"""POST a batch of serialized log entries to /api/v1/ingest."""
644-
payload = {"logs": [log.to_dict() for log in logs]}
645+
json_string = logtide_json_dumps({"logs": [log.to_dict() for log in log_entries]})
646+
645647
response = self._session.post(
646648
f"{self.options.api_url}/api/v1/ingest",
647649
headers=self._get_headers(),
648-
json=payload,
650+
data=json_string,
649651
timeout=30,
650652
)
651653
response.raise_for_status()
@@ -687,7 +689,7 @@ def _apply_payload_limits(self, entry: LogEntry) -> None:
687689
entry.metadata = _process_value(entry.metadata, "root", lim)
688690

689691
# Enforce total entry size
690-
raw = json.dumps(entry.to_dict(), default=lambda o: repr(o))
692+
raw = logtide_json_dumps(entry)
691693
if len(raw.encode()) > lim.max_log_size:
692694
if self.options.debug:
693695
print(f"[LogTide] Log entry too large ({len(raw)} bytes), truncating metadata")

0 commit comments

Comments
 (0)