Skip to content
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
19 changes: 0 additions & 19 deletions sdks/go/logwell/config_test.go
Original file line number Diff line number Diff line change
@@ -1,24 +1,5 @@
package logwell

import (
"context"
"testing"
)

func validAPIKey() string {
return "lw_" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" // 32 chars after lw_
}

func validEndpoint() string {
return "http://localhost:3000"
}

// Config bounds are pinned by the TS reference SDK; one smoke case proves the
// Go mirror accepts a valid config (incl. its 100ms flush floor parity).
func TestConfigAcceptsValidConfig(t *testing.T) {
cfg, err := New(validEndpoint(), validAPIKey())
if err != nil {
t.Fatalf("New() error = %v", err)
}
defer cfg.Shutdown(context.Background())
}
17 changes: 5 additions & 12 deletions sdks/python/tests/unit/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,12 @@ class TestFlushIntervalFloorGap:
"""Config bounds are pinned by the TS reference SDK. The one Python delta:
TS/Go enforce a 100ms flush floor, Python only rejects <= 0."""

def test_flush_interval_zero_rejected(self, valid_config: LogwellConfig) -> None:
config = dict(valid_config)
config["flush_interval"] = 0.0

with pytest.raises(LogwellError) as exc_info:
validate_config(config) # type: ignore[arg-type]

assert exc_info.value.code == LogwellErrorCode.INVALID_CONFIG
assert "flush_interval" in exc_info.value.message

def test_flush_interval_negative_rejected(self, valid_config: LogwellConfig) -> None:
@pytest.mark.parametrize("flush_interval", [0.0, -1.0], ids=["zero", "negative"])
def test_flush_interval_zero_or_negative_rejected(
self, valid_config: LogwellConfig, flush_interval: float
) -> None:
config = dict(valid_config)
config["flush_interval"] = -1.0
config["flush_interval"] = flush_interval

with pytest.raises(LogwellError) as exc_info:
validate_config(config) # type: ignore[arg-type]
Expand Down
89 changes: 14 additions & 75 deletions sdks/python/tests/unit/test_queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,19 +38,6 @@ async def mock_send(batch: list[LogEntry]) -> IngestResponse:


class TestQueueOverflow:
def test_overflow_drops_oldest_entry(self) -> None:
send_batch, _ = make_send_batch_mock()
config = QueueConfig(batch_size=100, max_queue_size=3)
queue = BatchQueue(send_batch, config)

queue.add(make_log_entry("one"))
queue.add(make_log_entry("two"))
queue.add(make_log_entry("three"))
assert queue.size == 3

queue.add(make_log_entry("four"))
assert queue.size == 3

@pytest.mark.asyncio
async def test_overflow_preserves_newest_entries(self) -> None:
send_batch, captured = make_send_batch_mock()
Expand All @@ -62,6 +49,7 @@ async def test_overflow_preserves_newest_entries(self) -> None:
queue.add(make_log_entry("three"))
queue.add(make_log_entry("four")) # Drops "one"
queue.add(make_log_entry("five")) # Drops "two"
assert queue.size == 3 # Cap holds, oldest evicted

await queue.flush()

Expand All @@ -87,7 +75,10 @@ def test_overflow_calls_on_error(self) -> None:

class TestBatchQueueShutdown:
@pytest.mark.asyncio
async def test_shutdown_awaits_in_flight_flush(self) -> None:
@pytest.mark.parametrize(
"triggered_by_add", [False, True], ids=["explicit-flush", "add-triggered-flush"]
)
async def test_shutdown_awaits_in_flight_flush(self, triggered_by_add: bool) -> None:
flush_started = threading.Event()
flush_continue = threading.Event()
captured: list[list[LogEntry]] = []
Expand All @@ -99,11 +90,12 @@ async def slow_send(batch: list[LogEntry]) -> IngestResponse:
await asyncio.sleep(0.01)
return {"accepted": len(batch)}

queue = BatchQueue(MagicMock(side_effect=slow_send), QueueConfig(batch_size=100))
config = QueueConfig(batch_size=1 if triggered_by_add else 100)
queue = BatchQueue(MagicMock(side_effect=slow_send), config)
queue.add(make_log_entry("one"))
queue.add(make_log_entry("two"))

