From c9c4c2db4ccdf424f27df63094536004145f2522 Mon Sep 17 00:00:00 2001 From: ZeroZ_JQ Date: Wed, 30 Apr 2025 15:50:02 +0800 Subject: [PATCH] feat(api): add OpenTelemetry instrumentation for requests and httpx --- api/extensions/ext_otel.py | 58 +++++++++++++++++++++++++++++++++++++- api/pyproject.toml | 2 ++ api/uv.lock | 34 ++++++++++++++++++++++ 3 files changed, 93 insertions(+), 1 deletion(-) diff --git a/api/extensions/ext_otel.py b/api/extensions/ext_otel.py index be47fdc6d6..7b02df6edf 100644 --- a/api/extensions/ext_otel.py +++ b/api/extensions/ext_otel.py @@ -12,6 +12,10 @@ from flask_login import user_loaded_from_request, user_logged_in # type: ignore from configs import dify_config from dify_app import DifyApp +import requests # To get requests.Response type hint +import httpx # To get httpx.Response type hint +from opentelemetry.semconv.trace import SpanAttributes + @user_logged_in.connect @user_loaded_from_request.connect @@ -108,6 +112,8 @@ def init_app(app: DifyApp): from opentelemetry.instrumentation.celery import CeleryInstrumentor from opentelemetry.instrumentation.flask import FlaskInstrumentor from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor + from opentelemetry.instrumentation.requests import RequestsInstrumentor + from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor from opentelemetry.metrics import get_meter, get_meter_provider, set_meter_provider from opentelemetry.propagate import set_global_textmap from opentelemetry.propagators.b3 import B3Format @@ -124,7 +130,7 @@ def init_app(app: DifyApp): from opentelemetry.semconv.resource import ResourceAttributes from opentelemetry.trace import Span, get_tracer_provider, set_tracer_provider from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator - from opentelemetry.trace.status import StatusCode + from opentelemetry.trace.status import StatusCode, Status setup_context_propagation() # Initialize OpenTelemetry @@ -178,11 +184,61 @@ def init_app(app: DifyApp): export_timeout_millis=dify_config.OTEL_METRIC_EXPORT_TIMEOUT, ) set_meter_provider(MeterProvider(resource=resource, metric_readers=[reader])) + + # Helper function to initialize requests instrumentor + def init_requests_instrumentor(): + def outgoing_requests_response_hook(span: Span, request: requests.PreparedRequest, response: requests.Response): + if span and span.is_recording() and response: + status_code = response.status_code + status_class = f"{status_code // 100}xx" + + # Set span status + if 200 <= status_code < 400: + span.set_status(Status(StatusCode.OK)) + else: + span.set_status(Status(StatusCode.ERROR, f"HTTP status code {status_code}")) + + # Record metric (references counter from outer scope) + _http_client_response_counter.add(1, {"status_code": status_code, "status_class": status_class}) + + # Only record the URL path attribute + if hasattr(request, 'path_url'): # For requests.PreparedRequest + path = request.path_url.split('?', 1)[0] # Get path before query string + span.set_attribute(SpanAttributes.URL_PATH, path) + + RequestsInstrumentor().instrument(response_hook=outgoing_requests_response_hook) + + # Helper function to initialize httpx instrumentor + def init_httpx_instrumentor(): + def outgoing_httpx_response_hook(span: Span, request: httpx.Request, response: httpx.Response): + if span and span.is_recording() and response: + status_code = response.status_code + status_class = f"{status_code // 100}xx" + + # Set span status + if 200 <= status_code < 400: + span.set_status(Status(StatusCode.OK)) + else: + span.set_status(Status(StatusCode.ERROR, f"HTTP status code {status_code}")) + + # Record metric (references counter from outer scope) + _http_client_response_counter.add(1, {"status_code": status_code, "status_class": status_class}) + + # Only record the URL path attribute + if hasattr(request, 'url') and hasattr(request.url, 'path'): # For httpx.Request + span.set_attribute(SpanAttributes.URL_PATH, request.url.path) + + HTTPXClientInstrumentor().instrument(response_hook=outgoing_httpx_response_hook) + + + # Initialize instrumentors if not is_celery_worker(): init_flask_instrumentor(app) CeleryInstrumentor(tracer_provider=get_tracer_provider(), meter_provider=get_meter_provider()).instrument() instrument_exception_logging() init_sqlalchemy_instrumentor(app) + init_requests_instrumentor() + init_httpx_instrumentor() atexit.register(shutdown_tracer) diff --git a/api/pyproject.toml b/api/pyproject.toml index f3526ec717..a1152d5fe7 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -50,6 +50,8 @@ dependencies = [ "opentelemetry-instrumentation-celery==0.48b0", "opentelemetry-instrumentation-flask==0.48b0", "opentelemetry-instrumentation-sqlalchemy==0.48b0", + "opentelemetry-instrumentation-requests==0.48b0", + "opentelemetry-instrumentation-httpx==0.48b0", "opentelemetry-propagator-b3==1.27.0", # opentelemetry-proto1.28.0 depends on protobuf (>=5.0,<6.0), # which is conflict with googleapis-common-protos (1.63.0) diff --git a/api/uv.lock b/api/uv.lock index 9ae14dbd25..32610762e8 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -1201,6 +1201,8 @@ dependencies = [ { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-instrumentation-celery" }, { name = "opentelemetry-instrumentation-flask" }, + { name = "opentelemetry-instrumentation-httpx" }, + { name = "opentelemetry-instrumentation-requests" }, { name = "opentelemetry-instrumentation-sqlalchemy" }, { name = "opentelemetry-propagator-b3" }, { name = "opentelemetry-proto" }, @@ -1371,6 +1373,8 @@ requires-dist = [ { name = "opentelemetry-instrumentation", specifier = "==0.48b0" }, { name = "opentelemetry-instrumentation-celery", specifier = "==0.48b0" }, { name = "opentelemetry-instrumentation-flask", specifier = "==0.48b0" }, + { name = "opentelemetry-instrumentation-httpx", specifier = ">=0.48b0" }, + { name = "opentelemetry-instrumentation-requests", specifier = "==0.48b0" }, { name = "opentelemetry-instrumentation-sqlalchemy", specifier = "==0.48b0" }, { name = "opentelemetry-propagator-b3", specifier = "==1.27.0" }, { name = "opentelemetry-proto", specifier = "==1.27.0" }, @@ -3569,6 +3573,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/3d/fcde4f8f0bf9fa1ee73a12304fa538076fb83fe0a2ae966ab0f0b7da5109/opentelemetry_instrumentation_flask-0.48b0-py3-none-any.whl", hash = "sha256:26b045420b9d76e85493b1c23fcf27517972423480dc6cf78fd6924248ba5808", size = 14588 }, ] +[[package]] +name = "opentelemetry-instrumentation-httpx" +version = "0.48b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/d9/c65d818607c16d1b7ea8d2de6111c6cecadf8d2fd38c1885a72733a7c6d3/opentelemetry_instrumentation_httpx-0.48b0.tar.gz", hash = "sha256:ee977479e10398931921fb995ac27ccdeea2e14e392cb27ef012fc549089b60a", size = 16931 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/fe/f2daa9d6d988c093b8c7b1d35df675761a8ece0b600b035dc04982746c9d/opentelemetry_instrumentation_httpx-0.48b0-py3-none-any.whl", hash = "sha256:d94f9d612c82d09fe22944d1904a30a464c19bea2ba76be656c99a28ad8be8e5", size = 13900 }, +] + +[[package]] +name = "opentelemetry-instrumentation-requests" +version = "0.48b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/ac/5eb78efde21ff21d0ad5dc8c6cc6a0f8ae482ce8a46293c2f45a628b6166/opentelemetry_instrumentation_requests-0.48b0.tar.gz", hash = "sha256:67ab9bd877a0352ee0db4616c8b4ae59736ddd700c598ed907482d44f4c9a2b3", size = 14120 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/df/0df9226d1b14f29d23c07e6194b9fd5ad50e7d987b7fd13df7dcf718aeb1/opentelemetry_instrumentation_requests-0.48b0-py3-none-any.whl", hash = "sha256:d4f01852121d0bd4c22f14f429654a735611d4f7bf3cf93f244bdf1489b2233d", size = 12366 }, +] + [[package]] name = "opentelemetry-instrumentation-sqlalchemy" version = "0.48b0"