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
17 changes: 6 additions & 11 deletions docs/integrations/javascript/browser.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,30 +130,25 @@ from logfire.experimental.forwarding import forward_export_request

logfire.configure()


class CustomFrameworkResponse:
"""Replace this with your framework's actual Response class."""

def __init__(self, content: bytes, status_code: int, headers: dict[str, str]):
pass


# Example generic route handler:
def my_custom_proxy_route(request):

# 1. Extract data from your framework's request object
path = request.path # e.g. "/v1/traces"
path = request.path # e.g. "/v1/traces"
headers = request.headers
body = request.read()

# 2. Forward the request to Logfire
response = forward_export_request(
path=path,
headers=headers,
body=body
)
response = forward_export_request(path=path, headers=headers, body=body)

# 3. Return the Logfire response to the browser
return CustomFrameworkResponse(
content=response.content,
status_code=response.status_code,
headers=response.headers
)
return CustomFrameworkResponse(content=response.content, status_code=response.status_code, headers=response.headers)
```
83 changes: 77 additions & 6 deletions logfire/_internal/cli/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,35 @@
'openai-agents': 'openai_agents',
}

# Map of instrumentation packages to the Logfire extras that install them
OTEL_PACKAGE_TO_LOGFIRE_EXTRA = {
'opentelemetry-instrumentation-aiohttp-client': 'aiohttp',
'opentelemetry-instrumentation-aiohttp-server': 'aiohttp-server',
'opentelemetry-instrumentation-asyncpg': 'asyncpg',
'opentelemetry-instrumentation-aws-lambda': 'aws-lambda',
'opentelemetry-instrumentation-celery': 'celery',
'opentelemetry-instrumentation-django': 'django',
'opentelemetry-instrumentation-fastapi': 'fastapi',
'opentelemetry-instrumentation-flask': 'flask',
'opentelemetry-instrumentation-google-genai': 'google-genai',
'opentelemetry-instrumentation-httpx': 'httpx',
'opentelemetry-instrumentation-mysql': 'mysql',
'opentelemetry-instrumentation-psycopg': 'psycopg',
'opentelemetry-instrumentation-psycopg2': 'psycopg2',
'opentelemetry-instrumentation-pymongo': 'pymongo',
'opentelemetry-instrumentation-redis': 'redis',
'opentelemetry-instrumentation-requests': 'requests',
'opentelemetry-instrumentation-sqlalchemy': 'sqlalchemy',
'opentelemetry-instrumentation-sqlite3': 'sqlite3',
'opentelemetry-instrumentation-starlette': 'starlette',
'opentelemetry-instrumentation-system-metrics': 'system-metrics',
'opentelemetry-instrumentation-urllib': 'urllib',
'opentelemetry-instrumentation-asgi': 'asgi',
'opentelemetry-instrumentation-wsgi': 'wsgi',
'openinference-instrumentation-litellm': 'litellm',
'openinference-instrumentation-dspy': 'dspy',
}


@dataclass
class InstrumentationContext:
Expand Down Expand Up @@ -232,12 +261,30 @@ def instrumented_packages_text(
return text


def _get_logfire_extras(recommendations: list[tuple[str, str]]) -> tuple[list[str], list[str]]:
"""Convert OTel packages to Logfire extras where possible."""
extras: set[str] = set()
standalone: list[str] = []
for pkg_name, _ in recommendations:
extra = OTEL_PACKAGE_TO_LOGFIRE_EXTRA.get(pkg_name)
if extra:
extras.add(extra)
else:
standalone.append(pkg_name)
return sorted(extras), sorted(standalone)


def get_recommendation_texts(recommendations: set[tuple[str, str]]) -> tuple[Text, Text]:
"""Return (recommended_packages_text, install_all_text) as Text objects."""
sorted_recommendations = sorted(recommendations)
recommended_text = Text()
for pkg_name, instrumented_pkg in sorted_recommendations:
recommended_text.append(f'☐ {instrumented_pkg} (need to install {pkg_name})\n', style='grey50')
extra = OTEL_PACKAGE_TO_LOGFIRE_EXTRA.get(pkg_name)
if extra:
suggestion = f'logfire[{extra}]'
else:
suggestion = pkg_name
recommended_text.append(f'☐ {instrumented_pkg} (need to install {suggestion})\n', style='grey50')
recommended_text.append('\n')

install_text = Text()
Expand Down Expand Up @@ -312,14 +359,38 @@ def _full_install_command(recommendations: list[tuple[str, str]]) -> str:
if not recommendations:
return '' # pragma: no cover

package_names = [pkg_name for pkg_name, _ in recommendations]
logfire_extras, standalone_packages = _get_logfire_extras(recommendations)
extras_str = f'[{",".join(logfire_extras)}]' if logfire_extras else ''

import shlex

# If run via `uvx logfire run` or `uv run --with logfire logfire run`
if os.environ.get('UV') == '1':
logfire_target = f'logfire{extras_str}'

# Heuristic to detect `uvx` (uv tool run)
if '/uv/tools/' in sys.executable:
with_args = [f'--with {shlex.quote(p)}' for p in standalone_packages]
return f'uvx --from {shlex.quote(logfire_target)} {" ".join(with_args)} logfire {shlex.join(sys.argv[1:])}'.replace(
' ', ' '
).strip()
Comment on lines +374 to +376
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 str.replace(' ', ' ') corrupts shell-quoted arguments containing double spaces

The _full_install_command function uses .replace(' ', ' ') to clean up double spaces caused by empty list joins in the f-string. However, str.replace doesn't understand shell quoting, so it will also corrupt legitimate double spaces inside shell-quoted sys.argv values. For example, if sys.argv contains 'arg with spaces', shlex.join correctly preserves these as 'arg with spaces' (within shell quotes), but the subsequent .replace(' ', ' ') blindly reduces all double spaces, producing the corrupted 'arg with spaces'.

Demonstration of the issue

Input: sys.argv = ['logfire', 'run', 'arg with spaces']

Before replace: uvx --from 'logfire[req]' logfire run 'arg with spaces'
After replace: uvx --from 'logfire[req]' logfire run 'arg with spaces'

A safer approach would be to build the command as a list of parts and join once, avoiding the need for .replace().

Prompt for agents
The _full_install_command function at logfire/_internal/cli/run.py:374-376 and :382 uses .replace('  ', ' ') on the fully assembled shell command string. This is a blunt approach that does not respect shell quoting -- if any element of sys.argv contains a literal double space, shlex.join will correctly quote it, but the .replace will then corrupt the quoted content.

A safer approach is to build the command as a list of string parts (each being a logical token in the command) and then join them with a single space. For example:

  cmd_parts = ['uvx', '--from', shlex.quote(logfire_target)]
  cmd_parts.extend(with_args)   # each with_args element is already '--with quoted_pkg'
  cmd_parts.append('logfire')
  cmd_parts.extend(shlex.quote(a) for a in sys.argv[1:])
  return ' '.join(cmd_parts)

This eliminates the structural double-space problem without needing the fragile .replace() hack. Apply the same approach to the uv run path at line 382.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


# Heuristic for `uv run --with logfire`
# We assume if it's run via uv and not as a tool, it's either `uv run` or `uv run --with`.
# Providing the `--with` version is safer for ephemeral environments.
all_with = [f'--with {shlex.quote(p)}' for p in [logfire_target] + standalone_packages]
return f'uv run {" ".join(all_with)} logfire {shlex.join(sys.argv[1:])}'.replace(' ', ' ').strip()
Comment on lines +367 to +382
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 UV path generates re-run commands, not install commands

When UV=1 is set, _full_install_command at logfire/_internal/cli/run.py:368-382 generates commands like uvx --from 'logfire[extras]' logfire ... or uv run --with 'logfire[extras]' logfire ... that re-invoke logfire with the extra dependencies, rather than uv add ... / pip install ... commands that permanently install packages. This is intentionally different behavior for ephemeral UV environments, but it means the logfire inspect command (which also calls this via print_otel_summary at logfire/_internal/cli/__init__.py:123) would suggest re-running logfire inspect rather than installing packages. This is semantically different from the non-UV path and could be confusing for users running logfire inspect in a uv-managed project.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


parts: list[str] = []
if logfire_extras:
parts.append(f'logfire{extras_str}')
parts.extend(standalone_packages)
all_packages = ' '.join(shlex.quote(p) for p in parts)

# TODO(Marcelo): We should customize this. If the user uses poetry, they'd use `poetry add`.
# Something like `--install-format` with options like `requirements`, `poetry`, `uv`, `pip`.
if is_uv_installed():
return f'uv add {" ".join(package_names)}'
return f'uv add {all_packages}\n # or\n pip install {all_packages}'
else:
return f'pip install {" ".join(package_names)}' # pragma: no cover
return f'pip install {all_packages}' # pragma: no cover


def collect_instrumentation_context(exclude: set[str]) -> InstrumentationContext:
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ redis = ["opentelemetry-instrumentation-redis >= 0.42b0"]
requests = ["opentelemetry-instrumentation-requests >= 0.42b0"]
mysql = ["opentelemetry-instrumentation-mysql >= 0.42b0"]
sqlite3 = ["opentelemetry-instrumentation-sqlite3 >= 0.42b0"]
urllib = ["opentelemetry-instrumentation-urllib >= 0.42b0"]
aws-lambda = ["opentelemetry-instrumentation-aws-lambda >= 0.42b0"]
google-genai = ["opentelemetry-instrumentation-google-genai >= 0.4b0"]
litellm = ["openinference-instrumentation-litellm >= 0"]
Expand Down
83 changes: 72 additions & 11 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,27 +267,47 @@ def test_clean_default_dir_is_not_a_directory(


def test_inspect(
tmp_dir_cwd: Path, logfire_credentials: LogfireCredentials, capsys: pytest.CaptureFixture[str]
tmp_dir_cwd: Path,
logfire_credentials: LogfireCredentials,
capsys: pytest.CaptureFixture[str],
monkeypatch: pytest.MonkeyPatch,
) -> None:
os.environ['COLUMNS'] = '150'
monkeypatch.setitem(os.environ, 'COLUMNS', '150')
logfire_credentials.write_creds_file(tmp_dir_cwd / '.logfire')

# Mock distributions so the test is environment-independent
class MockDist:
def __init__(self, metadata: dict[str, str]):
self.metadata = metadata

mock_pkgs = ['django', 'fastapi', 'flask', 'httpx', 'pymongo', 'redis', 'requests', 'sqlalchemy']
monkeypatch.setattr('importlib.metadata.distributions', lambda: [MockDist({'Name': p}) for p in mock_pkgs])
monkeypatch.setattr('logfire._internal.cli.run.is_uv_installed', lambda: True)

with pytest.raises(SystemExit):
main(['inspect'])
assert capsys.readouterr().err == snapshot("""\


╭───────────────────────────────────────────────────────────────── Logfire Summary ──────────────────────────────────────────────────────────────────╮
│ │
│ ☐ botocore (need to install opentelemetry-instrumentation-botocore) │
│ ☐ jinja2 (need to install opentelemetry-instrumentation-jinja2) │
│ ☐ pymysql (need to install opentelemetry-instrumentation-pymysql) │
│ ☐ urllib (need to install opentelemetry-instrumentation-urllib) │
│ ☐ django (need to install logfire[django]) │
│ ☐ fastapi (need to install logfire[fastapi]) │
│ ☐ flask (need to install logfire[flask]) │
│ ☐ httpx (need to install logfire[httpx]) │
│ ☐ pymongo (need to install logfire[pymongo]) │
│ ☐ redis (need to install logfire[redis]) │
│ ☐ requests (need to install logfire[requests]) │
│ ☐ sqlalchemy (need to install logfire[sqlalchemy]) │
│ ☐ sqlite3 (need to install logfire[sqlite3]) │
│ ☐ urllib (need to install logfire[urllib]) │
│ │
│ │
│ To install all recommended packages at once, run: │
│ │
│ uv add opentelemetry-instrumentation-botocore opentelemetry-instrumentation-jinja2 opentelemetry-instrumentation-pymysql │
│ opentelemetry-instrumentation-urllib │
│ uv add 'logfire[django,fastapi,flask,httpx,pymongo,redis,requests,sqlalchemy,sqlite3,urllib]' │
│ # or │
│ pip install 'logfire[django,fastapi,flask,httpx,pymongo,redis,requests,sqlalchemy,sqlite3,urllib]' │
│ │
│ ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── │
│ │
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Expand Down Expand Up @@ -1625,9 +1645,50 @@ def test_instrumented_packages_text_basic():
def test_get_recommendation_texts():
recs = {('opentelemetry-instrumentation-foo', 'foo'), ('opentelemetry-instrumentation-bar', 'bar')}
recommended, install = get_recommendation_texts(recs)
assert 'uv add opentelemetry-instrumentation-bar opentelemetry-instrumentation-foo' in install
assert 'need to install opentelemetry-instrumentation-bar' in recommended
assert 'need to install opentelemetry-instrumentation-foo' in recommended
assert 'pip install opentelemetry-instrumentation-bar opentelemetry-instrumentation-foo' in str(install)
assert 'need to install opentelemetry-instrumentation-bar' in str(recommended)
assert 'need to install opentelemetry-instrumentation-foo' in str(recommended)
Comment on lines 1645 to +1650
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Tests fail in CI because UV=1 environment variable is not cleared

CI runs tests via uv run --no-sync coverage run -m pytest (.github/workflows/main.yml:129), which sets UV=1 in the environment. The _full_install_command function at logfire/_internal/cli/run.py:368 checks os.environ.get('UV') == '1' and takes a completely different code path (producing uv run --with ... commands) before ever reaching the is_uv_installed() check. Four tests don't isolate this variable:

  1. test_inspect (line 285 mocks is_uv_installed but snapshot on line 308 expects uv add ...)
  2. test_get_recommendation_texts (line 1648 asserts pip install in output)
  3. test_get_recommendation_texts_with_extras (line 1659 asserts pip install in output)
  4. test_get_recommendation_texts_uv (line 1665 mocks is_uv_installed but line 1668 asserts uv add in output)

With UV=1, all four assertions fail because the code returns uv run --with ... instead of the expected uv add ... or pip install ... output.

Prompt for agents
Four tests in tests/test_cli.py are missing UV environment variable isolation and will fail in CI (where UV=1 is set by uv run).

The root cause is that _full_install_command in logfire/_internal/cli/run.py:368 checks os.environ.get('UV') == '1' before checking is_uv_installed(). When UV=1 is set (as it is in CI via uv run), a completely different code path is taken, producing uv run --with output instead of uv add or pip install.

Affected tests:
1. test_get_recommendation_texts (line 1645) - needs monkeypatch parameter added, then monkeypatch.delenv('UV', raising=False)
2. test_get_recommendation_texts_with_extras (line 1653) - same fix needed
3. test_get_recommendation_texts_uv (line 1664) - already has monkeypatch, just needs monkeypatch.delenv('UV', raising=False)
4. test_inspect (line 269) - already has monkeypatch, needs monkeypatch.delenv('UV', raising=False)

For tests 1 and 2, the function signature needs to be updated to accept monkeypatch: pytest.MonkeyPatch as a parameter. All four tests need monkeypatch.delenv('UV', raising=False) added before calling get_recommendation_texts or main.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines 1645 to +1650
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 UV=1 environment variable in tests not isolated

Several new tests (test_get_recommendation_texts, test_get_recommendation_texts_with_extras) do not monkeypatch the UV environment variable. If these tests happen to run in a CI environment where UV=1 is set (e.g., if the test runner itself is invoked via uv run), the _full_install_command at logfire/_internal/cli/run.py:368 would take the UV-specific path instead of the is_uv_installed() path, causing unexpected command output and test failures. The tests test_get_recommendation_texts_uv, test_get_recommendation_texts_uv_run, and test_get_recommendation_texts_uvx correctly set/control the UV env var, but the base tests don't unset it.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.



def test_get_recommendation_texts_with_extras():
recs = {
('opentelemetry-instrumentation-requests', 'requests'),
('opentelemetry-instrumentation-sqlite3', 'sqlite3'),
}
recommended, install = get_recommendation_texts(recs)
assert "pip install 'logfire[requests,sqlite3]'" in str(install)
assert 'need to install logfire[requests]' in str(recommended)
assert 'need to install logfire[sqlite3]' in str(recommended)


def test_get_recommendation_texts_uv(monkeypatch: pytest.MonkeyPatch):
monkeypatch.setattr('logfire._internal.cli.run.is_uv_installed', lambda: True)
recs = {('opentelemetry-instrumentation-requests', 'requests')}
_, install = get_recommendation_texts(recs)
assert "uv add 'logfire[requests]'\n # or\n pip install 'logfire[requests]'" in str(install)


def test_get_recommendation_texts_uv_run(monkeypatch: pytest.MonkeyPatch):
monkeypatch.setenv('UV', '1')
monkeypatch.setattr('sys.executable', '/path/to/venv/bin/python')
recs = {
('opentelemetry-instrumentation-requests', 'requests'),
('opentelemetry-instrumentation-jinja2', 'jinja2'),
}
monkeypatch.setattr(sys, 'argv', ['logfire', 'run', 'myapp.py'])
_, install = get_recommendation_texts(recs)
assert "uv run --with 'logfire[requests]' --with opentelemetry-instrumentation-jinja2 logfire run myapp.py" in str(
install
)


def test_get_recommendation_texts_uvx(monkeypatch: pytest.MonkeyPatch):
monkeypatch.setenv('UV', '1')
monkeypatch.setattr('sys.executable', '/Users/user/.cache/uv/tools/logfire/bin/python')
recs = {('opentelemetry-instrumentation-requests', 'requests')}
monkeypatch.setattr(sys, 'argv', ['logfire', 'run', 'myapp.py'])
_, install = get_recommendation_texts(recs)
assert "uvx --from 'logfire[requests]' logfire run myapp.py" in str(install)


def test_instrument_packages_openai() -> None:
Expand Down
Loading