flush_task = asyncio.create_task(queue.flush())
if not triggered_by_add:
queue.add(make_log_entry("two"))
flush_task = asyncio.create_task(queue.flush())
while not flush_started.is_set():
await asyncio.sleep(0.01)

Expand All @@ -114,37 +106,11 @@ async def slow_send(batch: list[LogEntry]) -> IngestResponse:

flush_continue.set()
await shutdown_task
await flush_task

assert [e["message"] for batch in captured for e in batch] == ["one", "two"]
assert queue._queue_loop is None

@pytest.mark.asyncio
async def test_shutdown_awaits_triggered_flush(self) -> None:
flush_started = threading.Event()
flush_continue = threading.Event()
captured: list[list[LogEntry]] = []

async def slow_send(batch: list[LogEntry]) -> IngestResponse:
captured.append(batch)
flush_started.set()
while not flush_continue.is_set():
await asyncio.sleep(0.01)
return {"accepted": len(batch)}

queue = BatchQueue(MagicMock(side_effect=slow_send), QueueConfig(batch_size=1))
queue.add(make_log_entry("one"))
while not flush_started.is_set():
await asyncio.sleep(0.01)

shutdown_task = asyncio.create_task(queue.shutdown())
await asyncio.sleep(0.05)
assert not shutdown_task.done()

flush_continue.set()
await shutdown_task
if not triggered_by_add:
await flush_task

assert [e["message"] for batch in captured for e in batch] == ["one"]
expected = ["one"] if triggered_by_add else ["one", "two"]
assert [e["message"] for batch in captured for e in batch] == expected
assert queue._queue_loop is None


Expand Down Expand Up @@ -244,33 +210,6 @@ async def periodic_flush() -> None:
total_captured = sum(len(batch) for batch in captured)
assert total_captured == num_adds

def test_size_is_thread_safe(self) -> None:
send_batch, _ = make_send_batch_mock()
config = QueueConfig(batch_size=10000)
queue = BatchQueue(send_batch, config)

num_adds = 1000
sizes: list[int] = []

def add_entries() -> None:
for _ in range(num_adds):
queue.add(make_log_entry())

def read_size() -> None:
for _ in range(num_adds):
sizes.append(queue.size)

t1 = threading.Thread(target=add_entries)
t2 = threading.Thread(target=read_size)

t1.start()
t2.start()
t1.join()
t2.join()

assert queue.size == num_adds
assert all(0 <= s <= num_adds for s in sizes)


class TestBatchQueueEdgeCases:
@pytest.mark.asyncio
Expand Down
19 changes: 2 additions & 17 deletions sdks/python/tests/unit/test_transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,22 +38,6 @@ def _make_429_response(retry_after: str) -> httpx.Response:


class TestRetryAfterCap:
@pytest.mark.asyncio
async def test_retry_after_capped_at_backoff_ceiling(self) -> None:
transport = _make_transport(max_retries=1)
transport._client.post = AsyncMock( # type: ignore[attr-defined]
return_value=_make_429_response("3600")
)

with (
patch("asyncio.sleep", new=AsyncMock()) as mock_sleep,
pytest.raises(LogwellError) as exc_info,
):
await transport.send([{"level": "info", "message": "hello"}])

assert exc_info.value.code == LogwellErrorCode.RATE_LIMITED
mock_sleep.assert_awaited_once_with(0.1)

@pytest.mark.asyncio
async def test_retry_after_below_backoff_is_honored(self) -> None:
transport = _make_transport(max_retries=1)
Expand All @@ -79,10 +63,11 @@ async def test_retry_after_cap_scales_with_attempt(self) -> None:

with (
patch("asyncio.sleep", new=AsyncMock()) as mock_sleep,
pytest.raises(LogwellError),
pytest.raises(LogwellError) as exc_info,
):
await transport.send([{"level": "info", "message": "hello"}])

assert exc_info.value.code == LogwellErrorCode.RATE_LIMITED
assert mock_sleep.await_args_list == [call(0.1), call(0.2)]

