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 .genignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pylintrc
docs/docs.json
docs/overview.mdx
src/openrouter/utils/logger.py
25 changes: 22 additions & 3 deletions src/openrouter/utils/logger.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT."""
"""Debug logging for the SDK.

Originally generated by Speakeasy, now hand-maintained and listed in `.genignore`
so a regeneration cannot reinstate the `logging.basicConfig` call below — see
`get_default_logger`.
"""

import httpx
import logging
Expand All @@ -21,7 +26,21 @@ def get_body_content(req: httpx.Request) -> str:


def get_default_logger() -> Logger:
"""Return the debug logger used when `OPENROUTER_DEBUG` is set.

Configuration is scoped to the `openrouter` logger. A library must not call
`logging.basicConfig`: that sets the level and attaches a handler on the
*root* logger, turning on DEBUG output for every library in the process and
overriding whatever logging the host application had already set up.

A `StreamHandler` is only attached when nothing up the hierarchy would emit
the records already, so an application that has configured its own handlers
keeps them, and repeated client construction does not stack duplicates.
"""
if os.getenv("OPENROUTER_DEBUG"):
logging.basicConfig(level=logging.DEBUG)
return logging.getLogger("openrouter")
logger = logging.getLogger("openrouter")
logger.setLevel(logging.DEBUG)
if not logger.hasHandlers():
logger.addHandler(logging.StreamHandler())
return logger
return NoOpLogger()
71 changes: 71 additions & 0 deletions tests/test_logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import json
import os
import subprocess
import sys
from pathlib import Path

SRC = str(Path(__file__).resolve().parents[1] / "src")

PROBE = """
import json, logging, sys
{setup}
root = logging.getLogger()
before = (root.level, len(root.handlers))

from openrouter import OpenRouter
OpenRouter(api_key="x")
OpenRouter(api_key="x") # twice: handlers must not stack

sdk = logging.getLogger("openrouter")
print(json.dumps({{
"root_before": before,
"root_after": (root.level, len(root.handlers)),
"sdk_level": sdk.level,
"sdk_handlers": len(sdk.handlers),
}}))
"""


def _probe(setup="", **env):
"""Run a fresh interpreter: logging config is process-global."""
out = subprocess.run(
[sys.executable, "-c", PROBE.format(setup=setup)],
capture_output=True,
text=True,
check=True,
env={**os.environ, "PYTHONPATH": SRC, **env},
).stdout
return json.loads(out.strip().splitlines()[-1])


def test_debug_mode_leaves_the_root_logger_alone():
r = _probe(OPENROUTER_DEBUG="1")

# basicConfig used to set root to DEBUG and attach a StreamHandler to it.
assert r["root_after"] == r["root_before"]
assert r["root_after"] == [30, 0] # WARNING, no handlers

# The SDK's own logger is what gets configured.
assert r["sdk_level"] == 10 # DEBUG
assert r["sdk_handlers"] == 1 # not 2 — repeated construction must not stack


def test_no_logging_is_touched_without_the_env_var():
r = _probe()

assert r["root_after"] == r["root_before"]
assert r["sdk_level"] == 0 # NOTSET
assert r["sdk_handlers"] == 0


def test_an_application_handler_is_not_duplicated():
r = _probe(
setup="logging.basicConfig(level=logging.INFO)",
OPENROUTER_DEBUG="1",
)

# The app configured root itself; we must not add a competing handler,
# which would print every record twice.
assert r["sdk_handlers"] == 0
assert r["sdk_level"] == 10
assert r["root_after"] == r["root_before"]