From 65ac2d5be702421ce29622220d1b4b73d576f54c Mon Sep 17 00:00:00 2001 From: onur-askui <210008309+onur-askui@users.noreply.github.com> Date: Tue, 25 Nov 2025 16:21:58 +0100 Subject: [PATCH 1/2] feat: introduce otel tracing --- .env.template | 6 + pdm.lock | 299 ++++++++++++++++++++++++++++++++- pyproject.toml | 6 + src/askui/chat/__main__.py | 4 + src/askui/chat/api/settings.py | 5 + src/askui/telemetry/otel.py | 75 +++++++++ 6 files changed, 388 insertions(+), 7 deletions(-) create mode 100644 src/askui/telemetry/otel.py diff --git a/.env.template b/.env.template index cd3b5ae2..df704083 100644 --- a/.env.template +++ b/.env.template @@ -15,3 +15,9 @@ OPEN_ROUTER_API_KEY= # Telemetry ASKUI__VA__TELEMETRY__ENABLED=True # Set to "False" to disable telemetry + +# OpenTelemetry Tracing Configuration +#ASKUI__CHAT_API__OTEL__ENABLED=False +#ASKUI__CHAT_API__OTEL__ENDPOINT=http://localhost/v1/traces +#ASKUI__CHAT_API__OTEL__SECRET= +#ASKUI__CHAT_API__OTEL__SERVICE_NAME=chat-api diff --git a/pdm.lock b/pdm.lock index ec19f5d5..4b929458 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "all", "android", "bedrock", "chat", "dev", "pynput", "vertex", "web"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:b332e07a6842e88bbc634645b10755fe3c5a69a341896a307af1ef8ee51114e9" +content_hash = "sha256:bceafb533a2ba147d58188facea3b328fbe5bf1adf15fa8af86950f7a65a1cf0" [[metadata.targets]] requires_python = ">=3.10,<3.14" @@ -149,6 +149,20 @@ files = [ {file = "asgi_correlation_id-4.3.4.tar.gz", hash = "sha256:ea6bc310380373cb9f731dc2e8b2b6fb978a76afe33f7a2384f697b8d6cd811d"}, ] +[[package]] +name = "asgiref" +version = "3.10.0" +requires_python = ">=3.9" +summary = "ASGI specs, helper code, and adapters" +groups = ["all", "chat"] +dependencies = [ + "typing-extensions>=4; python_version < \"3.11\"", +] +files = [ + {file = "asgiref-3.10.0-py3-none-any.whl", hash = "sha256:aef8a81283a34d0ab31630c9b7dfe70c812c95eba78171367ca8745e88124734"}, + {file = "asgiref-3.10.0.tar.gz", hash = "sha256:d89f2d8cd8b56dada7d52fa7dc8075baa08fb836560710d38c292a7a3f78c04e"}, +] + [[package]] name = "asyncer" version = "0.0.8" @@ -326,7 +340,7 @@ name = "certifi" version = "2025.8.3" requires_python = ">=3.7" summary = "Python package for providing Mozilla's CA Bundle." -groups = ["default", "all", "bedrock", "vertex"] +groups = ["default", "all", "bedrock", "chat", "vertex"] files = [ {file = "certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5"}, {file = "certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407"}, @@ -422,7 +436,7 @@ name = "charset-normalizer" version = "3.4.3" requires_python = ">=3.7" summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -groups = ["default", "all", "vertex"] +groups = ["default", "all", "chat", "vertex"] files = [ {file = "charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72"}, {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe"}, @@ -1317,7 +1331,7 @@ name = "googleapis-common-protos" version = "1.71.0" requires_python = ">=3.7" summary = "Common protobufs used in Google APIs" -groups = ["all", "vertex"] +groups = ["all", "chat", "vertex"] dependencies = [ "protobuf!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5,<7.0.0,>=3.20.2", ] @@ -1699,6 +1713,21 @@ files = [ {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, ] +[[package]] +name = "importlib-metadata" +version = "8.7.0" +requires_python = ">=3.9" +summary = "Read metadata from Python packages" +groups = ["all", "chat"] +dependencies = [ + "typing-extensions>=3.6.4; python_version < \"3.8\"", + "zipp>=3.20", +] +files = [ + {file = "importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd"}, + {file = "importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000"}, +] + [[package]] name = "inflect" version = "7.5.0" @@ -2592,6 +2621,200 @@ files = [ {file = "openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050"}, ] +[[package]] +name = "opentelemetry-api" +version = "1.38.0" +requires_python = ">=3.9" +summary = "OpenTelemetry Python API" +groups = ["all", "chat"] +dependencies = [ + "importlib-metadata<8.8.0,>=6.0", + "typing-extensions>=4.5.0", +] +files = [ + {file = "opentelemetry_api-1.38.0-py3-none-any.whl", hash = "sha256:2891b0197f47124454ab9f0cf58f3be33faca394457ac3e09daba13ff50aa582"}, + {file = "opentelemetry_api-1.38.0.tar.gz", hash = "sha256:f4c193b5e8acb0912b06ac5b16321908dd0843d75049c091487322284a3eea12"}, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.38.0" +requires_python = ">=3.9" +summary = "OpenTelemetry Protobuf encoding" +groups = ["all", "chat"] +dependencies = [ + "opentelemetry-proto==1.38.0", +] +files = [ + {file = "opentelemetry_exporter_otlp_proto_common-1.38.0-py3-none-any.whl", hash = "sha256:03cb76ab213300fe4f4c62b7d8f17d97fcfd21b89f0b5ce38ea156327ddda74a"}, + {file = "opentelemetry_exporter_otlp_proto_common-1.38.0.tar.gz", hash = "sha256:e333278afab4695aa8114eeb7bf4e44e65c6607d54968271a249c180b2cb605c"}, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.38.0" +requires_python = ">=3.9" +summary = "OpenTelemetry Collector Protobuf over HTTP Exporter" +groups = ["all", "chat"] +dependencies = [ + "googleapis-common-protos~=1.52", + "opentelemetry-api~=1.15", + "opentelemetry-exporter-otlp-proto-common==1.38.0", + "opentelemetry-proto==1.38.0", + "opentelemetry-sdk~=1.38.0", + "requests~=2.7", + "typing-extensions>=4.5.0", +] +files = [ + {file = "opentelemetry_exporter_otlp_proto_http-1.38.0-py3-none-any.whl", hash = "sha256:84b937305edfc563f08ec69b9cb2298be8188371217e867c1854d77198d0825b"}, + {file = "opentelemetry_exporter_otlp_proto_http-1.38.0.tar.gz", hash = "sha256:f16bd44baf15cbe07633c5112ffc68229d0edbeac7b37610be0b2def4e21e90b"}, +] + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.59b0" +requires_python = ">=3.9" +summary = "Instrumentation Tools & Auto Instrumentation for OpenTelemetry Python" +groups = ["all", "chat"] +dependencies = [ + "opentelemetry-api~=1.4", + "opentelemetry-semantic-conventions==0.59b0", + "packaging>=18.0", + "wrapt<2.0.0,>=1.0.0", +] +files = [ + {file = "opentelemetry_instrumentation-0.59b0-py3-none-any.whl", hash = "sha256:44082cc8fe56b0186e87ee8f7c17c327c4c2ce93bdbe86496e600985d74368ee"}, + {file = "opentelemetry_instrumentation-0.59b0.tar.gz", hash = "sha256:6010f0faaacdaf7c4dff8aac84e226d23437b331dcda7e70367f6d73a7db1adc"}, +] + +[[package]] +name = "opentelemetry-instrumentation-asgi" +version = "0.59b0" +requires_python = ">=3.9" +summary = "ASGI instrumentation for OpenTelemetry" +groups = ["all", "chat"] +dependencies = [ + "asgiref~=3.0", + "opentelemetry-api~=1.12", + "opentelemetry-instrumentation==0.59b0", + "opentelemetry-semantic-conventions==0.59b0", + "opentelemetry-util-http==0.59b0", +] +files = [ + {file = "opentelemetry_instrumentation_asgi-0.59b0-py3-none-any.whl", hash = "sha256:ba9703e09d2c33c52fa798171f344c8123488fcd45017887981df088452d3c53"}, + {file = "opentelemetry_instrumentation_asgi-0.59b0.tar.gz", hash = "sha256:2509d6fe9fd829399ce3536e3a00426c7e3aa359fc1ed9ceee1628b56da40e7a"}, +] + +[[package]] +name = "opentelemetry-instrumentation-fastapi" +version = "0.59b0" +requires_python = ">=3.9" +summary = "OpenTelemetry FastAPI Instrumentation" +groups = ["all", "chat"] +dependencies = [ + "opentelemetry-api~=1.12", + "opentelemetry-instrumentation-asgi==0.59b0", + "opentelemetry-instrumentation==0.59b0", + "opentelemetry-semantic-conventions==0.59b0", + "opentelemetry-util-http==0.59b0", +] +files = [ + {file = "opentelemetry_instrumentation_fastapi-0.59b0-py3-none-any.whl", hash = "sha256:0d8d00ff7d25cca40a4b2356d1d40a8f001e0668f60c102f5aa6bb721d660c4f"}, + {file = "opentelemetry_instrumentation_fastapi-0.59b0.tar.gz", hash = "sha256:e8fe620cfcca96a7d634003df1bc36a42369dedcdd6893e13fb5903aeeb89b2b"}, +] + +[[package]] +name = "opentelemetry-instrumentation-httpx" +version = "0.59b0" +requires_python = ">=3.9" +summary = "OpenTelemetry HTTPX Instrumentation" +groups = ["all", "chat"] +dependencies = [ + "opentelemetry-api~=1.12", + "opentelemetry-instrumentation==0.59b0", + "opentelemetry-semantic-conventions==0.59b0", + "opentelemetry-util-http==0.59b0", + "wrapt<2.0.0,>=1.0.0", +] +files = [ + {file = "opentelemetry_instrumentation_httpx-0.59b0-py3-none-any.whl", hash = "sha256:7dc9f66aef4ca3904d877f459a70c78eafd06131dc64d713b9b1b5a7d0a48f05"}, + {file = "opentelemetry_instrumentation_httpx-0.59b0.tar.gz", hash = "sha256:a1cb9b89d9f05a82701cc9ab9cfa3db54fd76932489449778b350bc1b9f0e872"}, +] + +[[package]] +name = "opentelemetry-instrumentation-sqlalchemy" +version = "0.59b0" +requires_python = ">=3.9" +summary = "OpenTelemetry SQLAlchemy instrumentation" +groups = ["all", "chat"] +dependencies = [ + "opentelemetry-api~=1.12", + "opentelemetry-instrumentation==0.59b0", + "opentelemetry-semantic-conventions==0.59b0", + "packaging>=21.0", + "wrapt>=1.11.2", +] +files = [ + {file = "opentelemetry_instrumentation_sqlalchemy-0.59b0-py3-none-any.whl", hash = "sha256:4ef150c49b6d1a8a7328f9d23ff40c285a245b88b0875ed2e5d277a40aa921c8"}, + {file = "opentelemetry_instrumentation_sqlalchemy-0.59b0.tar.gz", hash = "sha256:7647b1e63497deebd41f9525c414699e0d49f19efcadc8a0642b715897f62d32"}, +] + +[[package]] +name = "opentelemetry-proto" +version = "1.38.0" +requires_python = ">=3.9" +summary = "OpenTelemetry Python Proto" +groups = ["all", "chat"] +dependencies = [ + "protobuf<7.0,>=5.0", +] +files = [ + {file = "opentelemetry_proto-1.38.0-py3-none-any.whl", hash = "sha256:b6ebe54d3217c42e45462e2a1ae28c3e2bf2ec5a5645236a490f55f45f1a0a18"}, + {file = "opentelemetry_proto-1.38.0.tar.gz", hash = "sha256:88b161e89d9d372ce723da289b7da74c3a8354a8e5359992be813942969ed468"}, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.38.0" +requires_python = ">=3.9" +summary = "OpenTelemetry Python SDK" +groups = ["all", "chat"] +dependencies = [ + "opentelemetry-api==1.38.0", + "opentelemetry-semantic-conventions==0.59b0", + "typing-extensions>=4.5.0", +] +files = [ + {file = "opentelemetry_sdk-1.38.0-py3-none-any.whl", hash = "sha256:1c66af6564ecc1553d72d811a01df063ff097cdc82ce188da9951f93b8d10f6b"}, + {file = "opentelemetry_sdk-1.38.0.tar.gz", hash = "sha256:93df5d4d871ed09cb4272305be4d996236eedb232253e3ab864c8620f051cebe"}, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.59b0" +requires_python = ">=3.9" +summary = "OpenTelemetry Semantic Conventions" +groups = ["all", "chat"] +dependencies = [ + "opentelemetry-api==1.38.0", + "typing-extensions>=4.5.0", +] +files = [ + {file = "opentelemetry_semantic_conventions-0.59b0-py3-none-any.whl", hash = "sha256:35d3b8833ef97d614136e253c1da9342b4c3c083bbaf29ce31d572a1c3825eed"}, + {file = "opentelemetry_semantic_conventions-0.59b0.tar.gz", hash = "sha256:7a6db3f30d70202d5bf9fa4b69bc866ca6a30437287de6c510fb594878aed6b0"}, +] + +[[package]] +name = "opentelemetry-util-http" +version = "0.59b0" +requires_python = ">=3.9" +summary = "Web util for OpenTelemetry" +groups = ["all", "chat"] +files = [ + {file = "opentelemetry_util_http-0.59b0-py3-none-any.whl", hash = "sha256:6d036a07563bce87bf521839c0671b507a02a0d39d7ea61b88efa14c6e25355d"}, + {file = "opentelemetry_util_http-0.59b0.tar.gz", hash = "sha256:ae66ee91be31938d832f3b4bc4eb8a911f6eddd38969c4a871b1230db2a0a560"}, +] + [[package]] name = "packaging" version = "25.0" @@ -2879,7 +3102,7 @@ name = "protobuf" version = "6.32.1" requires_python = ">=3.9" summary = "" -groups = ["default", "all", "dev", "vertex"] +groups = ["default", "all", "chat", "dev", "vertex"] files = [ {file = "protobuf-6.32.1-cp310-abi3-win32.whl", hash = "sha256:a8a32a84bc9f2aad712041b8b366190f71dde248926da517bde9e832e4412085"}, {file = "protobuf-6.32.1-cp310-abi3-win_amd64.whl", hash = "sha256:b00a7d8c25fa471f16bc8153d0e53d6c9e827f0953f3c09aaa4331c718cae5e1"}, @@ -3528,7 +3751,7 @@ name = "requests" version = "2.32.5" requires_python = ">=3.9" summary = "Python HTTP for Humans." -groups = ["default", "all", "vertex"] +groups = ["default", "all", "chat", "vertex"] dependencies = [ "certifi>=2017.4.17", "charset-normalizer<4,>=2", @@ -4278,7 +4501,7 @@ name = "urllib3" version = "2.5.0" requires_python = ">=3.9" summary = "HTTP library with thread-safe connection pooling, file post, and more." -groups = ["default", "all", "bedrock", "dev", "vertex"] +groups = ["default", "all", "bedrock", "chat", "dev", "vertex"] files = [ {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, @@ -4387,6 +4610,57 @@ files = [ {file = "winregistry-2.1.1.tar.gz", hash = "sha256:8233c4261a9d937cd8f0670da0d1e61fd7b86712c39b1af08cb83e91316195a7"}, ] +[[package]] +name = "wrapt" +version = "1.17.3" +requires_python = ">=3.8" +summary = "Module for decorators, wrappers and monkey patching." +groups = ["all", "chat"] +files = [ + {file = "wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04"}, + {file = "wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2"}, + {file = "wrapt-1.17.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c"}, + {file = "wrapt-1.17.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775"}, + {file = "wrapt-1.17.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd"}, + {file = "wrapt-1.17.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05"}, + {file = "wrapt-1.17.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418"}, + {file = "wrapt-1.17.3-cp310-cp310-win32.whl", hash = "sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390"}, + {file = "wrapt-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6"}, + {file = "wrapt-1.17.3-cp310-cp310-win_arm64.whl", hash = "sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18"}, + {file = "wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7"}, + {file = "wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85"}, + {file = "wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f"}, + {file = "wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311"}, + {file = "wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1"}, + {file = "wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5"}, + {file = "wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2"}, + {file = "wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89"}, + {file = "wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77"}, + {file = "wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a"}, + {file = "wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0"}, + {file = "wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba"}, + {file = "wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd"}, + {file = "wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828"}, + {file = "wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9"}, + {file = "wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396"}, + {file = "wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc"}, + {file = "wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe"}, + {file = "wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c"}, + {file = "wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6"}, + {file = "wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0"}, + {file = "wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77"}, + {file = "wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7"}, + {file = "wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277"}, + {file = "wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d"}, + {file = "wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa"}, + {file = "wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050"}, + {file = "wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8"}, + {file = "wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb"}, + {file = "wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16"}, + {file = "wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22"}, + {file = "wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0"}, +] + [[package]] name = "xlrd" version = "2.0.2" @@ -4397,3 +4671,14 @@ files = [ {file = "xlrd-2.0.2-py2.py3-none-any.whl", hash = "sha256:ea762c3d29f4cca48d82df517b6d89fbce4db3107f9d78713e48cd321d5c9aa9"}, {file = "xlrd-2.0.2.tar.gz", hash = "sha256:08b5e25de58f21ce71dc7db3b3b8106c1fa776f3024c54e45b45b374e89234c9"}, ] + +[[package]] +name = "zipp" +version = "3.23.0" +requires_python = ">=3.9" +summary = "Backport of pathlib-compatible object wrapper for zip files" +groups = ["all", "chat"] +files = [ + {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, + {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, +] diff --git a/pyproject.toml b/pyproject.toml index 0ec4a063..6a21051f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -231,6 +231,12 @@ chat = [ "starlette-context>=0.4.0", "sqlalchemy>=2.0.43", "alembic>=1.16.5", + "opentelemetry-api>=1.38.0", + "opentelemetry-sdk>=1.38.0", + "opentelemetry-instrumentation-fastapi>=0.59b0", + "opentelemetry-exporter-otlp-proto-http>=1.38.0", + "opentelemetry-instrumentation-httpx>=0.59b0", + "opentelemetry-instrumentation-sqlalchemy>=0.59b0", ] pynput = [ "mss>=10.0.0", diff --git a/src/askui/chat/__main__.py b/src/askui/chat/__main__.py index 0e7d7772..2e72a5b5 100644 --- a/src/askui/chat/__main__.py +++ b/src/askui/chat/__main__.py @@ -3,10 +3,14 @@ from askui.chat.api.app import app from askui.chat.api.dependencies import get_settings from askui.chat.api.telemetry.integrations.fastapi import instrument +from askui.telemetry.otel import setup_opentelemetry_tracing if __name__ == "__main__": settings = get_settings() instrument(app, settings.telemetry) + if settings.otel.enabled: + setup_opentelemetry_tracing(app, settings.otel) + uvicorn.run( app, host=settings.host, diff --git a/src/askui/chat/api/settings.py b/src/askui/chat/api/settings.py index 72e9b6c9..093d54cc 100644 --- a/src/askui/chat/api/settings.py +++ b/src/askui/chat/api/settings.py @@ -7,6 +7,7 @@ from askui.chat.api.mcp_configs.models import McpConfig, RemoteMCPServer from askui.chat.api.telemetry.integrations.fastapi.settings import TelemetrySettings from askui.chat.api.telemetry.logs.settings import LogFilter, LogSettings +from askui.telemetry.otel import OtelSettings from askui.utils.datetime_utils import now @@ -116,3 +117,7 @@ class Settings(BaseSettings): ), ), ) + otel: OtelSettings = Field( + default_factory=OtelSettings, + description="OpenTelemetry configuration settings", + ) diff --git a/src/askui/telemetry/otel.py b/src/askui/telemetry/otel.py new file mode 100644 index 00000000..563df252 --- /dev/null +++ b/src/askui/telemetry/otel.py @@ -0,0 +1,75 @@ +from fastapi import FastAPI +from opentelemetry import trace +from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter +from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor +from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor +from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import BatchSpanProcessor +from pydantic import BaseModel, Field, SecretStr, model_validator +from typing_extensions import Self + +from askui import __version__ + + +class OtelSettings(BaseModel): + """Settings for otel configuration""" + + enabled: bool = Field(default=False) + secret: SecretStr | None = Field( + default=None, + description="Secret for OTLP authentication. Required when enabled=True.", + ) + service_name: str = Field(default="chat-api") + service_version: str = Field(default=__version__) + endpoint: str | None = Field( + default=None, + description="OTLP endpoint URL.", + ) + cluster_name: str = Field(default="askui-dev") + + @model_validator(mode="after") + def validate_secret_when_enabled(self) -> Self: + """Ensure secret is provided when OpenTelemetry is enabled.""" + if self.enabled and self.secret is None: + error_msg = "Secret is required when OpenTelemetry is enabled" + raise ValueError(error_msg) + return self + + +def setup_opentelemetry_tracing(app: FastAPI, settings: OtelSettings) -> None: + """ + Set up OpenTelemetry tracing for the FastAPI application. + + Args: + app (FastAPI): The FastAPI application to instrument for tracing. + settings (OtelSettings): OpenTelemetry configuration settings containing + endpoint, secret, service name, and version. + + Returns: + None + + """ + resource = Resource.create( + { + "service.name": settings.service_name, + "service.version": settings.service_version, + "cluster.name": settings.cluster_name, + } + ) + provider = TracerProvider(resource=resource) + + otlp_exporter = OTLPSpanExporter( + endpoint=settings.endpoint, + headers={"authorization": f"Basic {settings.secret.get_secret_value()}"}, # type: ignore[union-attr] + ) + + span_processor = BatchSpanProcessor(otlp_exporter) + provider.add_span_processor(span_processor) + + trace.set_tracer_provider(provider) + + FastAPIInstrumentor.instrument_app(app, excluded_urls="health") + HTTPXClientInstrumentor().instrument() + SQLAlchemyInstrumentor().instrument() From 34de818ca535b76c4fd691bfab60ac5fe22ecac8 Mon Sep 17 00:00:00 2001 From: onur-askui <210008309+onur-askui@users.noreply.github.com> Date: Wed, 3 Dec 2025 11:09:04 +0100 Subject: [PATCH 2/2] docs: otel tracing --- docs/tracing.md | 97 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 docs/tracing.md diff --git a/docs/tracing.md b/docs/tracing.md new file mode 100644 index 00000000..dc033777 --- /dev/null +++ b/docs/tracing.md @@ -0,0 +1,97 @@ +# OpenTelemetry Traces + +Traces give us the big picture of what happens when a request is made to an application and are essential to understanding the full flow a user request takes in our services. + +## Table of Contents + +- [Core Concepts](#core-concepts) +- [Components](#components) +- [Configuration](#configuration) +- [Usage](#usage) + - [Create a new span](#create-a-new-span) + - [Context Manager](#context-manager) + - [Decorator](#decorator) + +## Core Concepts +***Trace:*** The complete end-to-end journey of a request. +***Span:*** A single unit of work within a trace (e.g., an HTTP request, a function call, a DB query). +***Tracer:*** The object used to create spans. +***Processor***: Determines how completed spans are handled and queued before being sent to an Exporter. We typically use a BatchSpanProcessor to efficiently queue and send spans in batches. +***Exporter***: Exporters are responsible for formatting and sending the collected tracing data to a backend analysis system (like Grafana/Tempo) + +## Components + +There are different types of components we are using. + +Foundational components to work with OTEL like: +- opentelemetry-api +- opentelemetry-sdk + +Exporters to send data to Grafana: +- opentelemetry-exporter-otlp-proto-http + +Instrumentors for automatic instrumentation of certain libraries: +- opentelemetry-instrumentation-fastapi +- opentelemetry-instrumentation-httpx +- opentelemetry-instrumentation-sqlalchemy + +Automatic instrumentors (like opentelemetry-instrumentation-fastapi) handle context propagation automatically, which is how a single request/trace ID flows across multiple services. + +## Configuration + +This feature is entirely behind a feature flag and controlled via env variables see [.env.temp](https://github.com/askui/vision-agent/blob/main/.env.template). +To enable tracing we need to set the following flags: +- `ASKUI__CHAT_API__OTEL__ENABLED=True` +- `ASKUI__CHAT_API__OTEL__ENDPOINT=http://localhost/v1/traces` +- `ASKUI__CHAT_API__OTEL__SECRET=***` + +For further configuration options please refer to [OtelSettings](https://github.com/askui/vision-agent/blob/feat/otel-tracing/src/askui/telemetry/otel.py). + + +## Usage + +### Create a new span + +#### Context Manager +```python +def truncate(input): + with tracer.start_as_current_span("truncate") as span: + # set metadata + span.set_attribute("truncation.length", len(input)) + + return input[:10] + +``` + +#### Decorator +```python +@tracer.start_as_current_span("process-request") +def process_request(user_id): + # The span is already active here. We can get the current span: + current_span = trace.get_current_span() + current_span.set_attribute("user.id", user_id) + + # You can call another function which is also instrumented (e.g., the one + # using the context manager) to create a nested span automatically. + data = "super long string" + result = truncate(data) + + current_span.set_attribute("final.result", result) + return f"Processed for user {user_id} with result {result}" + +# Call the function +process_request(42) + +``` + +### Getting and modifying a span + +```python + +from opentelemetry import trace + +current_span = trace.get_current_span() +current_span.set_attribute("job.id", "123") + +``` +