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
2 changes: 1 addition & 1 deletion docs/enterprise.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Organizations requiring:

#### Single Sign-on (SSO) with OIDC Providers

Logfire supports SSO authentication with [OIDC (OpenID Connect)](https://openid.net/developers/how-connect-works/) Providers, allowing your team to access the platform using their existing corporate credentials.
Logfire supports SSO authentication with [OIDC (OpenID Connect)](https://openid.net/developers/how-connect-works/) Providers, allowing your team to access the platform using their existing corporate credentials.


When a user logs in, your OIDC provider verifies their identity and securely shares authorized user information with our platform—eliminating the need for separate passwords while maintaining enterprise-grade security.
Expand Down
38 changes: 19 additions & 19 deletions docs/guides/web-ui/issues.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,18 +52,18 @@ For each issue, you can:

## Turn on Issue Alerts

By default, Issues are only visible in the Logfire web interface. To be notified when Issues occur in your other tools, you can select external channels.
By default, Issues are only visible in the Logfire web interface. To be notified when Issues occur in your other tools, you can select external channels.

### Select an Alert Channel

You can:
You can:

1. Create a new channel - Add a webhook URL for services like Slack, Discord, Microsoft Teams, or any service that accepts webhooks
2. Use an existing channel - Select from previously configured notification channels.
1. Create a new channel - Add a webhook URL for services like Slack, Discord, Microsoft Teams, or any service that accepts webhooks
2. Use an existing channel - Select from previously configured notification channels.

### Create a new channel:

1. Go to **Settings** on the **Issues** page
1. Go to **Settings** on the **Issues** page
2. Click **Add another channel**
3. Enter a channel name and webhook URL
4. Test the channel before saving
Expand All @@ -77,16 +77,16 @@ Notifications are sent when new issues open and when resolved issues reopen. Ign

### Bulk Actions

To select multiple issues at once, hold down `shift` or `cmd` (macOS) / `ctrl` (windows).
To select multiple issues at once, hold down `shift` or `cmd` (macOS) / `ctrl` (windows).

After selecting more than one issue you can:

- Ignore all selected issues
- Resolve all selected issues
- Ignore all selected issues
- Resolve all selected issues

## Fix with AI

Use this feature to debug your exceptions using your local LLM coding tool plus the Logfire MCP server.
Use this feature to debug your exceptions using your local LLM coding tool plus the Logfire MCP server.

![Fix with AI button](../../images/guide/browser-issues-fix-with-ai.png)

Expand All @@ -101,7 +101,7 @@ Want us to integrate more AI Code assistants? [Let us know](https://logfire.pyda

## Sorting and Searching

Search for exception message text using the Search field.
Search for exception message text using the Search field.


Use the sort options to find specific issues:
Expand All @@ -110,7 +110,7 @@ _Click twice on any sort to reverse the order_
- **Sort by Last Seen** - most <> least recent issues
- **Sort by First Seen** - youngest <> oldest issues issues
- **Sort by Message** - sort exception message alphabetically (A-Z) / (Z-A)
- **Sort by Hits** - most <> least hits
- **Sort by Hits** - most <> least hits
- **Sort by Exception** - sort exception alphabetically (A-Z) / (Z-A)

## Best Practices
Expand All @@ -124,15 +124,15 @@ _Click twice on any sort to reverse the order_

### When to Resolve vs Ignore

**Resolve** when:
- You've fixed the underlying problem
- You want to be notified if the issue returns
- The exception indicates a real bug or problem
**Resolve** when:
- You've fixed the underlying problem
- You want to be notified if the issue returns
- The exception indicates a real bug or problem

**Ignore** when:
- The exception is expected behavior (e.g., user input validation errors)
- Third-party service errors you can't control
- Legacy code issues you've decided not to fix
**Ignore** when:
- The exception is expected behavior (e.g., user input validation errors)
- Third-party service errors you can't control
- Legacy code issues you've decided not to fix

### Cleanup

Expand Down
2 changes: 1 addition & 1 deletion docs/guides/web-ui/public-traces.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ When you create a public trace link, anyone with access to the link can view the

![Private button](../../images/public-traces/private-button.png)

4. Configure the link expiration
4. Configure the link expiration
5. If you want the link to navigate to the inner span you've selected, use **This span selected**, otherwise uncheck this and the public link will point to root span of the trace.
6. Click **Create** to generate the shareable link

Expand Down
4 changes: 2 additions & 2 deletions logfire/_internal/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,8 +222,8 @@ def logfire_info() -> str:

for dist in importlib_metadata.distributions():
metadata = dist.metadata
name = metadata.get('Name', '')
version = metadata.get('Version', 'UNKNOWN')
name = getattr(metadata, 'Name', '') or ''
version = getattr(metadata, 'Version', 'UNKNOWN') or 'UNKNOWN'
index = package_names.get(name)
if index is not None:
related_packages.append((index, name, version))
Expand Down
107 changes: 97 additions & 10 deletions logfire/_internal/cli/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,32 @@

STANDARD_LIBRARY_PACKAGES = {'urllib', 'sqlite3'}

OTEL_TO_LOGFIRE_GROUPS = {
'opentelemetry-instrumentation-requests': 'requests',
'opentelemetry-instrumentation-sqlite3': 'sqlite3',
'opentelemetry-instrumentation-urllib': 'urllib',
'opentelemetry-instrumentation-fastapi': 'fastapi',
'opentelemetry-instrumentation-flask': 'flask',
'opentelemetry-instrumentation-django': 'django',
'opentelemetry-instrumentation-starlette': 'starlette',
'opentelemetry-instrumentation-httpx': 'httpx',
'opentelemetry-instrumentation-sqlalchemy': 'sqlalchemy',
'opentelemetry-instrumentation-asyncpg': 'asyncpg',
'opentelemetry-instrumentation-psycopg': 'psycopg',
'opentelemetry-instrumentation-psycopg2': 'psycopg2',
'opentelemetry-instrumentation-pymongo': 'pymongo',
'opentelemetry-instrumentation-redis': 'redis',
'opentelemetry-instrumentation-celery': 'celery',
'opentelemetry-instrumentation-mysql': 'mysql',
'opentelemetry-instrumentation-aws-lambda': 'aws-lambda',
'opentelemetry-instrumentation-google-genai': 'google-genai',
'opentelemetry-instrumentation-aiohttp-client': 'aiohttp-client',
'opentelemetry-instrumentation-aiohttp-server': 'aiohttp-server',
'opentelemetry-instrumentation-asgi': 'asgi',
'opentelemetry-instrumentation-wsgi': 'wsgi',
'opentelemetry-instrumentation-system-metrics': 'system-metrics',
}

# Map of instrumentation packages to the packages they instrument
OTEL_INSTRUMENTATION_MAP = {
'opentelemetry-instrumentation-aio_pika': 'aio_pika',
Expand Down Expand Up @@ -145,6 +171,26 @@ def is_uv_installed() -> bool:
return shutil.which('uv') is not None


def detect_execution_context() -> str:
"""Detect how logfire was executed."""
if 'UVX' in os.environ or 'UVX_PACKAGE' in os.environ:
return 'uvx'
if 'UV_RUN' in os.environ or 'UV_PROJECT' in os.environ:
return 'uv_run'

pyproject_path = os.path.join(os.getcwd(), 'pyproject.toml')
if os.path.exists(pyproject_path):
try:
with open(pyproject_path) as f:
content = f.read()
if '[tool.uv]' in content or '[dependency-groups]' in content:
return 'uv_project'
except OSError:
pass

return 'regular'


def instrument_packages(installed_otel_packages: set[str], instrument_pkg_map: dict[str, str]) -> list[str]:
"""Call every `logfire.instrument_x()` we can based on what's installed.

Expand Down Expand Up @@ -236,12 +282,27 @@ def get_recommendation_texts(recommendations: set[tuple[str, str]]) -> tuple[Tex
"""Return (recommended_packages_text, install_all_text) as Text objects."""
sorted_recommendations = sorted(recommendations)
recommended_text = Text()

groups: set[str] = set()
remaining: list[tuple[str, str]] = []

for pkg_name, instrumented_pkg in sorted_recommendations:
group = OTEL_TO_LOGFIRE_GROUPS.get(pkg_name)
if group:
groups.add(group)
else:
remaining.append((pkg_name, instrumented_pkg))

for group in sorted(groups):
recommended_text.append(f'☐ {group} (need to install logfire[{group}])\n', style='grey50')

for pkg_name, instrumented_pkg in remaining:
recommended_text.append(f'☐ {instrumented_pkg} (need to install {pkg_name})\n', style='grey50')

recommended_text.append('\n')

install_text = Text()
if recommendations: # pragma: no branch
if recommendations:
install_text.append('To install all recommended packages at once, run:\n\n')
install_text.append(_full_install_command(sorted_recommendations), style='bold')
install_text.append('\n')
Expand Down Expand Up @@ -308,18 +369,44 @@ def installed_packages() -> set[str]:


def _full_install_command(recommendations: list[tuple[str, str]]) -> str:
"""Generate a command to install all recommended packages at once."""
"""Build an install command for all recommended packages."""
if not recommendations:
return '' # pragma: no cover
return ''

package_names = [pkg_name for pkg_name, _ in recommendations]
logfire_groups: set[str] = set()
extra_packages: list[str] = []

# 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)}'
else:
return f'pip install {" ".join(package_names)}' # pragma: no cover
for pkg_name, _ in recommendations:
group = OTEL_TO_LOGFIRE_GROUPS.get(pkg_name)
if group:
logfire_groups.add(group)
else:
extra_packages.append(pkg_name)

context = detect_execution_context()
groups_str = ','.join(sorted(logfire_groups)) if logfire_groups else None
extras_str = ' '.join(extra_packages) if extra_packages else None

if context == 'uvx':
return f"uvx --from 'logfire[{groups_str}]' logfire run" if groups_str else "uvx --from 'logfire' logfire run"

if context == 'uv_run':
return (
f"uv run --with 'logfire[{groups_str}]' logfire run" if groups_str else 'uv run --with logfire logfire run'
)

if context == 'uv_project' and is_uv_installed():
if groups_str and extras_str:
return f"uv add 'logfire[{groups_str}]' {extras_str}"
if groups_str:
return f"uv add 'logfire[{groups_str}]'"
return f'uv add logfire {extras_str}'

if groups_str and extras_str:
return f"pip install 'logfire[{groups_str}]' {extras_str}"
if groups_str:
return f"pip install 'logfire[{groups_str}]'"
return f'pip install logfire {extras_str}'


def collect_instrumentation_context(exclude: set[str]) -> InstrumentationContext:
Expand Down
10 changes: 5 additions & 5 deletions logfire/_internal/collect_system_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ def collect_package_info() -> dict[str, str]:
distributions = list(metadata.distributions())
try:
metas = [dist.metadata for dist in distributions]
pairs = [(meta['Name'], meta.get('Version', 'UNKNOWN')) for meta in metas if meta.get('Name')]
pairs = [
(getattr(meta, 'Name', '') or '', getattr(meta, 'Version', 'UNKNOWN') or 'UNKNOWN')
for meta in metas
if getattr(meta, 'Name', None)
]
except Exception: # pragma: no cover
# Just in case `dist.metadata['Name']` stops working but `dist.name` still works,
# not that this is expected.
# Currently this is about 2x slower because `dist.name` and `dist.version` each call `dist.metadata`,
# which reads and parses a file and is not cached.
pairs = [(dist.name, dist.version) for dist in distributions]
except Exception: # pragma: no cover
# Don't crash for this.
Expand Down
4 changes: 3 additions & 1 deletion tests/auto_trace_samples/param_spec.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
def check_param_spec_syntax[**P](*args: P.args, **kwargs: P.kwargs):
from typing import Any

def check_param_spec_syntax(*args: Any, **kwargs: Any) -> tuple[tuple[Any, ...], dict[str, Any]]:
return args, kwargs
38 changes: 19 additions & 19 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,24 +249,24 @@ def test_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)
│ To install all recommended packages at once, run:
uv add opentelemetry-instrumentation-botocore opentelemetry-instrumentation-jinja2 opentelemetry-instrumentation-pymysql
│ opentelemetry-instrumentation-urllib
│ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
│ To hide this summary box, use: logfire run --no-summary.
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
╭────────────────────────────── Logfire Summary ───────────────────────────────╮
│ │
│ ☐ urllib (need to install logfire[urllib])
│ ☐ botocore (need to install opentelemetry-instrumentation-botocore)
│ ☐ jinja2 (need to install opentelemetry-instrumentation-jinja2)
│ ☐ pymysql (need to install opentelemetry-instrumentation-pymysql)
│ │
│ │
│ To install all recommended packages at once, run: │
│ │
pip install 'logfire[urllib]' opentelemetry-instrumentation-botocore
│ opentelemetry-instrumentation-jinja2 opentelemetry-instrumentation-pymysql
│ │
│ ────────────────────────────────────────────────────────────────────────── │
│ │
│ To hide this summary box, use: logfire run --no-summary. │
│ │
╰──────────────────────────────────────────────────────────────────────────────╯

""")

Expand All @@ -284,8 +284,8 @@ def test_inspect(
snapshot(
{
('opentelemetry-instrumentation-fastapi', 'fastapi'),
('opentelemetry-instrumentation-urllib', 'urllib'),
('opentelemetry-instrumentation-sqlite3', 'sqlite3'),
('opentelemetry-instrumentation-urllib', 'urllib'),
}
),
),
Expand Down
Loading