Files
AI/참고/guardrails-main/guardrails/telemetry/runner_tracing.py
2026-05-12 19:40:31 +09:00

338 lines
12 KiB
Python

import json
from functools import wraps
from typing import (
AsyncIterator,
Awaitable,
Callable,
Iterator,
Optional,
)
try:
from openinference.semconv.trace import SpanAttributes # type: ignore
except ImportError:
SpanAttributes = None
from opentelemetry import context, trace
from opentelemetry.trace import StatusCode, Span
from guardrails.classes.history.iteration import Iteration
from guardrails.classes.llm.llm_response import LLMResponse
from guardrails.settings import settings
from guardrails.classes.output_type import OT
from guardrails.classes.validation_outcome import ValidationOutcome
from guardrails.stores.context import get_guard_name
from guardrails.telemetry.common import (
add_user_attributes,
serialize,
recursive_key_operation,
redact,
)
from guardrails.utils.safe_get import safe_get
from guardrails.version import GUARDRAILS_VERSION
import sys
if sys.version_info.minor < 10:
from guardrails.utils.polyfills import anext
#########################################
### START Runner.step Instrumentation ###
#########################################
# TODO: Track input arguments and outputs explicitly as named attributes
def add_step_attributes(
step_span: Span, response: Optional[Iteration], *args, **kwargs
):
step_number = safe_get(args, 1, kwargs.get("index", 0))
guard_name = get_guard_name()
step_span.set_attribute("guardrails.version", GUARDRAILS_VERSION)
step_span.set_attribute("type", "guardrails/guard/step")
step_span.set_attribute("guard.name", guard_name)
step_span.set_attribute("step.index", step_number)
ser_args = [serialize(arg) for arg in args]
ser_kwargs = {k: serialize(v) for k, v in kwargs.items()}
inputs = {
"args": [sarg for sarg in ser_args if sarg is not None],
"kwargs": {k: v for k, v in ser_kwargs.items() if v is not None},
}
for k in inputs:
inputs[k] = recursive_key_operation(inputs[k], redact)
step_span.set_attribute("input.mime_type", "application/json")
step_span.set_attribute("input.value", json.dumps(inputs))
ser_output = serialize(response)
if ser_output:
step_span.set_attribute("output.mime_type", "application/json")
step_span.set_attribute(
"output.value",
(json.dumps(ser_output) if isinstance(ser_output, dict) else ser_output),
)
def trace_step(fn: Callable[..., Iteration]):
@wraps(fn)
def trace_step_wrapper(*args, **kwargs) -> Iteration:
if not settings.disable_tracing:
current_otel_context = context.get_current()
tracer = trace.get_tracer("guardrails-ai", GUARDRAILS_VERSION)
with tracer.start_as_current_span(
name="step", # type: ignore
context=current_otel_context, # type: ignore
) as step_span:
if SpanAttributes is not None:
step_span.set_attribute(
SpanAttributes.OPENINFERENCE_SPAN_KIND, "GUARDRAIL"
)
try:
response = fn(*args, **kwargs)
add_step_attributes(step_span, response, *args, **kwargs)
add_user_attributes(step_span)
return response
except Exception as e:
step_span.set_status(status=StatusCode.ERROR, description=str(e))
add_step_attributes(step_span, None, *args, **kwargs)
add_user_attributes(step_span)
raise e
else:
return fn(*args, **kwargs)
return trace_step_wrapper
def trace_stream_step_generator(
fn: Callable[..., Iterator[ValidationOutcome[OT]]], *args, **kwargs
) -> Iterator[ValidationOutcome[OT]]:
current_otel_context = context.get_current()
tracer = trace.get_tracer("guardrails-ai", GUARDRAILS_VERSION)
exception = None
with tracer.start_as_current_span(
name="step", # type: ignore
context=current_otel_context, # type: ignore
) as step_span:
if SpanAttributes is not None:
step_span.set_attribute(SpanAttributes.OPENINFERENCE_SPAN_KIND, "GUARDRAIL")
try:
gen = fn(*args, **kwargs)
next_exists = True
while next_exists:
try:
res = next(gen)
yield res
except StopIteration:
next_exists = False
except Exception as e:
step_span.set_status(status=StatusCode.ERROR, description=str(e))
exception = e
finally:
call = safe_get(args, 8, kwargs.get("call_log", None))
iteration = call.iterations.last if call else None
add_step_attributes(step_span, iteration, *args, **kwargs)
add_user_attributes(step_span)
if exception:
raise exception
def trace_stream_step(
fn: Callable[..., Iterator[ValidationOutcome[OT]]],
) -> Callable[..., Iterator[ValidationOutcome[OT]]]:
@wraps(fn)
def trace_stream_step_wrapper(*args, **kwargs) -> Iterator[ValidationOutcome[OT]]:
if not settings.disable_tracing:
return trace_stream_step_generator(fn, *args, **kwargs)
else:
return fn(*args, **kwargs)
return trace_stream_step_wrapper
def trace_async_step(fn: Callable[..., Awaitable[Iteration]]):
@wraps(fn)
async def trace_async_step_wrapper(*args, **kwargs) -> Iteration:
if not settings.disable_tracing:
current_otel_context = context.get_current()
tracer = trace.get_tracer("guardrails-ai", GUARDRAILS_VERSION)
with tracer.start_as_current_span(
name="step", # type: ignore
context=current_otel_context, # type: ignore
) as step_span:
if SpanAttributes is not None:
step_span.set_attribute(
SpanAttributes.OPENINFERENCE_SPAN_KIND, "GUARDRAIL"
)
try:
response = await fn(*args, **kwargs)
add_user_attributes(step_span)
add_step_attributes(step_span, response, *args, **kwargs)
return response
except Exception as e:
step_span.set_status(status=StatusCode.ERROR, description=str(e))
add_user_attributes(step_span)
add_step_attributes(step_span, None, *args, **kwargs)
raise e
else:
return await fn(*args, **kwargs)
return trace_async_step_wrapper
async def trace_async_stream_step_generator(
fn: Callable[..., AsyncIterator[ValidationOutcome[OT]]], *args, **kwargs
) -> AsyncIterator[ValidationOutcome[OT]]:
current_otel_context = context.get_current()
tracer = trace.get_tracer("guardrails-ai", GUARDRAILS_VERSION)
exception = None
with tracer.start_as_current_span(
name="step", # type: ignore
context=current_otel_context, # type: ignore
) as step_span:
if SpanAttributes is not None:
step_span.set_attribute(SpanAttributes.OPENINFERENCE_SPAN_KIND, "GUARDRAIL")
try:
gen = fn(*args, **kwargs)
next_exists = True
while next_exists:
try:
res = await anext(gen) # type: ignore
yield res
except StopIteration:
next_exists = False
except StopAsyncIteration:
next_exists = False
except Exception as e:
step_span.set_status(status=StatusCode.ERROR, description=str(e))
exception = e
finally:
call = safe_get(args, 3, kwargs.get("call_log", None))
iteration = call.iterations.last if call else None
add_step_attributes(step_span, iteration, *args, **kwargs)
if exception:
raise exception
def trace_async_stream_step(
fn: Callable[..., AsyncIterator[ValidationOutcome[OT]]],
):
@wraps(fn)
async def trace_async_stream_step_wrapper(
*args, **kwargs
) -> AsyncIterator[ValidationOutcome[OT]]:
if not settings.disable_tracing:
return trace_async_stream_step_generator(fn, *args, **kwargs)
else:
return fn(*args, **kwargs)
return trace_async_stream_step_wrapper
#########################################
#### END Runner.step Instrumentation ####
#########################################
#########################################
### START Runner.call Instrumentation ###
#########################################
# TODO: Track input arguments and outputs explicitly as named attributes
def add_call_attributes(
call_span: Span, response: Optional[LLMResponse], *args, **kwargs
):
guard_name = get_guard_name()
call_span.set_attribute("guardrails.version", GUARDRAILS_VERSION)
call_span.set_attribute("type", "guardrails/guard/step/call")
call_span.set_attribute("guard.name", guard_name)
ser_args = [serialize(arg) for arg in args]
ser_kwargs = {k: serialize(v) for k, v in kwargs.items()}
inputs = {
"args": [sarg for sarg in ser_args if sarg is not None],
"kwargs": {k: v for k, v in ser_kwargs.items() if v is not None},
}
for k in inputs:
inputs[k] = recursive_key_operation(inputs[k], redact)
call_span.set_attribute("input.mime_type", "application/json")
call_span.set_attribute("input.value", json.dumps(inputs))
ser_output = serialize(response)
if ser_output:
call_span.set_attribute("output.mime_type", "application/json")
call_span.set_attribute(
"output.value",
(json.dumps(ser_output) if isinstance(ser_output, dict) else ser_output),
)
def trace_call(fn: Callable[..., LLMResponse]):
@wraps(fn)
def trace_call_wrapper(*args, **kwargs):
if not settings.disable_tracing:
current_otel_context = context.get_current()
tracer = trace.get_tracer("guardrails-ai", GUARDRAILS_VERSION)
with tracer.start_as_current_span(
name="call", # type: ignore
context=current_otel_context, # type: ignore
) as call_span:
try:
response = fn(*args, **kwargs)
if isinstance(response, LLMResponse) and (
response.async_stream_output or response.stream_output
):
# TODO: Iterate, add a call attr each time
return response
add_call_attributes(call_span, response, *args, **kwargs)
return response
except Exception as e:
call_span.set_status(status=StatusCode.ERROR, description=str(e))
add_call_attributes(call_span, None, *args, **kwargs)
raise e
else:
return fn(*args, **kwargs)
return trace_call_wrapper
def trace_async_call(fn: Callable[..., Awaitable[LLMResponse]]):
@wraps(fn)
async def trace_async_call_wrapper(*args, **kwargs):
if not settings.disable_tracing:
current_otel_context = context.get_current()
tracer = trace.get_tracer("guardrails-ai", GUARDRAILS_VERSION)
with tracer.start_as_current_span(
name="call", # type: ignore
context=current_otel_context, # type: ignore
) as call_span:
try:
response = await fn(*args, **kwargs)
add_call_attributes(call_span, response, *args, **kwargs)
return response
except Exception as e:
call_span.set_status(status=StatusCode.ERROR, description=str(e))
add_call_attributes(call_span, None, *args, **kwargs)
raise e
else:
return await fn(*args, **kwargs)
return trace_async_call_wrapper
#########################################
#### END Runner.call Instrumentation ####
#########################################