diff --git a/docs/enterprise.md b/docs/enterprise.md index e35e641f8..e8b311eb6 100644 --- a/docs/enterprise.md +++ b/docs/enterprise.md @@ -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. diff --git a/docs/guides/web-ui/issues.md b/docs/guides/web-ui/issues.md index 5168e036f..1d9c346ba 100644 --- a/docs/guides/web-ui/issues.md +++ b/docs/guides/web-ui/issues.md @@ -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 @@ -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) @@ -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: @@ -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 @@ -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 diff --git a/docs/guides/web-ui/public-traces.md b/docs/guides/web-ui/public-traces.md index 8815d6196..cc17de7f7 100644 --- a/docs/guides/web-ui/public-traces.md +++ b/docs/guides/web-ui/public-traces.md @@ -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 diff --git a/logfire/_internal/cli/__init__.py b/logfire/_internal/cli/__init__.py index 042905f88..75853a6d2 100644 --- a/logfire/_internal/cli/__init__.py +++ b/logfire/_internal/cli/__init__.py @@ -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)) diff --git a/logfire/_internal/cli/run.py b/logfire/_internal/cli/run.py index f62f61e2d..9d76a6777 100644 --- a/logfire/_internal/cli/run.py +++ b/logfire/_internal/cli/run.py @@ -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', @@ -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. @@ -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') @@ -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: diff --git a/logfire/_internal/collect_system_info.py b/logfire/_internal/collect_system_info.py index 8eff96f05..d862f3125 100644 --- a/logfire/_internal/collect_system_info.py +++ b/logfire/_internal/collect_system_info.py @@ -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. diff --git a/tests/auto_trace_samples/param_spec.py b/tests/auto_trace_samples/param_spec.py index a055f62a6..db5aae4a0 100644 --- a/tests/auto_trace_samples/param_spec.py +++ b/tests/auto_trace_samples/param_spec.py @@ -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 diff --git a/tests/test_cli.py b/tests/test_cli.py index 7199e0381..bfe63c4a4 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -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. │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────╯ """) @@ -284,8 +284,8 @@ def test_inspect( snapshot( { ('opentelemetry-instrumentation-fastapi', 'fastapi'), - ('opentelemetry-instrumentation-urllib', 'urllib'), ('opentelemetry-instrumentation-sqlite3', 'sqlite3'), + ('opentelemetry-instrumentation-urllib', 'urllib'), } ), ),