Spaces:
Sleeping
Sleeping
Upload 11 files
Browse files- aworld/trace/instrumentation/__init__.py +85 -0
- aworld/trace/instrumentation/asgi.py +203 -0
- aworld/trace/instrumentation/eventbus.py +115 -0
- aworld/trace/instrumentation/fastapi.py +107 -0
- aworld/trace/instrumentation/flask.py +279 -0
- aworld/trace/instrumentation/http_util.py +193 -0
- aworld/trace/instrumentation/llm_metrics.py +118 -0
- aworld/trace/instrumentation/requests.py +213 -0
- aworld/trace/instrumentation/semconv.py +29 -0
- aworld/trace/instrumentation/threading.py +149 -0
- aworld/trace/instrumentation/utils.py +31 -0
aworld/trace/instrumentation/__init__.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# coding: utf-8
|
| 2 |
+
# Copyright (c) 2025 inclusionAI.
|
| 3 |
+
from abc import ABC, abstractmethod
|
| 4 |
+
from typing import Any, Collection
|
| 5 |
+
from packaging.requirements import Requirement, InvalidRequirement
|
| 6 |
+
from importlib_metadata import version, PackageNotFoundError
|
| 7 |
+
from aworld.logs.util import logger
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class Instrumentor(ABC):
|
| 11 |
+
_instance = None
|
| 12 |
+
_has_instrumented = False
|
| 13 |
+
|
| 14 |
+
def __new__(cls, *args, **kwargs):
|
| 15 |
+
if cls._instance is None:
|
| 16 |
+
cls._instance = object.__new__(cls)
|
| 17 |
+
|
| 18 |
+
return cls._instance
|
| 19 |
+
|
| 20 |
+
def instrument(self, **kwargs: Any):
|
| 21 |
+
"""
|
| 22 |
+
Instrument the library.
|
| 23 |
+
"""
|
| 24 |
+
if self._has_instrumented:
|
| 25 |
+
logger.warning(
|
| 26 |
+
f"Instrumentor[{self.__class__.__name__}] has already instrumented, skip")
|
| 27 |
+
return
|
| 28 |
+
|
| 29 |
+
if not self._check_dependency_conflicts():
|
| 30 |
+
return
|
| 31 |
+
|
| 32 |
+
result = self._instrument(**kwargs)
|
| 33 |
+
self._has_instrumented = True
|
| 34 |
+
return result
|
| 35 |
+
|
| 36 |
+
def uninstrument(self, **kwargs: Any):
|
| 37 |
+
"""
|
| 38 |
+
Uninstrument the library.
|
| 39 |
+
"""
|
| 40 |
+
if not self._has_instrumented:
|
| 41 |
+
logger.warning("Instrumentor has not instrumented, skip")
|
| 42 |
+
return
|
| 43 |
+
self._uninstrument(**kwargs)
|
| 44 |
+
self._has_instrumented = False
|
| 45 |
+
|
| 46 |
+
@abstractmethod
|
| 47 |
+
def _uninstrument(self, **kwargs: Any):
|
| 48 |
+
"""
|
| 49 |
+
Uninstrument the library.
|
| 50 |
+
"""
|
| 51 |
+
|
| 52 |
+
@abstractmethod
|
| 53 |
+
def _instrument(self, **kwargs: Any):
|
| 54 |
+
"""
|
| 55 |
+
Instrument the library.
|
| 56 |
+
"""
|
| 57 |
+
|
| 58 |
+
def _check_dependency_conflicts(self):
|
| 59 |
+
dependencies = self.instrumentation_dependencies()
|
| 60 |
+
for dependence in dependencies:
|
| 61 |
+
try:
|
| 62 |
+
requirement = Requirement(dependence)
|
| 63 |
+
except InvalidRequirement as exc:
|
| 64 |
+
logger.warning(
|
| 65 |
+
f'error parsing dependency, reporting as a conflict: "{dependence}" - {exc}')
|
| 66 |
+
return False
|
| 67 |
+
try:
|
| 68 |
+
dist_version = version(requirement.name)
|
| 69 |
+
except PackageNotFoundError as exc:
|
| 70 |
+
logger.warning(
|
| 71 |
+
f'dependency not found, reporting as a conflict: "{dependence}" - {exc}')
|
| 72 |
+
return False
|
| 73 |
+
|
| 74 |
+
if requirement.specifier and not requirement.specifier.contains(dist_version):
|
| 75 |
+
logger.warning(
|
| 76 |
+
f'dependency version conflict, reporting as a conflict: requested: "{self.required}" but found: "{self.found}"')
|
| 77 |
+
return False
|
| 78 |
+
|
| 79 |
+
return True
|
| 80 |
+
|
| 81 |
+
@abstractmethod
|
| 82 |
+
def instrumentation_dependencies(self) -> Collection[str]:
|
| 83 |
+
"""
|
| 84 |
+
Return a list of dependencies that the instrumentation requires.
|
| 85 |
+
"""
|
aworld/trace/instrumentation/asgi.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from timeit import default_timer
|
| 2 |
+
from typing import Any, Awaitable, Callable
|
| 3 |
+
from functools import wraps
|
| 4 |
+
from aworld.metrics.context_manager import MetricContext
|
| 5 |
+
from aworld.trace.instrumentation.http_util import (
|
| 6 |
+
collect_request_attributes_asgi,
|
| 7 |
+
url_disabled,
|
| 8 |
+
parser_host_port_url_from_asgi
|
| 9 |
+
)
|
| 10 |
+
from aworld.trace.base import Span, TraceProvider, TraceContext, Tracer, SpanType
|
| 11 |
+
from aworld.trace.propagator import get_global_trace_propagator
|
| 12 |
+
from aworld.trace.propagator.carrier import DictCarrier, ListTupleCarrier
|
| 13 |
+
from aworld.metrics.metric import MetricType
|
| 14 |
+
from aworld.metrics.template import MetricTemplate
|
| 15 |
+
from aworld.logs.util import logger
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def _wrapped_receive(
|
| 19 |
+
server_span: Span,
|
| 20 |
+
server_span_name: str,
|
| 21 |
+
scope: dict[str, Any],
|
| 22 |
+
receive: Callable[[], Awaitable[dict[str, Any]]],
|
| 23 |
+
attributes: dict[str],
|
| 24 |
+
client_request_hook: Callable = None
|
| 25 |
+
):
|
| 26 |
+
|
| 27 |
+
@wraps(receive)
|
| 28 |
+
async def otel_receive():
|
| 29 |
+
message = await receive()
|
| 30 |
+
if client_request_hook and callable(client_request_hook):
|
| 31 |
+
client_request_hook(scope, message)
|
| 32 |
+
|
| 33 |
+
server_span.set_attribute("asgi.event.type", message.get("type", ""))
|
| 34 |
+
return message
|
| 35 |
+
|
| 36 |
+
return otel_receive
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def _wrapped_send(
|
| 40 |
+
server_span: Span,
|
| 41 |
+
server_span_name: str,
|
| 42 |
+
scope: dict[str, Any],
|
| 43 |
+
send: Callable[[dict[str, Any]], Awaitable[None]],
|
| 44 |
+
attributes: dict[str],
|
| 45 |
+
client_response_hook: Callable = None
|
| 46 |
+
):
|
| 47 |
+
expecting_trailers = False
|
| 48 |
+
|
| 49 |
+
@wraps(send)
|
| 50 |
+
async def otel_send(message: dict[str, Any]):
|
| 51 |
+
nonlocal expecting_trailers
|
| 52 |
+
|
| 53 |
+
status_code = None
|
| 54 |
+
if message["type"] == "http.response.start":
|
| 55 |
+
status_code = message["status"]
|
| 56 |
+
elif message["type"] == "websocket.send":
|
| 57 |
+
status_code = 200
|
| 58 |
+
|
| 59 |
+
# raw_headers = message.get("headers")
|
| 60 |
+
# if raw_headers:
|
| 61 |
+
if status_code:
|
| 62 |
+
server_span.set_attribute(
|
| 63 |
+
"http.response.status_code", status_code)
|
| 64 |
+
|
| 65 |
+
if callable(client_response_hook):
|
| 66 |
+
client_response_hook(scope, message)
|
| 67 |
+
|
| 68 |
+
if message["type"] == "http.response.start":
|
| 69 |
+
expecting_trailers = message.get("trailers", False)
|
| 70 |
+
|
| 71 |
+
propagator = get_global_trace_propagator()
|
| 72 |
+
if propagator:
|
| 73 |
+
trace_context = TraceContext(
|
| 74 |
+
trace_id=server_span.get_trace_id(),
|
| 75 |
+
span_id=server_span.get_span_id()
|
| 76 |
+
)
|
| 77 |
+
propagator.inject(
|
| 78 |
+
trace_context, DictCarrier(message))
|
| 79 |
+
|
| 80 |
+
await send(message)
|
| 81 |
+
|
| 82 |
+
if (
|
| 83 |
+
not expecting_trailers
|
| 84 |
+
and message["type"] == "http.response.body"
|
| 85 |
+
and not message.get("more_body", False)
|
| 86 |
+
) or (
|
| 87 |
+
expecting_trailers
|
| 88 |
+
and message["type"] == "http.response.trailers"
|
| 89 |
+
and not message.get("more_trailers", False)
|
| 90 |
+
):
|
| 91 |
+
server_span.end()
|
| 92 |
+
|
| 93 |
+
return otel_send
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
class TraceMiddleware:
|
| 97 |
+
"""
|
| 98 |
+
A ASGI Middleware for tracing requests and responses.
|
| 99 |
+
"""
|
| 100 |
+
|
| 101 |
+
def __init__(
|
| 102 |
+
self,
|
| 103 |
+
app,
|
| 104 |
+
excluded_urls=None,
|
| 105 |
+
tracer_provider: TraceProvider = None,
|
| 106 |
+
tracer: Tracer = None,
|
| 107 |
+
server_request_hook: Callable = None,
|
| 108 |
+
client_request_hook: Callable = None,
|
| 109 |
+
client_response_hook: Callable = None,):
|
| 110 |
+
self.app = app
|
| 111 |
+
self.excluded_urls = excluded_urls
|
| 112 |
+
self.tracer_provider = tracer_provider
|
| 113 |
+
self.server_request_hook = server_request_hook
|
| 114 |
+
self.client_request_hook = client_request_hook
|
| 115 |
+
self.client_response_hook = client_response_hook
|
| 116 |
+
|
| 117 |
+
self.tracer: Tracer = (self.tracer_provider.get_tracer(
|
| 118 |
+
"aworld.trace.instrumentation.asgi"
|
| 119 |
+
) if tracer is None else tracer)
|
| 120 |
+
|
| 121 |
+
self.duration_histogram = MetricTemplate(
|
| 122 |
+
type=MetricType.HISTOGRAM,
|
| 123 |
+
name="asgi_request_duration_histogram",
|
| 124 |
+
description="Duration of flask HTTP server requests."
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
self.active_requests_counter = MetricTemplate(
|
| 128 |
+
type=MetricType.UPDOWNCOUNTER,
|
| 129 |
+
name="asgi_active_request_counter",
|
| 130 |
+
unit="1",
|
| 131 |
+
description="Number of active HTTP server requests.",
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
async def __call__(
|
| 135 |
+
self,
|
| 136 |
+
scope: dict[str, Any],
|
| 137 |
+
receive: Callable[[], Awaitable[dict[str, Any]]],
|
| 138 |
+
send: Callable[[dict[str, Any]], Awaitable[None]],
|
| 139 |
+
):
|
| 140 |
+
start = default_timer()
|
| 141 |
+
if scope["type"] not in ("http", "websocket"):
|
| 142 |
+
return await self.app(scope, receive, send)
|
| 143 |
+
|
| 144 |
+
_, _, url = parser_host_port_url_from_asgi(scope)
|
| 145 |
+
if self.excluded_urls and url_disabled(url, self.excluded_urls):
|
| 146 |
+
return await self.app(scope, receive, send)
|
| 147 |
+
|
| 148 |
+
span_name = scope.get("method", "HTTP").strip(
|
| 149 |
+
).upper() + "_" + scope.get("path", "").strip()
|
| 150 |
+
|
| 151 |
+
attributes = collect_request_attributes_asgi(scope)
|
| 152 |
+
|
| 153 |
+
if scope["type"] == "http" and MetricContext.metric_initialized():
|
| 154 |
+
MetricContext.inc(self.active_requests_counter, 1, attributes)
|
| 155 |
+
|
| 156 |
+
trace_context = None
|
| 157 |
+
propagator = get_global_trace_propagator()
|
| 158 |
+
if propagator:
|
| 159 |
+
trace_context = propagator.extract(
|
| 160 |
+
ListTupleCarrier(scope.get("headers", [])))
|
| 161 |
+
logger.info(
|
| 162 |
+
f"asgi extract trace_context: {trace_context}, scope: {scope}")
|
| 163 |
+
try:
|
| 164 |
+
with self.tracer.start_as_current_span(
|
| 165 |
+
span_name, span_type=SpanType.SERVER, trace_context=trace_context, attributes=attributes
|
| 166 |
+
) as span:
|
| 167 |
+
|
| 168 |
+
if callable(self.server_request_hook):
|
| 169 |
+
self.server_request_hook(scope)
|
| 170 |
+
|
| 171 |
+
wrappered_receive = _wrapped_receive(
|
| 172 |
+
span,
|
| 173 |
+
span_name,
|
| 174 |
+
scope,
|
| 175 |
+
receive,
|
| 176 |
+
attributes,
|
| 177 |
+
self.client_request_hook
|
| 178 |
+
)
|
| 179 |
+
wrappered_send = _wrapped_send(
|
| 180 |
+
span,
|
| 181 |
+
span_name,
|
| 182 |
+
scope,
|
| 183 |
+
send,
|
| 184 |
+
attributes,
|
| 185 |
+
self.client_response_hook
|
| 186 |
+
)
|
| 187 |
+
|
| 188 |
+
await self.app(scope, wrappered_receive, wrappered_send)
|
| 189 |
+
finally:
|
| 190 |
+
if scope["type"] == "http":
|
| 191 |
+
duration_s = default_timer() - start
|
| 192 |
+
|
| 193 |
+
if MetricContext.metric_initialized():
|
| 194 |
+
MetricContext.histogram_record(
|
| 195 |
+
self.duration_histogram,
|
| 196 |
+
duration_s,
|
| 197 |
+
attributes
|
| 198 |
+
)
|
| 199 |
+
MetricContext.inc(
|
| 200 |
+
self.active_requests_counter, -1, attributes)
|
| 201 |
+
|
| 202 |
+
if span.is_recording():
|
| 203 |
+
span.end()
|
aworld/trace/instrumentation/eventbus.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import wrapt
|
| 2 |
+
from typing import Any, Collection
|
| 3 |
+
from aworld.trace.instrumentation import Instrumentor
|
| 4 |
+
from aworld.trace.base import Tracer, get_tracer_provider_silent, TraceContext
|
| 5 |
+
from aworld.trace.propagator import get_global_trace_propagator, get_global_trace_context
|
| 6 |
+
from aworld.trace.propagator.carrier import DictCarrier
|
| 7 |
+
from aworld.logs.util import logger
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def _emit_message_class_wrapper(tracer: Tracer):
|
| 11 |
+
async def awrapper(wrapped, instance, args, kwargs):
|
| 12 |
+
from aworld.core.event.base import Message
|
| 13 |
+
try:
|
| 14 |
+
event = args[0] if len(args) > 0 else kwargs.get("event")
|
| 15 |
+
propagator = get_global_trace_propagator()
|
| 16 |
+
trace_provider = get_tracer_provider_silent()
|
| 17 |
+
if trace_provider and propagator and event and isinstance(event, Message):
|
| 18 |
+
if not event.headers:
|
| 19 |
+
event.headers = {}
|
| 20 |
+
current_span = trace_provider.get_current_span()
|
| 21 |
+
if current_span:
|
| 22 |
+
trace_context = TraceContext(
|
| 23 |
+
trace_id=current_span.get_trace_id(), span_id=current_span.get_span_id())
|
| 24 |
+
propagator.inject(trace_context=trace_context,
|
| 25 |
+
carrier=DictCarrier(event.headers))
|
| 26 |
+
logger.info(
|
| 27 |
+
f"EventManager emit_message trace propagate, event.headers={event.headers}")
|
| 28 |
+
except Exception as e:
|
| 29 |
+
logger.error(
|
| 30 |
+
f"EventManager emit_message trace propagate exception: {e}")
|
| 31 |
+
return await wrapped(*args, **kwargs)
|
| 32 |
+
return awrapper
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def _emit_message_instance_wrapper(tracer: Tracer):
|
| 36 |
+
|
| 37 |
+
@wrapt.decorator
|
| 38 |
+
async def awrapper(wrapped, instance, args, kwargs):
|
| 39 |
+
wrapper = _emit_message_class_wrapper(tracer)
|
| 40 |
+
return await wrapper(wrapped, instance, args, kwargs)
|
| 41 |
+
|
| 42 |
+
return awrapper
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def _consume_class_wrapper(tracer: Tracer):
|
| 46 |
+
async def awrapper(wrapped, instance, args, kwargs):
|
| 47 |
+
from aworld.core.event.base import Message
|
| 48 |
+
event = await wrapped(*args, **kwargs)
|
| 49 |
+
try:
|
| 50 |
+
propagator = get_global_trace_propagator()
|
| 51 |
+
if propagator and event and isinstance(event, Message) and event.headers:
|
| 52 |
+
trace_context = propagator.extract(DictCarrier(event.headers))
|
| 53 |
+
logger.info(
|
| 54 |
+
f"extract trace_context from event: {trace_context}")
|
| 55 |
+
if trace_context:
|
| 56 |
+
get_global_trace_context().set(trace_context)
|
| 57 |
+
except Exception as e:
|
| 58 |
+
logger.error(
|
| 59 |
+
f"EventManager consume trace propagate exception: {e}")
|
| 60 |
+
return event
|
| 61 |
+
return awrapper
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def _consume_instance_wrapper(tracer: Tracer):
|
| 65 |
+
|
| 66 |
+
@wrapt.decorator
|
| 67 |
+
async def awrapper(wrapped, instance, args, kwargs):
|
| 68 |
+
wrapper = _consume_class_wrapper(tracer)
|
| 69 |
+
return await wrapper(wrapped, instance, args, kwargs)
|
| 70 |
+
|
| 71 |
+
return awrapper
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
class EventBusInstrumentor(Instrumentor):
|
| 75 |
+
|
| 76 |
+
def instrumentation_dependencies(self) -> Collection[str]:
|
| 77 |
+
return ()
|
| 78 |
+
|
| 79 |
+
def _uninstrument(self, **kwargs: Any):
|
| 80 |
+
pass
|
| 81 |
+
|
| 82 |
+
def _instrument(self, **kwargs: Any):
|
| 83 |
+
tracer_provider = get_tracer_provider_silent()
|
| 84 |
+
if not tracer_provider:
|
| 85 |
+
return
|
| 86 |
+
tracer = tracer_provider.get_tracer(
|
| 87 |
+
"aworld.trace.instrumentation.eventbus")
|
| 88 |
+
|
| 89 |
+
wrapt.wrap_function_wrapper(
|
| 90 |
+
"aworld.events.manager",
|
| 91 |
+
"EventManager.emit_message",
|
| 92 |
+
_emit_message_class_wrapper(tracer=tracer)
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
wrapt.wrap_function_wrapper(
|
| 96 |
+
"aworld.events.manager",
|
| 97 |
+
"EventManager.consume",
|
| 98 |
+
_consume_class_wrapper(tracer=tracer)
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def wrap_event_manager(manager: 'aworld.events.manager.EventManager'):
|
| 103 |
+
tracer_provider = get_tracer_provider_silent()
|
| 104 |
+
if not tracer_provider:
|
| 105 |
+
return manager
|
| 106 |
+
tracer = tracer_provider.get_tracer(
|
| 107 |
+
"aworld.trace.instrumentation.eventbus")
|
| 108 |
+
|
| 109 |
+
emit_wrapper = _emit_message_instance_wrapper(tracer)
|
| 110 |
+
consume_wrapper = _consume_instance_wrapper(tracer)
|
| 111 |
+
|
| 112 |
+
manager.emit_message = emit_wrapper(manager.emit_message)
|
| 113 |
+
manager.consume = consume_wrapper(manager.consume)
|
| 114 |
+
|
| 115 |
+
return manager
|
aworld/trace/instrumentation/fastapi.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Any, Callable
|
| 2 |
+
from .asgi import TraceMiddleware
|
| 3 |
+
from aworld.trace.instrumentation import Instrumentor
|
| 4 |
+
from aworld.trace.base import TraceProvider, get_tracer_provider
|
| 5 |
+
from aworld.trace.instrumentation.http_util import (
|
| 6 |
+
get_excluded_urls,
|
| 7 |
+
parse_excluded_urls,
|
| 8 |
+
)
|
| 9 |
+
from aworld.utils.import_package import import_packages
|
| 10 |
+
from aworld.logs.util import logger
|
| 11 |
+
|
| 12 |
+
import_packages(['fastapi']) # noqa
|
| 13 |
+
import fastapi # noqa
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class _InstrumentedFastAPI(fastapi.FastAPI):
|
| 17 |
+
"""Instrumented FastAPI class."""
|
| 18 |
+
_tracer_provider: TraceProvider = None
|
| 19 |
+
_excluded_urls: list[str] = None
|
| 20 |
+
_server_request_hook: Callable = None
|
| 21 |
+
_client_request_hook: Callable = None
|
| 22 |
+
_client_response_hook: Callable = None
|
| 23 |
+
_instrumented_fastapi_apps = set()
|
| 24 |
+
|
| 25 |
+
def __init__(self, *args, **kwargs):
|
| 26 |
+
super().__init__(*args, **kwargs)
|
| 27 |
+
|
| 28 |
+
tracer = self._tracer_provider.get_tracer(
|
| 29 |
+
"aworld.trace.instrumentation.fastapi")
|
| 30 |
+
|
| 31 |
+
self.add_middleware(
|
| 32 |
+
TraceMiddleware,
|
| 33 |
+
tracer=tracer,
|
| 34 |
+
excluded_urls=self._excluded_urls,
|
| 35 |
+
server_request_hook=self._server_request_hook,
|
| 36 |
+
client_request_hook=self._client_request_hook,
|
| 37 |
+
client_response_hook=self._client_response_hook
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
self._is_instrumented_by_trace = True
|
| 41 |
+
self._instrumented_fastapi_apps.add(self)
|
| 42 |
+
|
| 43 |
+
def __del__(self):
|
| 44 |
+
if self in self._instrumented_fastapi_apps:
|
| 45 |
+
self._instrumented_fastapi_apps.remove(self)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class FastAPIInstrumentor(Instrumentor):
|
| 49 |
+
"""FastAPI Instrumentor."""
|
| 50 |
+
_original_fastapi = None
|
| 51 |
+
|
| 52 |
+
@staticmethod
|
| 53 |
+
def uninstrument_app(app: fastapi.FastAPI):
|
| 54 |
+
app.user_middleware = [
|
| 55 |
+
x
|
| 56 |
+
for x in app.user_middleware
|
| 57 |
+
if x.cls is not TraceMiddleware
|
| 58 |
+
]
|
| 59 |
+
app.middleware_stack = app.build_middleware_stack()
|
| 60 |
+
app._is_instrumented_by_trace = False
|
| 61 |
+
|
| 62 |
+
def instrumentation_dependencies(self) -> dict[str, Any]:
|
| 63 |
+
return {"fastapi": fastapi}
|
| 64 |
+
|
| 65 |
+
def _instrument(self, **kwargs):
|
| 66 |
+
self._original_fastapi = fastapi.FastAPI
|
| 67 |
+
_InstrumentedFastAPI._tracer_provider = kwargs.get("tracer_provider")
|
| 68 |
+
_InstrumentedFastAPI._server_request_hook = kwargs.get(
|
| 69 |
+
"server_request_hook"
|
| 70 |
+
)
|
| 71 |
+
_InstrumentedFastAPI._client_request_hook = kwargs.get(
|
| 72 |
+
"client_request_hook"
|
| 73 |
+
)
|
| 74 |
+
_InstrumentedFastAPI._client_response_hook = kwargs.get(
|
| 75 |
+
"client_response_hook"
|
| 76 |
+
)
|
| 77 |
+
excluded_urls = kwargs.get("excluded_urls")
|
| 78 |
+
_InstrumentedFastAPI._excluded_urls = (
|
| 79 |
+
get_excluded_urls("FASTAPI")
|
| 80 |
+
if excluded_urls is None
|
| 81 |
+
else parse_excluded_urls(excluded_urls)
|
| 82 |
+
)
|
| 83 |
+
fastapi.FastAPI = _InstrumentedFastAPI
|
| 84 |
+
|
| 85 |
+
def _uninstrument(self, **kwargs):
|
| 86 |
+
for app in _InstrumentedFastAPI._instrumented_fastapi_apps:
|
| 87 |
+
self.uninstrument_app(app)
|
| 88 |
+
_InstrumentedFastAPI._instrumented_fastapi_apps.clear()
|
| 89 |
+
fastapi.FastAPI = self._original_fastapi
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def instrument_fastapi(excluded_urls: str = None,
|
| 93 |
+
server_request_hook: Callable = None,
|
| 94 |
+
client_request_hook: Callable = None,
|
| 95 |
+
client_response_hook: Callable = None,
|
| 96 |
+
tracer_provider: TraceProvider = None,
|
| 97 |
+
**kwargs: Any,
|
| 98 |
+
):
|
| 99 |
+
kwargs.update({
|
| 100 |
+
"excluded_urls": excluded_urls,
|
| 101 |
+
"server_request_hook": server_request_hook,
|
| 102 |
+
"client_request_hook": client_request_hook,
|
| 103 |
+
"client_response_hook": client_response_hook,
|
| 104 |
+
"tracer_provider": tracer_provider or get_tracer_provider(),
|
| 105 |
+
})
|
| 106 |
+
FastAPIInstrumentor().instrument(**kwargs)
|
| 107 |
+
logger.info("FastAPI instrumented.")
|
aworld/trace/instrumentation/flask.py
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import flask
|
| 2 |
+
import weakref
|
| 3 |
+
from typing import Any, Callable, Collection
|
| 4 |
+
from time import time_ns
|
| 5 |
+
from timeit import default_timer
|
| 6 |
+
from importlib_metadata import version
|
| 7 |
+
from packaging import version as package_version
|
| 8 |
+
from aworld.trace.instrumentation import Instrumentor
|
| 9 |
+
from aworld.trace.base import Span, TraceProvider, TraceContext, Tracer, SpanType, get_tracer_provider
|
| 10 |
+
from aworld.metrics.metric import MetricType
|
| 11 |
+
from aworld.metrics.template import MetricTemplate
|
| 12 |
+
from aworld.logs.util import logger
|
| 13 |
+
from aworld.trace.instrumentation.http_util import (
|
| 14 |
+
collect_request_attributes,
|
| 15 |
+
url_disabled,
|
| 16 |
+
get_excluded_urls,
|
| 17 |
+
parse_excluded_urls,
|
| 18 |
+
HTTP_ROUTE
|
| 19 |
+
)
|
| 20 |
+
from aworld.trace.propagator import get_global_trace_propagator
|
| 21 |
+
from aworld.metrics.context_manager import MetricContext
|
| 22 |
+
from aworld.trace.propagator.carrier import ListTupleCarrier, DictCarrier
|
| 23 |
+
|
| 24 |
+
_ENVIRON_STARTTIME_KEY = "aworld-flask.starttime_key"
|
| 25 |
+
_ENVIRON_SPAN_KEY = "aworld-flask.span_key"
|
| 26 |
+
_ENVIRON_REQCTX_REF_KEY = "aworld-flask.reqctx_ref_key"
|
| 27 |
+
|
| 28 |
+
flask_version = version("flask")
|
| 29 |
+
if package_version.parse(flask_version) >= package_version.parse("2.2.0"):
|
| 30 |
+
|
| 31 |
+
def _request_ctx_ref() -> weakref.ReferenceType:
|
| 32 |
+
return weakref.ref(flask.globals.request_ctx._get_current_object())
|
| 33 |
+
|
| 34 |
+
else:
|
| 35 |
+
|
| 36 |
+
def _request_ctx_ref() -> weakref.ReferenceType:
|
| 37 |
+
return weakref.ref(flask._request_ctx_stack.top)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def _rewrapped_app(
|
| 41 |
+
wsgi_app,
|
| 42 |
+
active_requests_counter,
|
| 43 |
+
duration_histogram,
|
| 44 |
+
response_hook=None,
|
| 45 |
+
excluded_urls=None,
|
| 46 |
+
):
|
| 47 |
+
def _wrapped_app(wrapped_app_environ, start_response):
|
| 48 |
+
# We want to measure the time for route matching, etc.
|
| 49 |
+
# In theory, we could start the span here and use
|
| 50 |
+
# update_name later but that API is "highly discouraged" so
|
| 51 |
+
# we better avoid it.
|
| 52 |
+
wrapped_app_environ[_ENVIRON_STARTTIME_KEY] = time_ns()
|
| 53 |
+
start = default_timer()
|
| 54 |
+
attributes = collect_request_attributes(wrapped_app_environ)
|
| 55 |
+
|
| 56 |
+
if MetricContext.metric_initialized():
|
| 57 |
+
MetricContext.inc(active_requests_counter, 1, attributes)
|
| 58 |
+
|
| 59 |
+
request_route = None
|
| 60 |
+
|
| 61 |
+
def _start_response(status, response_headers, *args, **kwargs):
|
| 62 |
+
if flask.request and (
|
| 63 |
+
excluded_urls is None
|
| 64 |
+
or not url_disabled(flask.request.url, excluded_urls)
|
| 65 |
+
):
|
| 66 |
+
nonlocal request_route
|
| 67 |
+
request_route = flask.request.url_rule
|
| 68 |
+
|
| 69 |
+
span: Span = flask.request.environ.get(_ENVIRON_SPAN_KEY)
|
| 70 |
+
|
| 71 |
+
propagator = get_global_trace_propagator()
|
| 72 |
+
if propagator and span:
|
| 73 |
+
trace_context = TraceContext(
|
| 74 |
+
trace_id=span.get_trace_id(),
|
| 75 |
+
span_id=span.get_span_id()
|
| 76 |
+
)
|
| 77 |
+
propagator.inject(
|
| 78 |
+
trace_context, ListTupleCarrier(response_headers))
|
| 79 |
+
|
| 80 |
+
if span and span.is_recording():
|
| 81 |
+
status_code_str, _ = status.split(" ", 1)
|
| 82 |
+
try:
|
| 83 |
+
status_code = int(status_code_str)
|
| 84 |
+
except ValueError:
|
| 85 |
+
status_code = -1
|
| 86 |
+
|
| 87 |
+
span.set_attribute(
|
| 88 |
+
"http.response.status_code", status_code)
|
| 89 |
+
span.set_attributes(attributes)
|
| 90 |
+
|
| 91 |
+
if response_hook is not None:
|
| 92 |
+
response_hook(span, status, response_headers)
|
| 93 |
+
return start_response(status, response_headers, *args, **kwargs)
|
| 94 |
+
|
| 95 |
+
result = wsgi_app(wrapped_app_environ, _start_response)
|
| 96 |
+
duration_s = default_timer() - start
|
| 97 |
+
|
| 98 |
+
if MetricContext.metric_initialized():
|
| 99 |
+
MetricContext.histogram_record(
|
| 100 |
+
duration_histogram,
|
| 101 |
+
duration_s,
|
| 102 |
+
attributes
|
| 103 |
+
)
|
| 104 |
+
MetricContext.dec(active_requests_counter, 1, attributes)
|
| 105 |
+
return result
|
| 106 |
+
|
| 107 |
+
return _wrapped_app
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def _wrapped_before_request(
|
| 111 |
+
request_hook=None,
|
| 112 |
+
tracer: Tracer = None,
|
| 113 |
+
excluded_urls=None
|
| 114 |
+
):
|
| 115 |
+
def _before_request():
|
| 116 |
+
if excluded_urls and url_disabled(flask.request.url, excluded_urls):
|
| 117 |
+
return
|
| 118 |
+
flask_request_environ = flask.request.environ
|
| 119 |
+
logger.info(
|
| 120 |
+
f"_wrapped_before_request flask_request_environ={flask_request_environ}")
|
| 121 |
+
|
| 122 |
+
attributes = collect_request_attributes(flask_request_environ)
|
| 123 |
+
|
| 124 |
+
if flask.request.url_rule:
|
| 125 |
+
# For 404 that result from no route found, etc, we
|
| 126 |
+
# don't have a url_rule.
|
| 127 |
+
attributes[HTTP_ROUTE] = flask.request.url_rule.rule
|
| 128 |
+
span_name = f"HTTP {flask.request.url_rule.rule}"
|
| 129 |
+
else:
|
| 130 |
+
span_name = f"HTTP {flask.request.url}"
|
| 131 |
+
|
| 132 |
+
propagator = get_global_trace_propagator()
|
| 133 |
+
trace_context = None
|
| 134 |
+
if propagator:
|
| 135 |
+
trace_context = propagator.extract(
|
| 136 |
+
DictCarrier(flask_request_environ))
|
| 137 |
+
|
| 138 |
+
logger.info(f"_wrapped_before_request trace_context={trace_context}")
|
| 139 |
+
|
| 140 |
+
span = tracer.start_span(
|
| 141 |
+
span_name,
|
| 142 |
+
SpanType.SERVER,
|
| 143 |
+
attributes=attributes,
|
| 144 |
+
start_time=flask_request_environ.get(_ENVIRON_STARTTIME_KEY),
|
| 145 |
+
trace_context=trace_context
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
+
if request_hook:
|
| 149 |
+
request_hook(span, flask_request_environ)
|
| 150 |
+
|
| 151 |
+
flask_request_environ[_ENVIRON_SPAN_KEY] = span
|
| 152 |
+
flask_request_environ[_ENVIRON_REQCTX_REF_KEY] = _request_ctx_ref()
|
| 153 |
+
|
| 154 |
+
return _before_request
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def _wrapped_teardown_request(
|
| 158 |
+
excluded_urls=None,
|
| 159 |
+
):
|
| 160 |
+
def _teardown_request(exc):
|
| 161 |
+
if excluded_urls and url_disabled(flask.request.url, excluded_urls):
|
| 162 |
+
return
|
| 163 |
+
|
| 164 |
+
span: Span = flask.request.environ.get(_ENVIRON_SPAN_KEY)
|
| 165 |
+
|
| 166 |
+
original_reqctx_ref = flask.request.environ.get(
|
| 167 |
+
_ENVIRON_REQCTX_REF_KEY
|
| 168 |
+
)
|
| 169 |
+
current_reqctx_ref = _request_ctx_ref()
|
| 170 |
+
if not span or original_reqctx_ref != current_reqctx_ref:
|
| 171 |
+
return
|
| 172 |
+
if exc is None:
|
| 173 |
+
span.end()
|
| 174 |
+
else:
|
| 175 |
+
span.record_exception(exc)
|
| 176 |
+
span.end()
|
| 177 |
+
|
| 178 |
+
return _teardown_request
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
class _InstrumentedFlask(flask.Flask):
|
| 182 |
+
_excluded_urls = None
|
| 183 |
+
_tracer_provider: TraceProvider = None
|
| 184 |
+
_request_hook = None
|
| 185 |
+
_response_hook = None
|
| 186 |
+
|
| 187 |
+
def __init__(self, *args, **kwargs):
|
| 188 |
+
super().__init__(*args, **kwargs)
|
| 189 |
+
|
| 190 |
+
tracer = self._tracer_provider.get_tracer(
|
| 191 |
+
"aworld.trace.instrumentation.flask")
|
| 192 |
+
|
| 193 |
+
duration_histogram = MetricTemplate(
|
| 194 |
+
type=MetricType.HISTOGRAM,
|
| 195 |
+
name="flask_request_duration_histogram",
|
| 196 |
+
description="Duration of flask HTTP server requests."
|
| 197 |
+
)
|
| 198 |
+
|
| 199 |
+
active_requests_counter = MetricTemplate(
|
| 200 |
+
type=MetricType.UPDOWNCOUNTER,
|
| 201 |
+
name="flask_active_request_counter",
|
| 202 |
+
unit="1",
|
| 203 |
+
description="Number of active HTTP server requests.",
|
| 204 |
+
)
|
| 205 |
+
|
| 206 |
+
self.wsgi_app = _rewrapped_app(
|
| 207 |
+
self.wsgi_app,
|
| 208 |
+
active_requests_counter,
|
| 209 |
+
duration_histogram,
|
| 210 |
+
_InstrumentedFlask._response_hook,
|
| 211 |
+
excluded_urls=_InstrumentedFlask._excluded_urls
|
| 212 |
+
)
|
| 213 |
+
|
| 214 |
+
_before_request = _wrapped_before_request(
|
| 215 |
+
_InstrumentedFlask._request_hook,
|
| 216 |
+
tracer,
|
| 217 |
+
excluded_urls=_InstrumentedFlask._excluded_urls
|
| 218 |
+
)
|
| 219 |
+
self._before_request = _before_request
|
| 220 |
+
self.before_request(_before_request)
|
| 221 |
+
|
| 222 |
+
_teardown_request = _wrapped_teardown_request(
|
| 223 |
+
excluded_urls=_InstrumentedFlask._excluded_urls,
|
| 224 |
+
)
|
| 225 |
+
self.teardown_request(_teardown_request)
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
class FlaskInstrumentor(Instrumentor):
|
| 229 |
+
|
| 230 |
+
def instrumentation_dependencies(self) -> Collection[str]:
|
| 231 |
+
return ("flask >= 1.0",)
|
| 232 |
+
|
| 233 |
+
def _instrument(self, **kwargs: Any):
|
| 234 |
+
logger.info("Flask _instrument entered.")
|
| 235 |
+
self._original_flask = flask.Flask
|
| 236 |
+
request_hook = kwargs.get("request_hook")
|
| 237 |
+
response_hook = kwargs.get("response_hook")
|
| 238 |
+
if callable(request_hook):
|
| 239 |
+
_InstrumentedFlask._request_hook = request_hook
|
| 240 |
+
if callable(response_hook):
|
| 241 |
+
_InstrumentedFlask._response_hook = response_hook
|
| 242 |
+
tracer_provider = kwargs.get("tracer_provider")
|
| 243 |
+
_InstrumentedFlask._tracer_provider = tracer_provider
|
| 244 |
+
excluded_urls = kwargs.get("excluded_urls")
|
| 245 |
+
_InstrumentedFlask._excluded_urls = (
|
| 246 |
+
get_excluded_urls("FLASK")
|
| 247 |
+
if excluded_urls is None
|
| 248 |
+
else parse_excluded_urls(excluded_urls)
|
| 249 |
+
)
|
| 250 |
+
flask.Flask = _InstrumentedFlask
|
| 251 |
+
logger.info("Flask _instrument exited.")
|
| 252 |
+
|
| 253 |
+
def _uninstrument(self, **kwargs):
|
| 254 |
+
flask.Flask = self._original_flask
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
def instrument_flask(excluded_urls: str = None,
|
| 258 |
+
request_hook: Callable = None,
|
| 259 |
+
response_hook: Callable = None,
|
| 260 |
+
tracer_provider: TraceProvider = None,
|
| 261 |
+
**kwargs: Any,
|
| 262 |
+
):
|
| 263 |
+
"""
|
| 264 |
+
Instrument the Flask application.
|
| 265 |
+
Args:
|
| 266 |
+
excluded_urls (str): A comma separated list of URLs to be excluded from instrumentation.
|
| 267 |
+
request_hook (Callable): A function to be called before a request is processed.
|
| 268 |
+
response_hook (Callable): A function to be called after a request is processed.
|
| 269 |
+
tracer_provider (TraceProvider): The trace provider to use.
|
| 270 |
+
"""
|
| 271 |
+
all_kwargs = {
|
| 272 |
+
"excluded_urls": excluded_urls,
|
| 273 |
+
"request_hook": request_hook,
|
| 274 |
+
"response_hook": response_hook,
|
| 275 |
+
"tracer_provider": tracer_provider or get_tracer_provider(),
|
| 276 |
+
**kwargs
|
| 277 |
+
}
|
| 278 |
+
FlaskInstrumentor().instrument(**all_kwargs)
|
| 279 |
+
logger.info("Flask instrumented.")
|
aworld/trace/instrumentation/http_util.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from re import compile as re_compile
|
| 3 |
+
from re import search
|
| 4 |
+
from typing import Final, Iterable, Any
|
| 5 |
+
from urllib.parse import urlparse, urlunparse, unquote
|
| 6 |
+
from wsgiref.types import WSGIEnvironment
|
| 7 |
+
from requests.models import PreparedRequest
|
| 8 |
+
|
| 9 |
+
HTTP_REQUEST_METHOD: Final = "http.request.method"
|
| 10 |
+
HTTP_FLAVOR: Final = "http.flavor"
|
| 11 |
+
HTTP_HOST: Final = "http.host"
|
| 12 |
+
HTTP_SCHEME: Final = "http.scheme"
|
| 13 |
+
HTTP_USER_AGENT: Final = "http.user_agent"
|
| 14 |
+
HTTP_SERVER_NAME: Final = "http.server_name"
|
| 15 |
+
SERVER_ADDRESS: Final = "server.address"
|
| 16 |
+
SERVER_PORT: Final = "server.port"
|
| 17 |
+
URL_PATH: Final = "url.path"
|
| 18 |
+
URL_QUERY: Final = "url.query"
|
| 19 |
+
CLIENT_ADDRESS: Final = "client.address"
|
| 20 |
+
CLIENT_PORT: Final = "client.port"
|
| 21 |
+
URL_FULL: Final = "url.full"
|
| 22 |
+
|
| 23 |
+
HTTP_REQUEST_BODY_SIZE: Final = "http.request.body.size"
|
| 24 |
+
HTTP_REQUEST_HEADER: Final = "http.request.header"
|
| 25 |
+
HTTP_REQUEST_SIZE: Final = "http.request.size"
|
| 26 |
+
HTTP_RESPONSE_BODY_SIZE: Final = "http.response.body.size"
|
| 27 |
+
HTTP_RESPONSE_HEADER: Final = "http.response.header"
|
| 28 |
+
HTTP_RESPONSE_SIZE: Final = "http.response.size"
|
| 29 |
+
HTTP_RESPONSE_STATUS_CODE: Final = "http.response.status_code"
|
| 30 |
+
HTTP_ROUTE = "http.route"
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def collect_request_attributes(environ: WSGIEnvironment):
|
| 34 |
+
|
| 35 |
+
attributes: dict[str] = {}
|
| 36 |
+
|
| 37 |
+
request_method = environ.get("REQUEST_METHOD", "")
|
| 38 |
+
request_method = request_method.upper()
|
| 39 |
+
attributes[HTTP_REQUEST_METHOD] = request_method
|
| 40 |
+
attributes[HTTP_FLAVOR] = environ.get("SERVER_PROTOCOL", "")
|
| 41 |
+
attributes[HTTP_SCHEME] = environ.get("wsgi.url_scheme", "")
|
| 42 |
+
attributes[HTTP_SERVER_NAME] = environ.get("SERVER_NAME", "")
|
| 43 |
+
attributes[HTTP_HOST] = environ.get("HTTP_HOST", "")
|
| 44 |
+
host_port = environ.get("SERVER_PORT")
|
| 45 |
+
if host_port:
|
| 46 |
+
attributes[SERVER_PORT] = host_port
|
| 47 |
+
target = environ.get("RAW_URI")
|
| 48 |
+
if target is None:
|
| 49 |
+
target = environ.get("REQUEST_URI")
|
| 50 |
+
if target:
|
| 51 |
+
path, query = _parse_url_query(target)
|
| 52 |
+
attributes[URL_PATH] = path
|
| 53 |
+
attributes[URL_QUERY] = query
|
| 54 |
+
remote_addr = environ.get("REMOTE_ADDR", "")
|
| 55 |
+
attributes[CLIENT_ADDRESS] = remote_addr
|
| 56 |
+
attributes[CLIENT_PORT] = environ.get("REMOTE_PORT", "")
|
| 57 |
+
remote_host = environ.get("REMOTE_HOST")
|
| 58 |
+
if remote_host and remote_host != remote_addr:
|
| 59 |
+
attributes[CLIENT_ADDRESS] = remote_host
|
| 60 |
+
attributes[HTTP_USER_AGENT] = environ.get("HTTP_USER_AGENT", "")
|
| 61 |
+
return attributes
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def collect_attributes_from_request(request: PreparedRequest) -> dict[str]:
|
| 65 |
+
attributes: dict[str] = {}
|
| 66 |
+
|
| 67 |
+
url = remove_url_credentials(request.url)
|
| 68 |
+
attributes[HTTP_REQUEST_METHOD] = request.method
|
| 69 |
+
attributes[URL_FULL] = url
|
| 70 |
+
parsed_url = urlparse(url)
|
| 71 |
+
if parsed_url.scheme:
|
| 72 |
+
attributes[HTTP_SCHEME] = parsed_url.scheme
|
| 73 |
+
if parsed_url.hostname:
|
| 74 |
+
attributes[HTTP_HOST] = parsed_url.hostname
|
| 75 |
+
if parsed_url.port:
|
| 76 |
+
attributes[SERVER_PORT] = parsed_url.port
|
| 77 |
+
return attributes
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def url_disabled(url: str, excluded_urls: Iterable[str]) -> bool:
|
| 81 |
+
"""
|
| 82 |
+
Check if the url is disabled.
|
| 83 |
+
Args:
|
| 84 |
+
url: The url to check.
|
| 85 |
+
excluded_urls: The excluded urls.
|
| 86 |
+
Returns:
|
| 87 |
+
True if the url is disabled, False otherwise.
|
| 88 |
+
"""
|
| 89 |
+
if excluded_urls is None:
|
| 90 |
+
return False
|
| 91 |
+
regex = re_compile("|".join(excluded_urls))
|
| 92 |
+
return search(regex, url)
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def get_excluded_urls(instrumentation: str) -> list[str]:
|
| 96 |
+
"""
|
| 97 |
+
Get the excluded urls.
|
| 98 |
+
Args:
|
| 99 |
+
instrumentation: The instrumentation to get the excluded urls for.
|
| 100 |
+
Returns:
|
| 101 |
+
The excluded urls.
|
| 102 |
+
"""
|
| 103 |
+
|
| 104 |
+
excluded_urls = os.environ.get(f"{instrumentation}_EXCLUDED_URLS")
|
| 105 |
+
|
| 106 |
+
return parse_excluded_urls(excluded_urls)
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def parse_excluded_urls(excluded_urls: str) -> list[str]:
|
| 110 |
+
"""
|
| 111 |
+
Parse the excluded urls.
|
| 112 |
+
Args:
|
| 113 |
+
excluded_urls: The excluded urls.
|
| 114 |
+
Returns:
|
| 115 |
+
The excluded urls.
|
| 116 |
+
"""
|
| 117 |
+
if excluded_urls:
|
| 118 |
+
excluded_url_list = [
|
| 119 |
+
excluded_url.strip() for excluded_url in excluded_urls.split(",")
|
| 120 |
+
]
|
| 121 |
+
else:
|
| 122 |
+
excluded_url_list = []
|
| 123 |
+
|
| 124 |
+
return excluded_url_list
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def remove_url_credentials(url: str) -> str:
|
| 128 |
+
"""Given a string url, remove the username and password only if it is a valid url"""
|
| 129 |
+
|
| 130 |
+
try:
|
| 131 |
+
parsed = urlparse(url)
|
| 132 |
+
if all([parsed.scheme, parsed.netloc]): # checks for valid url
|
| 133 |
+
parsed_url = urlparse(url)
|
| 134 |
+
_, _, netloc = parsed.netloc.rpartition("@")
|
| 135 |
+
return urlunparse(
|
| 136 |
+
(
|
| 137 |
+
parsed_url.scheme,
|
| 138 |
+
netloc,
|
| 139 |
+
parsed_url.path,
|
| 140 |
+
parsed_url.params,
|
| 141 |
+
parsed_url.query,
|
| 142 |
+
parsed_url.fragment,
|
| 143 |
+
)
|
| 144 |
+
)
|
| 145 |
+
except ValueError: # an unparsable url was passed
|
| 146 |
+
pass
|
| 147 |
+
return url
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
def parser_host_port_url_from_asgi(scope: dict[str, Any]):
|
| 151 |
+
"""Returns (host, port, full_url) tuple."""
|
| 152 |
+
server = scope.get("server") or ["0.0.0.0", 80]
|
| 153 |
+
port = server[1]
|
| 154 |
+
server_host = server[0] + (":" + str(port) if str(port) != "80" else "")
|
| 155 |
+
full_path = scope.get("path", "")
|
| 156 |
+
http_url = scope.get("scheme", "http") + "://" + server_host + full_path
|
| 157 |
+
return server_host, port, http_url
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def collect_request_attributes_asgi(scope: dict[str, Any]):
|
| 161 |
+
attributes: dict[str] = {}
|
| 162 |
+
server_host, port, http_url = parser_host_port_url_from_asgi(scope)
|
| 163 |
+
query_string = scope.get("query_string")
|
| 164 |
+
if query_string and http_url:
|
| 165 |
+
if isinstance(query_string, bytes):
|
| 166 |
+
query_string = query_string.decode("utf8")
|
| 167 |
+
http_url += "?" + unquote(query_string)
|
| 168 |
+
attributes[HTTP_REQUEST_METHOD] = scope.get("method", "")
|
| 169 |
+
attributes[HTTP_FLAVOR] = scope.get("http_version", "")
|
| 170 |
+
attributes[HTTP_SCHEME] = scope.get("scheme", "")
|
| 171 |
+
attributes[HTTP_HOST] = server_host
|
| 172 |
+
attributes[SERVER_PORT] = port
|
| 173 |
+
attributes[URL_FULL] = remove_url_credentials(http_url)
|
| 174 |
+
attributes[URL_PATH] = scope.get("path", "")
|
| 175 |
+
header = scope.get("headers")
|
| 176 |
+
if header:
|
| 177 |
+
for key, value in header:
|
| 178 |
+
if key == b"user-agent":
|
| 179 |
+
attributes[HTTP_USER_AGENT] = value.decode("utf8")
|
| 180 |
+
|
| 181 |
+
client = scope.get("client")
|
| 182 |
+
if client:
|
| 183 |
+
attributes[CLIENT_ADDRESS] = client[0]
|
| 184 |
+
attributes[CLIENT_PORT] = client[1]
|
| 185 |
+
|
| 186 |
+
return attributes
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def _parse_url_query(url: str):
|
| 190 |
+
parsed_url = urlparse(url)
|
| 191 |
+
path = parsed_url.path
|
| 192 |
+
query_params = parsed_url.query
|
| 193 |
+
return path, query_params
|
aworld/trace/instrumentation/llm_metrics.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from aworld.metrics.context_manager import MetricContext
|
| 2 |
+
from aworld.metrics.template import MetricTemplate
|
| 3 |
+
from aworld.metrics.metric import MetricType
|
| 4 |
+
|
| 5 |
+
tokens_usage_histogram = MetricTemplate(
|
| 6 |
+
type=MetricType.HISTOGRAM,
|
| 7 |
+
name="llm_token_usage",
|
| 8 |
+
unit="token",
|
| 9 |
+
description="Measures number of input and output tokens used"
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
chat_choice_counter = MetricTemplate(
|
| 13 |
+
type=MetricType.COUNTER,
|
| 14 |
+
name="llm_generation_choice_counter",
|
| 15 |
+
unit="choice",
|
| 16 |
+
description="Number of choices returned by chat completions call"
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
duration_histogram = MetricTemplate(
|
| 20 |
+
type=MetricType.HISTOGRAM,
|
| 21 |
+
name="llm_chat_duration",
|
| 22 |
+
unit="s",
|
| 23 |
+
description="AI chat duration",
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
chat_exception_counter = MetricTemplate(
|
| 27 |
+
type=MetricType.COUNTER,
|
| 28 |
+
name="llm_chat_exception_counter",
|
| 29 |
+
unit="time",
|
| 30 |
+
description="Number of exceptions occurred during chat completions",
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
streaming_time_to_first_token_histogram = MetricTemplate(
|
| 34 |
+
type=MetricType.HISTOGRAM,
|
| 35 |
+
name="llm_streaming_time_to_first_token",
|
| 36 |
+
unit="s",
|
| 37 |
+
description="Time to first token in streaming chat completions",
|
| 38 |
+
)
|
| 39 |
+
streaming_time_to_generate_histogram = MetricTemplate(
|
| 40 |
+
type=MetricType.HISTOGRAM,
|
| 41 |
+
name="streaming_time_to_generate",
|
| 42 |
+
unit="s",
|
| 43 |
+
description="Time between first token and completion in streaming chat completions",
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def record_exception_metric(exception, duration):
|
| 48 |
+
'''
|
| 49 |
+
record chat exception to metrics
|
| 50 |
+
'''
|
| 51 |
+
if MetricContext.metric_initialized():
|
| 52 |
+
labels = {
|
| 53 |
+
"error.type": exception.__class__.__name__,
|
| 54 |
+
}
|
| 55 |
+
if duration_histogram:
|
| 56 |
+
MetricContext.histogram_record(
|
| 57 |
+
duration_histogram, duration, labels=labels)
|
| 58 |
+
if chat_exception_counter:
|
| 59 |
+
MetricContext.count(
|
| 60 |
+
chat_exception_counter, 1, labels=labels)
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def record_streaming_time_to_first_token(duration, labels):
|
| 64 |
+
'''
|
| 65 |
+
Record duration of start time to first token in stream.
|
| 66 |
+
'''
|
| 67 |
+
if MetricContext.metric_initialized():
|
| 68 |
+
MetricContext.histogram_record(
|
| 69 |
+
streaming_time_to_first_token_histogram, duration, labels=labels)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def record_streaming_time_to_generate(first_token_to_generate_duration, labels):
|
| 73 |
+
'''
|
| 74 |
+
Record duration the first token to response to generation
|
| 75 |
+
'''
|
| 76 |
+
if MetricContext.metric_initialized():
|
| 77 |
+
MetricContext.histogram_record(
|
| 78 |
+
streaming_time_to_generate_histogram, first_token_to_generate_duration, labels=labels)
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def record_chat_response_metric(attributes,
|
| 82 |
+
prompt_tokens,
|
| 83 |
+
completion_tokens,
|
| 84 |
+
duration,
|
| 85 |
+
choices=None
|
| 86 |
+
):
|
| 87 |
+
'''
|
| 88 |
+
Record chat response to metrics
|
| 89 |
+
'''
|
| 90 |
+
if MetricContext.metric_initialized():
|
| 91 |
+
if prompt_tokens and tokens_usage_histogram:
|
| 92 |
+
labels = {
|
| 93 |
+
**attributes,
|
| 94 |
+
"llm.prompt_usage_type": "prompt_tokens"
|
| 95 |
+
}
|
| 96 |
+
MetricContext.histogram_record(
|
| 97 |
+
tokens_usage_histogram, prompt_tokens, labels=labels)
|
| 98 |
+
if completion_tokens and tokens_usage_histogram:
|
| 99 |
+
labels = {
|
| 100 |
+
**attributes,
|
| 101 |
+
"llm.prompt_usage_type": "completion_tokens"
|
| 102 |
+
}
|
| 103 |
+
MetricContext.histogram_record(
|
| 104 |
+
tokens_usage_histogram, completion_tokens, labels=labels)
|
| 105 |
+
if duration and duration_histogram:
|
| 106 |
+
MetricContext.histogram_record(
|
| 107 |
+
duration_histogram, duration, labels=attributes)
|
| 108 |
+
if choices and chat_choice_counter:
|
| 109 |
+
MetricContext.count(chat_choice_counter,
|
| 110 |
+
len(choices), labels=attributes)
|
| 111 |
+
for choice in choices:
|
| 112 |
+
if choice.get("finish_reason"):
|
| 113 |
+
finish_reason_attr = {
|
| 114 |
+
**attributes,
|
| 115 |
+
"llm.finish_reason": choice.get("finish_reason")
|
| 116 |
+
}
|
| 117 |
+
MetricContext.count(
|
| 118 |
+
chat_choice_counter, 1, labels=finish_reason_attr)
|
aworld/trace/instrumentation/requests.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from aworld.logs.util import logger
|
| 2 |
+
from aworld.trace.instrumentation.http_util import (
|
| 3 |
+
collect_attributes_from_request,
|
| 4 |
+
url_disabled,
|
| 5 |
+
get_excluded_urls,
|
| 6 |
+
parse_excluded_urls,
|
| 7 |
+
HTTP_RESPONSE_STATUS_CODE,
|
| 8 |
+
HTTP_FLAVOR
|
| 9 |
+
)
|
| 10 |
+
from aworld.metrics.context_manager import MetricContext
|
| 11 |
+
from aworld.metrics.template import MetricTemplate
|
| 12 |
+
from aworld.metrics.metric import MetricType
|
| 13 |
+
from aworld.trace.instrumentation import Instrumentor
|
| 14 |
+
from aworld.trace.propagator.carrier import DictCarrier
|
| 15 |
+
import functools
|
| 16 |
+
from timeit import default_timer
|
| 17 |
+
from requests import sessions
|
| 18 |
+
from requests.models import PreparedRequest, Response
|
| 19 |
+
from requests.structures import CaseInsensitiveDict
|
| 20 |
+
from typing import Collection, Any, Callable
|
| 21 |
+
from aworld.trace.base import TraceProvider, TraceContext, Tracer, SpanType, get_tracer_provider
|
| 22 |
+
from aworld.trace.propagator import get_global_trace_propagator
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def _wrapped_send(
|
| 26 |
+
tracer: Tracer = None,
|
| 27 |
+
excluded_urls=None,
|
| 28 |
+
request_hook: Callable = None,
|
| 29 |
+
response_hook: Callable = None,
|
| 30 |
+
duration_histogram: MetricTemplate = None
|
| 31 |
+
):
|
| 32 |
+
|
| 33 |
+
oringinal_send = sessions.Session.send
|
| 34 |
+
|
| 35 |
+
@functools.wraps(oringinal_send)
|
| 36 |
+
def instrumented_send(
|
| 37 |
+
self: sessions.Session, request: PreparedRequest, **kwargs: Any
|
| 38 |
+
):
|
| 39 |
+
if excluded_urls and url_disabled(request.url, excluded_urls):
|
| 40 |
+
return oringinal_send(self, request, **kwargs)
|
| 41 |
+
|
| 42 |
+
def get_or_create_headers():
|
| 43 |
+
request.headers = (
|
| 44 |
+
request.headers
|
| 45 |
+
if request.headers is not None
|
| 46 |
+
else CaseInsensitiveDict()
|
| 47 |
+
)
|
| 48 |
+
return request.headers
|
| 49 |
+
|
| 50 |
+
method = request.method
|
| 51 |
+
if method is None:
|
| 52 |
+
method = "HTTP"
|
| 53 |
+
span_name = method.upper()
|
| 54 |
+
|
| 55 |
+
span_attributes = collect_attributes_from_request(request)
|
| 56 |
+
with tracer.start_as_current_span(
|
| 57 |
+
span_name, span_type=SpanType.CLIENT, attributes=span_attributes
|
| 58 |
+
) as span:
|
| 59 |
+
exception = None
|
| 60 |
+
if callable(request_hook):
|
| 61 |
+
request_hook(span, request)
|
| 62 |
+
|
| 63 |
+
headers = get_or_create_headers()
|
| 64 |
+
|
| 65 |
+
trace_context = TraceContext(
|
| 66 |
+
trace_id=span.get_trace_id(),
|
| 67 |
+
span_id=span.get_span_id(),
|
| 68 |
+
)
|
| 69 |
+
propagator = get_global_trace_propagator()
|
| 70 |
+
if propagator:
|
| 71 |
+
propagator.inject(trace_context, DictCarrier(headers))
|
| 72 |
+
|
| 73 |
+
start_time = default_timer()
|
| 74 |
+
try:
|
| 75 |
+
logger.info("Sending headers: %s", request.headers)
|
| 76 |
+
result = oringinal_send(
|
| 77 |
+
self, request, **kwargs
|
| 78 |
+
) # *** PROCEED
|
| 79 |
+
except Exception as exc: # pylint: disable=W0703
|
| 80 |
+
exception = exc
|
| 81 |
+
result = getattr(exc, "response", None)
|
| 82 |
+
finally:
|
| 83 |
+
elapsed_time = max(default_timer() - start_time, 0)
|
| 84 |
+
|
| 85 |
+
if isinstance(result, Response):
|
| 86 |
+
span_attributes = {}
|
| 87 |
+
span_attributes[HTTP_RESPONSE_STATUS_CODE] = result.status_code
|
| 88 |
+
|
| 89 |
+
if result.raw is not None:
|
| 90 |
+
version = getattr(result.raw, "version", None)
|
| 91 |
+
if version:
|
| 92 |
+
# Only HTTP/1 is supported by requests
|
| 93 |
+
version_text = "1.1" if version == 11 else "1.0"
|
| 94 |
+
span_attributes[HTTP_FLAVOR] = version_text
|
| 95 |
+
span.set_attributes(span_attributes)
|
| 96 |
+
|
| 97 |
+
if callable(response_hook):
|
| 98 |
+
response_hook(span, request, result)
|
| 99 |
+
|
| 100 |
+
if exception is not None:
|
| 101 |
+
span.record_exception(exception)
|
| 102 |
+
|
| 103 |
+
if duration_histogram is not None and MetricContext.metric_initialized():
|
| 104 |
+
MetricContext.histogram_record(
|
| 105 |
+
duration_histogram,
|
| 106 |
+
elapsed_time,
|
| 107 |
+
span_attributes
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
if exception is not None:
|
| 111 |
+
raise exception.with_traceback(exception.__traceback__)
|
| 112 |
+
|
| 113 |
+
return result
|
| 114 |
+
|
| 115 |
+
return instrumented_send
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
class _InstrumentedSession(sessions.Session):
|
| 119 |
+
"""
|
| 120 |
+
An instrumented requests.Session class.
|
| 121 |
+
"""
|
| 122 |
+
_excluded_urls = None
|
| 123 |
+
_tracer_provider: TraceProvider = None
|
| 124 |
+
_request_hook = None
|
| 125 |
+
_response_hook = None
|
| 126 |
+
|
| 127 |
+
def __init__(self, *args, **kwargs):
|
| 128 |
+
super().__init__(*args, **kwargs)
|
| 129 |
+
|
| 130 |
+
tracer = self._tracer_provider.get_tracer(
|
| 131 |
+
"aworld.trace.instrumentation.requests")
|
| 132 |
+
excluded_urls = kwargs.get("excluded_urls")
|
| 133 |
+
|
| 134 |
+
duration_histogram = MetricTemplate(
|
| 135 |
+
type=MetricType.HISTOGRAM,
|
| 136 |
+
name="client_request_duration_histogram",
|
| 137 |
+
unit="s",
|
| 138 |
+
description="Duration of HTTP client requests."
|
| 139 |
+
)
|
| 140 |
+
self.send = functools.partial(_wrapped_send(
|
| 141 |
+
tracer=tracer,
|
| 142 |
+
excluded_urls=excluded_urls,
|
| 143 |
+
request_hook=self._request_hook,
|
| 144 |
+
response_hook=self._response_hook,
|
| 145 |
+
duration_histogram=duration_histogram
|
| 146 |
+
), self)
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
class RequestsInstrumentor(Instrumentor):
|
| 150 |
+
"""
|
| 151 |
+
An instrumentor for the requests module.
|
| 152 |
+
"""
|
| 153 |
+
|
| 154 |
+
def instrumentation_dependencies(self) -> Collection[str]:
|
| 155 |
+
return ["requests"]
|
| 156 |
+
|
| 157 |
+
def _instrument(self, **kwargs):
|
| 158 |
+
"""
|
| 159 |
+
Instruments the requests module.
|
| 160 |
+
"""
|
| 161 |
+
logger.info("requests _instrument entered.")
|
| 162 |
+
self._original_session = sessions.Session
|
| 163 |
+
request_hook = kwargs.get("request_hook")
|
| 164 |
+
response_hook = kwargs.get("response_hook")
|
| 165 |
+
if callable(request_hook):
|
| 166 |
+
_InstrumentedSession._request_hook = request_hook
|
| 167 |
+
if callable(response_hook):
|
| 168 |
+
_InstrumentedSession._response_hook = response_hook
|
| 169 |
+
tracer_provider = kwargs.get("tracer_provider")
|
| 170 |
+
_InstrumentedSession._tracer_provider = tracer_provider
|
| 171 |
+
excluded_urls = kwargs.get("excluded_urls")
|
| 172 |
+
_InstrumentedSession._excluded_urls = (
|
| 173 |
+
get_excluded_urls("FLASK")
|
| 174 |
+
if excluded_urls is None
|
| 175 |
+
else parse_excluded_urls(excluded_urls)
|
| 176 |
+
)
|
| 177 |
+
sessions.Session = _InstrumentedSession
|
| 178 |
+
logger.info("requests _instrument exited.")
|
| 179 |
+
|
| 180 |
+
def _uninstrument(self, **kwargs):
|
| 181 |
+
"""
|
| 182 |
+
Uninstruments the requests module.
|
| 183 |
+
"""
|
| 184 |
+
sessions.Session = self._original_session
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
def instrument_requests(excluded_urls: str = None,
|
| 188 |
+
request_hook: Callable = None,
|
| 189 |
+
response_hook: Callable = None,
|
| 190 |
+
tracer_provider: TraceProvider = None,
|
| 191 |
+
**kwargs: Any,
|
| 192 |
+
):
|
| 193 |
+
"""
|
| 194 |
+
Instruments the requests module.
|
| 195 |
+
Args:
|
| 196 |
+
excluded_urls: A comma separated list of URLs to exclude from tracing.
|
| 197 |
+
request_hook: A function that will be called before a request is sent.
|
| 198 |
+
The function will be called with the span and the request.
|
| 199 |
+
response_hook: A function that will be called after a response is received.
|
| 200 |
+
The function will be called with the span and the response.
|
| 201 |
+
tracer_provider: The tracer provider to use. If not provided, the global
|
| 202 |
+
tracer provider will be used.
|
| 203 |
+
kwargs: Additional keyword arguments.
|
| 204 |
+
"""
|
| 205 |
+
all_kwargs = {
|
| 206 |
+
"excluded_urls": excluded_urls,
|
| 207 |
+
"request_hook": request_hook,
|
| 208 |
+
"response_hook": response_hook,
|
| 209 |
+
"tracer_provider": tracer_provider or get_tracer_provider(),
|
| 210 |
+
**kwargs
|
| 211 |
+
}
|
| 212 |
+
RequestsInstrumentor().instrument(**all_kwargs)
|
| 213 |
+
logger.info("Requests instrumented.")
|
aworld/trace/instrumentation/semconv.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# GenAI semconv attribute names
|
| 2 |
+
GEN_AI_SYSTEM = "gen_ai.system"
|
| 3 |
+
GEN_AI_REQUEST_MODEL = "gen_ai.request.model"
|
| 4 |
+
GEN_AI_REQUEST_FREQUENCY_PENALTY = "gen_ai.request.frequency_penalty"
|
| 5 |
+
GEN_AI_REQUEST_MAX_TOKENS = "gen_ai.request.max_tokens"
|
| 6 |
+
GEN_AI_REQUEST_PRESENCE_PENALTY = "gen_ai.request.presence_penalty"
|
| 7 |
+
GEN_AI_REQUEST_STOP_SEQUENCES = "gen_ai.request.stop_sequences"
|
| 8 |
+
GEN_AI_REQUEST_TEMPERATURE = "gen_ai.request.temperature"
|
| 9 |
+
GEN_AI_REQUEST_TOP_K = "gen_ai.request.top_k"
|
| 10 |
+
GEN_AI_REQUEST_TOP_P = "gen_ai.request.top_p"
|
| 11 |
+
GEN_AI_REQUEST_STREAMING = "gen_ai.request.streaming"
|
| 12 |
+
GEN_AI_REQUEST_USER = "gen_ai.request.user"
|
| 13 |
+
GEN_AI_REQUEST_EXTRA_HEADERS = "gen_ai.request.extra_headers"
|
| 14 |
+
GEN_AI_PROMPT = "gen_ai.prompt"
|
| 15 |
+
GEN_AI_PROMPT_TOOLS = "gen_ai.prompt.tools"
|
| 16 |
+
GEN_AI_COMPLETION = "gen_ai.completion"
|
| 17 |
+
GEN_AI_COMPLETION_TOOL_CALLS = "gen_ai.completion.tool_calls"
|
| 18 |
+
GEN_AI_COMPLETION_CONTENT = "gen_ai.completion.content"
|
| 19 |
+
GEN_AI_DURATION = "gen_ai.duration"
|
| 20 |
+
GEN_AI_FIRST_TOKEN_DURATION = "gen_ai.first_token_duration"
|
| 21 |
+
GEN_AI_RESPONSE_FINISH_REASONS = "gen_ai.response.finish_reasons"
|
| 22 |
+
GEN_AI_RESPONSE_ID = "gen_ai.response.id"
|
| 23 |
+
GEN_AI_RESPONSE_MODEL = "gen_ai.response.model"
|
| 24 |
+
GEN_AI_USAGE_INPUT_TOKENS = "gen_ai.usage.input_tokens"
|
| 25 |
+
GEN_AI_USAGE_OUTPUT_TOKENS = "gen_ai.usage.output_tokens"
|
| 26 |
+
GEN_AI_USAGE_TOTAL_TOKENS = "gen_ai.usage.total_tokens"
|
| 27 |
+
GEN_AI_OPERATION_NAME = "gen_ai.operation.name"
|
| 28 |
+
GEN_AI_METHOD_NAME = "gen_ai.method.name"
|
| 29 |
+
GEN_AI_SERVER_ADDRESS = "gen_ai.server.address"
|
aworld/trace/instrumentation/threading.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import threading
|
| 2 |
+
from typing import Protocol, TypeVar, Any, Callable
|
| 3 |
+
from wrapt import wrap_function_wrapper
|
| 4 |
+
from concurrent import futures
|
| 5 |
+
import aworld.trace as trace
|
| 6 |
+
from aworld.trace.base import TraceContext, Span
|
| 7 |
+
from aworld.trace.propagator import get_global_trace_context
|
| 8 |
+
from aworld.trace.instrumentation import Instrumentor
|
| 9 |
+
from aworld.trace.instrumentation.utils import unwrap
|
| 10 |
+
from aworld.logs.util import logger
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
R = TypeVar("R")
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class HasTraceContext(Protocol):
|
| 17 |
+
_trace_context: TraceContext
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class ThreadingInstrumentor(Instrumentor):
|
| 21 |
+
'''
|
| 22 |
+
Trace instrumentor for threading
|
| 23 |
+
'''
|
| 24 |
+
|
| 25 |
+
def instrumentation_dependencies(self) -> str:
|
| 26 |
+
return ()
|
| 27 |
+
|
| 28 |
+
def _instrument(self, **kwargs: Any):
|
| 29 |
+
self._instrument_thread()
|
| 30 |
+
self._instrument_timer()
|
| 31 |
+
self._instrument_thread_pool()
|
| 32 |
+
|
| 33 |
+
def _uninstrument(self, **kwargs: Any):
|
| 34 |
+
self._uninstrument_thread()
|
| 35 |
+
self._uninstrument_timer()
|
| 36 |
+
self._uninstrument_thread_pool()
|
| 37 |
+
|
| 38 |
+
@staticmethod
|
| 39 |
+
def _instrument_thread():
|
| 40 |
+
wrap_function_wrapper(
|
| 41 |
+
threading.Thread,
|
| 42 |
+
"start",
|
| 43 |
+
ThreadingInstrumentor.__wrap_threading_start,
|
| 44 |
+
)
|
| 45 |
+
wrap_function_wrapper(
|
| 46 |
+
threading.Thread,
|
| 47 |
+
"run",
|
| 48 |
+
ThreadingInstrumentor.__wrap_threading_run,
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
@staticmethod
|
| 52 |
+
def _instrument_timer():
|
| 53 |
+
wrap_function_wrapper(
|
| 54 |
+
threading.Timer,
|
| 55 |
+
"start",
|
| 56 |
+
ThreadingInstrumentor.__wrap_threading_start,
|
| 57 |
+
)
|
| 58 |
+
wrap_function_wrapper(
|
| 59 |
+
threading.Timer,
|
| 60 |
+
"run",
|
| 61 |
+
ThreadingInstrumentor.__wrap_threading_run,
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
@staticmethod
|
| 65 |
+
def _instrument_thread_pool():
|
| 66 |
+
wrap_function_wrapper(
|
| 67 |
+
futures.ThreadPoolExecutor,
|
| 68 |
+
"submit",
|
| 69 |
+
ThreadingInstrumentor.__wrap_thread_pool_submit,
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
@staticmethod
|
| 73 |
+
def _uninstrument_thread():
|
| 74 |
+
unwrap(threading.Thread, "start")
|
| 75 |
+
unwrap(threading.Thread, "run")
|
| 76 |
+
|
| 77 |
+
@staticmethod
|
| 78 |
+
def _uninstrument_timer():
|
| 79 |
+
unwrap(threading.Timer, "start")
|
| 80 |
+
unwrap(threading.Timer, "run")
|
| 81 |
+
|
| 82 |
+
@staticmethod
|
| 83 |
+
def _uninstrument_thread_pool():
|
| 84 |
+
unwrap(futures.ThreadPoolExecutor, "submit")
|
| 85 |
+
|
| 86 |
+
@staticmethod
|
| 87 |
+
def __wrap_threading_start(
|
| 88 |
+
call_wrapped: Callable[[], None],
|
| 89 |
+
instance: HasTraceContext,
|
| 90 |
+
args: tuple[()],
|
| 91 |
+
kwargs: dict[str, Any],
|
| 92 |
+
) -> None:
|
| 93 |
+
span: Span = trace.get_current_span()
|
| 94 |
+
if span:
|
| 95 |
+
instance._trace_context = TraceContext(
|
| 96 |
+
trace_id=span.get_trace_id(), span_id=span.get_span_id())
|
| 97 |
+
return call_wrapped(*args, **kwargs)
|
| 98 |
+
|
| 99 |
+
@staticmethod
|
| 100 |
+
def __wrap_threading_run(
|
| 101 |
+
call_wrapped: Callable[..., R],
|
| 102 |
+
instance: HasTraceContext,
|
| 103 |
+
args: tuple[Any, ...],
|
| 104 |
+
kwargs: dict[str, Any],
|
| 105 |
+
) -> R:
|
| 106 |
+
|
| 107 |
+
token = None
|
| 108 |
+
try:
|
| 109 |
+
if hasattr(instance, "_trace_context"):
|
| 110 |
+
if instance._trace_context:
|
| 111 |
+
token = get_global_trace_context().set(instance._trace_context)
|
| 112 |
+
return call_wrapped(*args, **kwargs)
|
| 113 |
+
finally:
|
| 114 |
+
if token:
|
| 115 |
+
get_global_trace_context().reset(token)
|
| 116 |
+
|
| 117 |
+
@staticmethod
|
| 118 |
+
def __wrap_thread_pool_submit(
|
| 119 |
+
call_wrapped: Callable[..., R],
|
| 120 |
+
instance: futures.ThreadPoolExecutor,
|
| 121 |
+
args: tuple[Callable[..., Any], ...],
|
| 122 |
+
kwargs: dict[str, Any],
|
| 123 |
+
) -> R:
|
| 124 |
+
# obtain the original function and wrapped kwargs
|
| 125 |
+
original_func = args[0]
|
| 126 |
+
trace_context = None
|
| 127 |
+
span: Span = trace.get_current_span()
|
| 128 |
+
if span and span.get_trace_id() != "":
|
| 129 |
+
trace_context = TraceContext(
|
| 130 |
+
trace_id=span.get_trace_id(), span_id=span.get_span_id())
|
| 131 |
+
|
| 132 |
+
def wrapped_func(*func_args: Any, **func_kwargs: Any) -> R:
|
| 133 |
+
token = None
|
| 134 |
+
try:
|
| 135 |
+
if trace_context:
|
| 136 |
+
token = get_global_trace_context().set(trace_context)
|
| 137 |
+
return original_func(*func_args, **func_kwargs)
|
| 138 |
+
finally:
|
| 139 |
+
if token:
|
| 140 |
+
get_global_trace_context().reset(token)
|
| 141 |
+
|
| 142 |
+
# replace the original function with the wrapped function
|
| 143 |
+
new_args: tuple[Callable[..., Any], ...] = (wrapped_func,) + args[1:]
|
| 144 |
+
return call_wrapped(*new_args, **kwargs)
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def instrument_theading(**kwargs: Any) -> None:
|
| 148 |
+
ThreadingInstrumentor().instrument(**kwargs)
|
| 149 |
+
logger.info("Threading instrumented")
|
aworld/trace/instrumentation/utils.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from importlib import import_module
|
| 2 |
+
from wrapt import ObjectProxy
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def unwrap(obj: object, attr: str):
|
| 6 |
+
"""Given a function that was wrapped by wrapt.wrap_function_wrapper, unwrap it
|
| 7 |
+
|
| 8 |
+
The object containing the function to unwrap may be passed as dotted module path string.
|
| 9 |
+
|
| 10 |
+
Args:
|
| 11 |
+
obj: Object that holds a reference to the wrapped function or dotted import path as string
|
| 12 |
+
attr (str): Name of the wrapped function
|
| 13 |
+
"""
|
| 14 |
+
if isinstance(obj, str):
|
| 15 |
+
try:
|
| 16 |
+
module_path, class_name = obj.rsplit(".", 1)
|
| 17 |
+
except ValueError as exc:
|
| 18 |
+
raise ImportError(
|
| 19 |
+
f"Cannot parse '{obj}' as dotted import path"
|
| 20 |
+
) from exc
|
| 21 |
+
module = import_module(module_path)
|
| 22 |
+
try:
|
| 23 |
+
obj = getattr(module, class_name)
|
| 24 |
+
except AttributeError as exc:
|
| 25 |
+
raise ImportError(
|
| 26 |
+
f"Cannot import '{class_name}' from '{module}'"
|
| 27 |
+
) from exc
|
| 28 |
+
|
| 29 |
+
func = getattr(obj, attr, None)
|
| 30 |
+
if func and isinstance(func, ObjectProxy) and hasattr(func, "__wrapped__"):
|
| 31 |
+
setattr(obj, attr, func.__wrapped__)
|