@pytest.mark.asyncio
Expand Down
6 changes: 0 additions & 6 deletions sdks/typescript/tests/fixtures/configs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,6 @@ export const validConfigs = {
onFlush: () => {},
} satisfies LogwellConfig,

withService: {
apiKey: "lw_aBcDeFgHiJkLmNoPqRsTuVwXyZ123456",
endpoint: "https://test.logwell.io",
service: "my-app",
} satisfies LogwellConfig,

withSourceLocation: {
apiKey: "lw_aBcDeFgHiJkLmNoPqRsTuVwXyZ123456",
endpoint: "https://test.logwell.io",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ describe("Logwell Client", () => {

expect(client.queueSize).toBe(2);

await client.flush();
await client.shutdown();

expect(capturedLogs).toHaveLength(2);
});
Expand Down
80 changes: 0 additions & 80 deletions sdks/typescript/tests/unit/client.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,75 +19,7 @@ describe("Logwell Client - Source Location", () => {
vi.useRealTimers();
});

describe("source location disabled (default)", () => {
it("does not include sourceFile when captureSourceLocation is false", () => {
const client = new Logwell(defaultConfig);
const queueAddSpy = vi.spyOn(client["queue"], "add");

client.info("Test message");

expect(queueAddSpy).toHaveBeenCalledTimes(1);
const entry = queueAddSpy.mock.calls[0][0] as LogEntry;
expect(entry.sourceFile).toBeUndefined();
expect(entry.lineNumber).toBeUndefined();
});

it("does not include sourceFile by default", () => {
const client = new Logwell(validConfigs.minimal);
const queueAddSpy = vi.spyOn(client["queue"], "add");

client.info("Test message");

const entry = queueAddSpy.mock.calls[0][0] as LogEntry;
expect(entry.sourceFile).toBeUndefined();
expect(entry.lineNumber).toBeUndefined();
});
});

describe("source location enabled", () => {
it("includes sourceFile when captureSourceLocation is true", () => {
const client = new Logwell({
...defaultConfig,
captureSourceLocation: true,
});
const queueAddSpy = vi.spyOn(client["queue"], "add");

client.info("Test message");

const entry = queueAddSpy.mock.calls[0][0] as LogEntry;
expect(entry.sourceFile).toBeDefined();
expect(entry.sourceFile).toContain("client.unit.test.ts");
});

it("includes lineNumber when captureSourceLocation is true", () => {
const client = new Logwell({
...defaultConfig,
captureSourceLocation: true,
});
const queueAddSpy = vi.spyOn(client["queue"], "add");

client.info("Test message");

const entry = queueAddSpy.mock.calls[0][0] as LogEntry;
expect(entry.lineNumber).toBeDefined();
expect(typeof entry.lineNumber).toBe("number");
expect(entry.lineNumber).toBeGreaterThan(0);
});

it("captures correct location for info()", () => {
const client = new Logwell({
...defaultConfig,
captureSourceLocation: true,
});
const queueAddSpy = vi.spyOn(client["queue"], "add");

client.info("Test message"); // This line's number should be captured

const entry = queueAddSpy.mock.calls[0][0] as LogEntry;
expect(entry.sourceFile).not.toContain("client.ts");
expect(entry.sourceFile).toContain("client.unit.test.ts");
});

it("captures correct location for log()", () => {
const client = new Logwell({
...defaultConfig,
Expand Down Expand Up @@ -170,18 +102,6 @@ describe("Logwell Client - Source Location", () => {
expect(client.queueSize).toBe(1);
});

it("parent shutdown still flushes the shared queue", async () => {
const client = new Logwell(defaultConfig);
const child = client.child({});
const shutdownSpy = vi.spyOn(client["queue"], "shutdown").mockResolvedValue(null);

child.info("From child");
const result = await client.shutdown();

expect(shutdownSpy).toHaveBeenCalledTimes(1);
expect(result).toBeNull();
});

it("child without metadata does not create an empty metadata object", async () => {
const client = new Logwell(defaultConfig);
const child = client.child({});
Expand Down
Loading
Loading