Skip to content

fix(client): set AS_ROOT attribute to False in Langfuse remote span context #1266

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

romanglo
Copy link

@romanglo romanglo commented Jul 21, 2025

When using multi-level span with SDK v3, with remote parent span (pass langfuse_trace_id and langfuse_parent_observation_id), the trace input and output reflects the input and output of the latest executed span (the inner one). This creates opposite default behavior to what is written in the documentation and how it behaves if they are not passed:

Trace Input/Output Behavior

In v3, trace input and output are automatically set from the root observation (first span/generation) by default. This differs from v2 where integrations could set trace-level inputs/outputs directly.

I reproduced the issue with the following code on version 3.2.1:

import uuid

from typing import Any

from dotenv import load_dotenv
from langfuse import __version__ as langfuse_version, get_client, observe


print("langfuse_version: ", langfuse_version)

load_dotenv()

langfuse_client = get_client()


WITH_REMOTE_TRACING = True
SUFFIX = "remote" if WITH_REMOTE_TRACING else "no-remote"


def get_tracing_params() -> dict[str, Any]:
    if WITH_REMOTE_TRACING:
        trace_id = langfuse_client.get_current_trace_id()
        parent_observation_id = langfuse_client.get_current_observation_id()
        return {"langfuse_trace_id": trace_id, "langfuse_parent_observation_id": parent_observation_id}
    else:
        return {}


@observe(name=f"root-{SUFFIX}")
async def root(called_by: str):
    id_ = str(uuid.uuid4())
    res = await child("root", **get_tracing_params())
    output = {"output-override": "roman", "id": id_}
    # langfuse_client.update_current_trace(output=output, metadata={"test": "test-tom"})
    return res | {"root_workflow": "root_workflow", "id": id_}


@observe(name=f"child-{SUFFIX}")
async def child(called_by: str):
    return {"child_workflow": "child_workflow"} | await activity("child", **get_tracing_params())


@observe(name=f"activity-{SUFFIX}")
async def activity(called_by: str):
    return {"cool_activity": "cool_activity"}


async def main():
    """Execute the workflows with proper context"""
    try:
        # Wait for the workflow to complete
        result = await root("main")
        print("✅ completed successfully!")
        print(f"📊 Result: {result}")

        return result

    except Exception as e:
        print(f"❌ execution failed: {e}")
        raise


if __name__ == "__main__":
    import asyncio

    asyncio.run(main())

Following a trace and an activity examples with the issue:
before trace
before span

And here a trace example after the fix:
after trace


Important

Sets AS_ROOT to False for remote parent spans in client.py, fixing trace input/output behavior for multi-level spans.

  • Behavior:
    • Sets LangfuseOtelSpanAttributes.AS_ROOT to False in _create_span_with_parent_context() in client.py for remote parent spans.
    • Fixes trace input/output behavior for multi-level spans with remote parent spans, aligning with documented default behavior.

This description was created by Ellipsis for d9eaf51. You can customize this summary. It will automatically update as commits are pushed.

Disclaimer: Experimental PR review

Greptile Summary

This PR fixes a critical behavioral inconsistency in the Python SDK v3 related to trace input/output handling when using remote parent spans. The change modifies how the AS_ROOT attribute is set in the OpenTelemetry span context.

Specifically, when using remote parent spans (by passing langfuse_trace_id and langfuse_parent_observation_id), the trace was incorrectly using the input/output from the most recently executed (innermost) span instead of the root span. This behavior contradicted both the documentation and the intended v3 API design, which specifies that trace input/output should be automatically set from the root observation.

The fix involves setting AS_ROOT to False in the Langfuse remote span context, ensuring consistent behavior between remote and local span tracking.

Confidence score: 5/5

  1. This PR is very safe to merge as it fixes incorrect behavior to match documented specifications
  2. The fix is minimal, well-tested, and corrects a clear logical inconsistency in span attribute setting
  3. Key files needing attention:
    • langfuse/_client/client.py - verify the AS_ROOT attribute setting logic

@CLAassistant
Copy link

CLAassistant commented Jul 21, 2025

CLA assistant check
All committers have signed the CLA.

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

1 file reviewed, no comments

Edit Code Review Bot Settings | Greptile

@hassiebp hassiebp self-assigned this Jul 21, 2025
@hassiebp hassiebp self-requested a review July 28, 2025 12:51
@hassiebp hassiebp removed their assignment Jul 28, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants