diff --git a/poetry.lock b/poetry.lock index b09a72d70..45b3d9f80 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. [[package]] name = "absl-py" @@ -64,6 +64,26 @@ doc = ["Sphinx (>=8.2,<9.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""] trio = ["trio (>=0.26.1)"] +[[package]] +name = "asgiref" +version = "3.12.1" +description = "ASGI specs, helper code, and adapters" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "extra == \"otel\" or extra == \"all\"" +files = [ + {file = "asgiref-3.12.1-py3-none-any.whl", hash = "sha256:fe386d1c2bff7259ea95929266d12a8cf9a8b5a1c2598402967d8792e7a7c094"}, + {file = "asgiref-3.12.1.tar.gz", hash = "sha256:59dcb51c272ad209d59bed5708a64a333083e86017d7fcdd67498eeab7784340"}, +] + +[package.dependencies] +typing_extensions = {version = ">=4", markers = "python_version < \"3.11\""} + +[package.extras] +mypy = ["mypy (>=1.14.0)"] +tests = ["pytest", "pytest-asyncio"] + [[package]] name = "async-timeout" version = "5.0.1" @@ -1289,6 +1309,25 @@ test-downstream = ["aiobotocore (>=2.5.4,<3.0.0)", "dask[dataframe,test]", "moto test-full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "cloudpickle", "dask", "distributed", "dropbox", "dropboxdrivefs", "fastparquet", "fusepy", "gcsfs", "jinja2", "kerchunk", "libarchive-c", "lz4", "notebook", "numpy", "ocifs", "pandas", "panel", "paramiko", "pyarrow", "pyarrow (>=1)", "pyftpdlib", "pygit2", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "python-snappy", "requests", "smbprotocol", "tqdm", "urllib3", "zarr", "zstandard ; python_version < \"3.14\""] tqdm = ["tqdm"] +[[package]] +name = "googleapis-common-protos" +version = "1.75.0" +description = "Common protobufs used in Google APIs" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"otel\" or extra == \"all\"" +files = [ + {file = "googleapis_common_protos-1.75.0-py3-none-any.whl", hash = "sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed"}, + {file = "googleapis_common_protos-1.75.0.tar.gz", hash = "sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd"}, +] + +[package.dependencies] +protobuf = ">=4.25.8,<8.0.0" + +[package.extras] +grpc = ["grpcio (>=1.44.0,<2.0.0)"] + [[package]] name = "greenlet" version = "3.2.3" @@ -1418,7 +1457,7 @@ files = [ {file = "grpcio-1.73.1-cp39-cp39-win_amd64.whl", hash = "sha256:42f0660bce31b745eb9d23f094a332d31f210dcadd0fc8e5be7e4c62a87ce86b"}, {file = "grpcio-1.73.1.tar.gz", hash = "sha256:7fce2cd1c0c1116cf3850564ebfc3264fba75d3c74a7414373f1238ea365ef87"}, ] -markers = {main = "extra == \"pref-mem\" or extra == \"all\""} +markers = {main = "extra == \"otel\" or extra == \"all\" or extra == \"pref-mem\""} [package.extras] protobuf = ["grpcio-tools (>=1.73.1)"] @@ -1701,6 +1740,31 @@ files = [ [package.extras] all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] +[[package]] +name = "importlib-metadata" +version = "8.7.1" +description = "Read metadata from Python packages" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.14\" and (extra == \"otel\" or extra == \"all\")" +files = [ + {file = "importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151"}, + {file = "importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb"}, +] + +[package.dependencies] +zipp = ">=3.20" + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=3.4)"] +perf = ["ipython"] +test = ["flufl.flake8", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +type = ["mypy (<1.19) ; platform_python_implementation == \"PyPy\"", "pytest-mypy (>=1.0.1)"] + [[package]] name = "iniconfig" version = "2.1.0" @@ -1897,7 +1961,7 @@ files = [ [package.dependencies] attrs = ">=22.2.0" -jsonschema-specifications = ">=2023.03.6" +jsonschema-specifications = ">=2023.3.6" referencing = ">=0.28.4" rpds-py = ">=0.7.1" @@ -2672,6 +2736,7 @@ files = [ {file = "nltk-3.9.1-py3-none-any.whl", hash = "sha256:4fa26829c5b00715afe3061398a8989dc643b92ce7dd93fb4585a70930d168a1"}, {file = "nltk-3.9.1.tar.gz", hash = "sha256:87d127bd3de4bd89a4f81265e5fa59cb1b199b27440175370f7417d2bc7ae868"}, ] +markers = {main = "extra == \"all\""} [package.dependencies] click = "*" @@ -3161,6 +3226,435 @@ files = [ [package.dependencies] et-xmlfile = "*" +[[package]] +name = "opentelemetry-api" +version = "1.39.1" +description = "OpenTelemetry Python API" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.14\" and (extra == \"otel\" or extra == \"all\")" +files = [ + {file = "opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950"}, + {file = "opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c"}, +] + +[package.dependencies] +importlib-metadata = ">=6.0,<8.8.0" +typing-extensions = ">=4.5.0" + +[[package]] +name = "opentelemetry-api" +version = "1.44.0" +description = "OpenTelemetry Python API" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "python_version < \"3.14\" and (extra == \"otel\" or extra == \"all\")" +files = [ + {file = "opentelemetry_api-1.44.0-py3-none-any.whl", hash = "sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef"}, + {file = "opentelemetry_api-1.44.0.tar.gz", hash = "sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a"}, +] + +[package.dependencies] +typing-extensions = ">=4.5.0" + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.39.1" +description = "OpenTelemetry Protobuf encoding" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.14\" and (extra == \"otel\" or extra == \"all\")" +files = [ + {file = "opentelemetry_exporter_otlp_proto_common-1.39.1-py3-none-any.whl", hash = "sha256:08f8a5862d64cc3435105686d0216c1365dc5701f86844a8cd56597d0c764fde"}, + {file = "opentelemetry_exporter_otlp_proto_common-1.39.1.tar.gz", hash = "sha256:763370d4737a59741c89a67b50f9e39271639ee4afc999dadfe768541c027464"}, +] + +[package.dependencies] +opentelemetry-proto = "1.39.1" + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.44.0" +description = "OpenTelemetry Protobuf encoding" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "python_version < \"3.14\" and (extra == \"otel\" or extra == \"all\")" +files = [ + {file = "opentelemetry_exporter_otlp_proto_common-1.44.0-py3-none-any.whl", hash = "sha256:9a9fe61bba73d802904bc989f1d6b4a7b1ee40f06c40e98d6f85af65aaebb694"}, + {file = "opentelemetry_exporter_otlp_proto_common-1.44.0.tar.gz", hash = "sha256:dc87a5a5bc58f149a56d1547e4691588fa12994cdc3bc039a694ccb3375862ac"}, +] + +[package.dependencies] +opentelemetry-proto = "1.44.0" + +[[package]] +name = "opentelemetry-exporter-otlp-proto-grpc" +version = "1.39.1" +description = "OpenTelemetry Collector Protobuf over gRPC Exporter" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.14\" and (extra == \"otel\" or extra == \"all\")" +files = [ + {file = "opentelemetry_exporter_otlp_proto_grpc-1.39.1-py3-none-any.whl", hash = "sha256:fa1c136a05c7e9b4c09f739469cbdb927ea20b34088ab1d959a849b5cc589c18"}, + {file = "opentelemetry_exporter_otlp_proto_grpc-1.39.1.tar.gz", hash = "sha256:772eb1c9287485d625e4dbe9c879898e5253fea111d9181140f51291b5fec3ad"}, +] + +[package.dependencies] +googleapis-common-protos = ">=1.57,<2.0" +grpcio = {version = ">=1.66.2,<2.0.0", markers = "python_version >= \"3.13\""} +opentelemetry-api = ">=1.15,<2.0" +opentelemetry-exporter-otlp-proto-common = "1.39.1" +opentelemetry-proto = "1.39.1" +opentelemetry-sdk = ">=1.39.1,<1.40.0" +typing-extensions = ">=4.6.0" + +[package.extras] +gcp-auth = ["opentelemetry-exporter-credential-provider-gcp (>=0.59b0)"] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-grpc" +version = "1.44.0" +description = "OpenTelemetry Collector Protobuf over gRPC Exporter" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "python_version < \"3.14\" and (extra == \"otel\" or extra == \"all\")" +files = [ + {file = "opentelemetry_exporter_otlp_proto_grpc-1.44.0-py3-none-any.whl", hash = "sha256:6a1a645ea182a2f59440c51fa8301d309f3324a8f9d65f8395584b064b67ee4e"}, + {file = "opentelemetry_exporter_otlp_proto_grpc-1.44.0.tar.gz", hash = "sha256:40d1ae9e03fcc36de3cbac610cc99f35894938bff9cfd90fc4ec68bd85448463"}, +] + +[package.dependencies] +googleapis-common-protos = ">=1.57,<2.0" +grpcio = [ + {version = ">=1.63.2,<2.0.0", markers = "python_version < \"3.13\""}, + {version = ">=1.66.2,<2.0.0", markers = "python_version == \"3.13\""}, +] +opentelemetry-api = ">=1.15,<2.0" +opentelemetry-exporter-otlp-proto-common = "1.44.0" +opentelemetry-proto = "1.44.0" +opentelemetry-sdk = ">=1.44.0,<1.45.0" +typing-extensions = ">=4.6.0" + +[package.extras] +gcp-auth = ["opentelemetry-exporter-credential-provider-gcp (>=0.59b0)"] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.39.1" +description = "OpenTelemetry Collector Protobuf over HTTP Exporter" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.14\" and (extra == \"otel\" or extra == \"all\")" +files = [ + {file = "opentelemetry_exporter_otlp_proto_http-1.39.1-py3-none-any.whl", hash = "sha256:d9f5207183dd752a412c4cd564ca8875ececba13be6e9c6c370ffb752fd59985"}, + {file = "opentelemetry_exporter_otlp_proto_http-1.39.1.tar.gz", hash = "sha256:31bdab9745c709ce90a49a0624c2bd445d31a28ba34275951a6a362d16a0b9cb"}, +] + +[package.dependencies] +googleapis-common-protos = ">=1.52,<2.0" +opentelemetry-api = ">=1.15,<2.0" +opentelemetry-exporter-otlp-proto-common = "1.39.1" +opentelemetry-proto = "1.39.1" +opentelemetry-sdk = ">=1.39.1,<1.40.0" +requests = ">=2.7,<3.0" +typing-extensions = ">=4.5.0" + +[package.extras] +gcp-auth = ["opentelemetry-exporter-credential-provider-gcp (>=0.59b0)"] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.44.0" +description = "OpenTelemetry Collector Protobuf over HTTP Exporter" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "python_version < \"3.14\" and (extra == \"otel\" or extra == \"all\")" +files = [ + {file = "opentelemetry_exporter_otlp_proto_http-1.44.0-py3-none-any.whl", hash = "sha256:838592fce774c1c8bb7b9a0a7facbfa82e17be5a8a4e94cef10cb84ae026bae3"}, + {file = "opentelemetry_exporter_otlp_proto_http-1.44.0.tar.gz", hash = "sha256:c633d7270ad6b57cd4cfbe8b0007a9e2e7c0cb50bd6c50fe2a7b245f721a09d8"}, +] + +[package.dependencies] +googleapis-common-protos = ">=1.52,<2.0" +opentelemetry-api = ">=1.15,<2.0" +opentelemetry-exporter-otlp-proto-common = "1.44.0" +opentelemetry-proto = "1.44.0" +opentelemetry-sdk = ">=1.44.0,<1.45.0" +requests = ">=2.7,<3.0" +typing-extensions = ">=4.5.0" + +[package.extras] +gcp-auth = ["opentelemetry-exporter-credential-provider-gcp (>=0.59b0)"] + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.60b1" +description = "Instrumentation Tools & Auto Instrumentation for OpenTelemetry Python" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.14\" and (extra == \"otel\" or extra == \"all\")" +files = [ + {file = "opentelemetry_instrumentation-0.60b1-py3-none-any.whl", hash = "sha256:04480db952b48fb1ed0073f822f0ee26012b7be7c3eac1a3793122737c78632d"}, + {file = "opentelemetry_instrumentation-0.60b1.tar.gz", hash = "sha256:57ddc7974c6eb35865af0426d1a17132b88b2ed8586897fee187fd5b8944bd6a"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.4,<2.0" +opentelemetry-semantic-conventions = "0.60b1" +packaging = ">=18.0" +wrapt = ">=1.0.0,<2.0.0" + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.65b0" +description = "Instrumentation Tools & Auto Instrumentation for OpenTelemetry Python" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "python_version < \"3.14\" and (extra == \"otel\" or extra == \"all\")" +files = [ + {file = "opentelemetry_instrumentation-0.65b0-py3-none-any.whl", hash = "sha256:ea967a72b9939b5fcfdad572753b4306c59dcb99e3f382d95dae04286805e137"}, + {file = "opentelemetry_instrumentation-0.65b0.tar.gz", hash = "sha256:071d9d9eced9bd6460444ec3b0c77229870ed05a881c22c84fdede58e4eed09b"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.4,<2.0" +opentelemetry-semantic-conventions = "0.65b0" +packaging = ">=18.0" +wrapt = ">=1.0.0,<3.0.0" + +[[package]] +name = "opentelemetry-instrumentation-asgi" +version = "0.60b1" +description = "ASGI instrumentation for OpenTelemetry" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.14\" and (extra == \"otel\" or extra == \"all\")" +files = [ + {file = "opentelemetry_instrumentation_asgi-0.60b1-py3-none-any.whl", hash = "sha256:d48def2dbed10294c99cfcf41ebbd0c414d390a11773a41f472d20000fcddc25"}, + {file = "opentelemetry_instrumentation_asgi-0.60b1.tar.gz", hash = "sha256:16bfbe595cd24cda309a957456d0fc2523f41bc7b076d1f2d7e98a1ad9876d6f"}, +] + +[package.dependencies] +asgiref = ">=3.0,<4.0" +opentelemetry-api = ">=1.12,<2.0" +opentelemetry-instrumentation = "0.60b1" +opentelemetry-semantic-conventions = "0.60b1" +opentelemetry-util-http = "0.60b1" + +[package.extras] +instruments = ["asgiref (>=3.0,<4.0)"] + +[[package]] +name = "opentelemetry-instrumentation-asgi" +version = "0.65b0" +description = "ASGI instrumentation for OpenTelemetry" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "python_version < \"3.14\" and (extra == \"otel\" or extra == \"all\")" +files = [ + {file = "opentelemetry_instrumentation_asgi-0.65b0-py3-none-any.whl", hash = "sha256:3a845a8ebd1c4ef0d8263401e6545f5b219b2feee612090d50f578a87e71fd65"}, + {file = "opentelemetry_instrumentation_asgi-0.65b0.tar.gz", hash = "sha256:892bca67c56522ffa85a8a83cf934d7b50b3be2132e45cbee705825f0a5ba426"}, +] + +[package.dependencies] +asgiref = ">=3.0,<4.0" +opentelemetry-api = ">=1.12,<2.0" +opentelemetry-instrumentation = "0.65b0" +opentelemetry-semantic-conventions = "0.65b0" +opentelemetry-util-http = "0.65b0" + +[package.extras] +instruments = ["asgiref (>=3.0,<4.0)"] + +[[package]] +name = "opentelemetry-instrumentation-fastapi" +version = "0.60b1" +description = "OpenTelemetry FastAPI Instrumentation" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.14\" and (extra == \"otel\" or extra == \"all\")" +files = [ + {file = "opentelemetry_instrumentation_fastapi-0.60b1-py3-none-any.whl", hash = "sha256:af94b7a239ad1085fc3a820ecf069f67f579d7faf4c085aaa7bd9b64eafc8eaf"}, + {file = "opentelemetry_instrumentation_fastapi-0.60b1.tar.gz", hash = "sha256:de608955f7ff8eecf35d056578346a5365015fd7d8623df9b1f08d1c74769c01"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.12,<2.0" +opentelemetry-instrumentation = "0.60b1" +opentelemetry-instrumentation-asgi = "0.60b1" +opentelemetry-semantic-conventions = "0.60b1" +opentelemetry-util-http = "0.60b1" + +[package.extras] +instruments = ["fastapi (>=0.92,<1.0)"] + +[[package]] +name = "opentelemetry-instrumentation-fastapi" +version = "0.65b0" +description = "OpenTelemetry FastAPI Instrumentation" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "python_version < \"3.14\" and (extra == \"otel\" or extra == \"all\")" +files = [ + {file = "opentelemetry_instrumentation_fastapi-0.65b0-py3-none-any.whl", hash = "sha256:cda2610a0ec1b22d19886f33e4d861e9f5dbb886aeaa3a1263b47aff82c36943"}, + {file = "opentelemetry_instrumentation_fastapi-0.65b0.tar.gz", hash = "sha256:10a3a95486036230413a58fe4fdf4a83fa6bba46918407e527476994bd92bd97"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.12,<2.0" +opentelemetry-instrumentation = "0.65b0" +opentelemetry-instrumentation-asgi = "0.65b0" +opentelemetry-semantic-conventions = "0.65b0" +opentelemetry-util-http = "0.65b0" + +[package.extras] +instruments = ["fastapi (>=0.92,<1.0)"] + +[[package]] +name = "opentelemetry-proto" +version = "1.39.1" +description = "OpenTelemetry Python Proto" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.14\" and (extra == \"otel\" or extra == \"all\")" +files = [ + {file = "opentelemetry_proto-1.39.1-py3-none-any.whl", hash = "sha256:22cdc78efd3b3765d09e68bfbd010d4fc254c9818afd0b6b423387d9dee46007"}, + {file = "opentelemetry_proto-1.39.1.tar.gz", hash = "sha256:6c8e05144fc0d3ed4d22c2289c6b126e03bcd0e6a7da0f16cedd2e1c2772e2c8"}, +] + +[package.dependencies] +protobuf = ">=5.0,<7.0" + +[[package]] +name = "opentelemetry-proto" +version = "1.44.0" +description = "OpenTelemetry Python Proto" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "python_version < \"3.14\" and (extra == \"otel\" or extra == \"all\")" +files = [ + {file = "opentelemetry_proto-1.44.0-py3-none-any.whl", hash = "sha256:898b155a0e1557afd867478fb6158e8122a46329ca0bb8dc53cc55e98f017f56"}, + {file = "opentelemetry_proto-1.44.0.tar.gz", hash = "sha256:c547a79c2f8c0c515d31509154682e5921c7cfd5ca67b70e1f9266e2c3e103f3"}, +] + +[package.dependencies] +protobuf = ">=5.0,<8.0" + +[[package]] +name = "opentelemetry-sdk" +version = "1.39.1" +description = "OpenTelemetry Python SDK" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.14\" and (extra == \"otel\" or extra == \"all\")" +files = [ + {file = "opentelemetry_sdk-1.39.1-py3-none-any.whl", hash = "sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c"}, + {file = "opentelemetry_sdk-1.39.1.tar.gz", hash = "sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6"}, +] + +[package.dependencies] +opentelemetry-api = "1.39.1" +opentelemetry-semantic-conventions = "0.60b1" +typing-extensions = ">=4.5.0" + +[[package]] +name = "opentelemetry-sdk" +version = "1.44.0" +description = "OpenTelemetry Python SDK" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "python_version < \"3.14\" and (extra == \"otel\" or extra == \"all\")" +files = [ + {file = "opentelemetry_sdk-1.44.0-py3-none-any.whl", hash = "sha256:df081c4c6bcfdb1211e3e86140376792643128a25f8d72d1d27675936e7e96ad"}, + {file = "opentelemetry_sdk-1.44.0.tar.gz", hash = "sha256:cebe7f65dc12f26ead75c6064de12fd2a9052e5060c0272d402cfa203aae123b"}, +] + +[package.dependencies] +opentelemetry-api = "1.44.0" +opentelemetry-semantic-conventions = "0.65b0" +typing-extensions = ">=4.5.0" + +[package.extras] +file-configuration = ["opentelemetry-configuration (==0.65b0)"] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.60b1" +description = "OpenTelemetry Semantic Conventions" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.14\" and (extra == \"otel\" or extra == \"all\")" +files = [ + {file = "opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb"}, + {file = "opentelemetry_semantic_conventions-0.60b1.tar.gz", hash = "sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953"}, +] + +[package.dependencies] +opentelemetry-api = "1.39.1" +typing-extensions = ">=4.5.0" + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.65b0" +description = "OpenTelemetry Semantic Conventions" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "python_version < \"3.14\" and (extra == \"otel\" or extra == \"all\")" +files = [ + {file = "opentelemetry_semantic_conventions-0.65b0-py3-none-any.whl", hash = "sha256:1cacde7b0ad306f84c5ef08c3dbe1bbaf20165bba6f8bff43b670e555a086bcb"}, + {file = "opentelemetry_semantic_conventions-0.65b0.tar.gz", hash = "sha256:f9b2b81e9d5b64f11bc952075e7e9c7fb0aab075c7fd1c46d597f1b919852d60"}, +] + +[package.dependencies] +opentelemetry-api = "1.44.0" +typing-extensions = ">=4.5.0" + +[[package]] +name = "opentelemetry-util-http" +version = "0.60b1" +description = "Web util for OpenTelemetry" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.14\" and (extra == \"otel\" or extra == \"all\")" +files = [ + {file = "opentelemetry_util_http-0.60b1-py3-none-any.whl", hash = "sha256:66381ba28550c91bee14dcba8979ace443444af1ed609226634596b4b0faf199"}, + {file = "opentelemetry_util_http-0.60b1.tar.gz", hash = "sha256:0d97152ca8c8a41ced7172d29d3622a219317f74ae6bb3027cfbdcf22c3cc0d6"}, +] + +[[package]] +name = "opentelemetry-util-http" +version = "0.65b0" +description = "Web util for OpenTelemetry" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "python_version < \"3.14\" and (extra == \"otel\" or extra == \"all\")" +files = [ + {file = "opentelemetry_util_http-0.65b0-py3-none-any.whl", hash = "sha256:7553b606f963097cb190536dc30556cce85090692e471a422fff30ca29b04348"}, + {file = "opentelemetry_util_http-0.65b0.tar.gz", hash = "sha256:84f82d826978bba416ab453460ff6a7391cdc3534c93a786595e4068680016b7"}, +] + [[package]] name = "orjson" version = "3.11.0" @@ -3690,7 +4184,7 @@ files = [ {file = "protobuf-6.31.1-py3-none-any.whl", hash = "sha256:720a6c7e6b77288b85063569baae8536671b39f15cc22037ec7045658d80489e"}, {file = "protobuf-6.31.1.tar.gz", hash = "sha256:d8cac4c982f0b957a4dc73a80e2ea24fab08e679c0de9deb835f4a12d69aca9a"}, ] -markers = {main = "extra == \"mem-reader\" or extra == \"all\" or extra == \"pref-mem\""} +markers = {main = "extra == \"mem-reader\" or extra == \"all\" or extra == \"otel\" or extra == \"pref-mem\""} [[package]] name = "pycparser" @@ -6340,6 +6834,202 @@ files = [ {file = "websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee"}, ] +[[package]] +name = "wrapt" +version = "1.17.3" +description = "Module for decorators, wrappers and monkey patching." +optional = true +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version >= \"3.14\" and (extra == \"otel\" or extra == \"all\")" +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-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39"}, + {file = "wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235"}, + {file = "wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c"}, + {file = "wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b"}, + {file = "wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa"}, + {file = "wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7"}, + {file = "wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4"}, + {file = "wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10"}, + {file = "wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6"}, + {file = "wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58"}, + {file = "wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a"}, + {file = "wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067"}, + {file = "wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454"}, + {file = "wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e"}, + {file = "wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f"}, + {file = "wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056"}, + {file = "wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804"}, + {file = "wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977"}, + {file = "wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116"}, + {file = "wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6"}, + {file = "wrapt-1.17.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:70d86fa5197b8947a2fa70260b48e400bf2ccacdcab97bb7de47e3d1e6312225"}, + {file = "wrapt-1.17.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:df7d30371a2accfe4013e90445f6388c570f103d61019b6b7c57e0265250072a"}, + {file = "wrapt-1.17.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:caea3e9c79d5f0d2c6d9ab96111601797ea5da8e6d0723f77eabb0d4068d2b2f"}, + {file = "wrapt-1.17.3-cp38-cp38-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:758895b01d546812d1f42204bd443b8c433c44d090248bf22689df673ccafe00"}, + {file = "wrapt-1.17.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02b551d101f31694fc785e58e0720ef7d9a10c4e62c1c9358ce6f63f23e30a56"}, + {file = "wrapt-1.17.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:656873859b3b50eeebe6db8b1455e99d90c26ab058db8e427046dbc35c3140a5"}, + {file = "wrapt-1.17.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a9a2203361a6e6404f80b99234fe7fb37d1fc73487b5a78dc1aa5b97201e0f22"}, + {file = "wrapt-1.17.3-cp38-cp38-win32.whl", hash = "sha256:55cbbc356c2842f39bcc553cf695932e8b30e30e797f961860afb308e6b1bb7c"}, + {file = "wrapt-1.17.3-cp38-cp38-win_amd64.whl", hash = "sha256:ad85e269fe54d506b240d2d7b9f5f2057c2aa9a2ea5b32c66f8902f768117ed2"}, + {file = "wrapt-1.17.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:30ce38e66630599e1193798285706903110d4f057aab3168a34b7fdc85569afc"}, + {file = "wrapt-1.17.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:65d1d00fbfb3ea5f20add88bbc0f815150dbbde3b026e6c24759466c8b5a9ef9"}, + {file = "wrapt-1.17.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a7c06742645f914f26c7f1fa47b8bc4c91d222f76ee20116c43d5ef0912bba2d"}, + {file = "wrapt-1.17.3-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7e18f01b0c3e4a07fe6dfdb00e29049ba17eadbc5e7609a2a3a4af83ab7d710a"}, + {file = "wrapt-1.17.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f5f51a6466667a5a356e6381d362d259125b57f059103dd9fdc8c0cf1d14139"}, + {file = "wrapt-1.17.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:59923aa12d0157f6b82d686c3fd8e1166fa8cdfb3e17b42ce3b6147ff81528df"}, + {file = "wrapt-1.17.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:46acc57b331e0b3bcb3e1ca3b421d65637915cfcd65eb783cb2f78a511193f9b"}, + {file = "wrapt-1.17.3-cp39-cp39-win32.whl", hash = "sha256:3e62d15d3cfa26e3d0788094de7b64efa75f3a53875cdbccdf78547aed547a81"}, + {file = "wrapt-1.17.3-cp39-cp39-win_amd64.whl", hash = "sha256:1f23fa283f51c890eda8e34e4937079114c74b4c81d2b2f1f1d94948f5cc3d7f"}, + {file = "wrapt-1.17.3-cp39-cp39-win_arm64.whl", hash = "sha256:24c2ed34dc222ed754247a2702b1e1e89fdbaa4016f324b4b8f1a802d4ffe87f"}, + {file = "wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22"}, + {file = "wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0"}, +] + +[[package]] +name = "wrapt" +version = "2.3.0" +description = "Module for decorators, wrappers and monkey patching." +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version < \"3.14\" and (extra == \"otel\" or extra == \"all\")" +files = [ + {file = "wrapt-2.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0bb2797048db0956348cb3058c33bc4184614f13231389cfbccc16a5d32780a7"}, + {file = "wrapt-2.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce9f398f868d2b3b27aa2ea4de79645ef9077aeeac8dfc2814b0d542c6a2b87f"}, + {file = "wrapt-2.3.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ad71df7a04dd3497e9302e81f4a7c91bd401ea0e15a9df9029527900f94bee43"}, + {file = "wrapt-2.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc82c2ccc8e234c844f5303d9f2984b346dcdd53e94823ce8420d2c75b4b9023"}, + {file = "wrapt-2.3.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6e19531ae33c508cea7d84a7edfda01fa86e51b8d1a93a77712c55e6e469152"}, + {file = "wrapt-2.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:df4ce31150bcd5d9f36f816aac3010ab4f4bf8672ac1d3b0ac7d539ec61c7c02"}, + {file = "wrapt-2.3.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e2e692bc0d63f881cf7006730a56bd4e0c2fab5dc318466942805d692b166276"}, + {file = "wrapt-2.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c8388ba7faf5dbf9ee106bb70d66f257629b1bd98091123e19e8a4553a319199"}, + {file = "wrapt-2.3.0-cp310-cp310-win32.whl", hash = "sha256:e045ff75d7d94900fc32896ed93c45ce2d2cac28c9dead582ff9a5a49d446e35"}, + {file = "wrapt-2.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:b4fc96b159af0a3e0faa72475a69d66292bea72a5bed1e1aca1bffbddc3cb2b0"}, + {file = "wrapt-2.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:1236fa25173ca964c97422470482e9011b9e3c7ed0d75798b40b3da3b0e0e760"}, + {file = "wrapt-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5ab559e1b2551d23d54db2a0001c6d73bad022a254639561c5f6c382a9d6c2fe"}, + {file = "wrapt-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bff9a671bc00709cab5a7f745c592b5671873449db0ee2a569af994f16b29a4d"}, + {file = "wrapt-2.3.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fc648a335d7e01adb3640b25f02fd0ea05886cf04d0af7f4ee902bc7b5e466e8"}, + {file = "wrapt-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d0077f3d65541925fa83002f967b22ad6550d24813ac64cb905f717194128d9c"}, + {file = "wrapt-2.3.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9790ea25190a4e0fe4cdf4eeb868e9d75f8a024a70a5b6bf9c348a3a2b72e731"}, + {file = "wrapt-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:816877aa749253149f9ecfd2635d4d948ecfa338e1a0311d187b1acb1bb8a3eb"}, + {file = "wrapt-2.3.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3d1c2c1b808600d2ea808e6360910a60ed5f409a4011655e10f9164ba0a414a6"}, + {file = "wrapt-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5ba1e5e08ddc46130e9682b2c249f2d1dd39bda9106ed4bd401b7519f18f41bd"}, + {file = "wrapt-2.3.0-cp311-cp311-win32.whl", hash = "sha256:45c9279b373d15649dfa2c2077cb3408ea1a6d3125afbdab9d6b809a66f68e14"}, + {file = "wrapt-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:195b1842b4122fb54e3cd3dd5b2b4aa49302a5a61da901df0481f5c97aedde84"}, + {file = "wrapt-2.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:6db604ef0c67bdb2042ecdfd7b7f037cf09733557ca42360d1018285634f7b98"}, + {file = "wrapt-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0a45ffae742ce91a16e11cb6c7cd71e7f9994f3cbd283b962ab093f5c6dcf525"}, + {file = "wrapt-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:69e477046f2237ef0bc6547544ee73008dc764ca26eff44f09e976d221b34d5d"}, + {file = "wrapt-2.3.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d221a6e6ddd302b8397433184e96b59f259f50024b854db1c411a881586b6b8"}, + {file = "wrapt-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:392158c9a7f2ab1b8699418bfc0fe6f83548788c418b27d7bf2019ad3405cebb"}, + {file = "wrapt-2.3.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e5301c35cf75655eb33498f2bd6ae8703ca19940e3167dc9cdf740c712a39c60"}, + {file = "wrapt-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:418f54bb09d1762db02c7009b4051149893af3153a87f92d70356703c11eea02"}, + {file = "wrapt-2.3.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1598becd30f8f2777d18564064eb4f4dbe1ab0e05a8f09786d0ef505ac782bf3"}, + {file = "wrapt-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3da470536bf9645143323dd41b32db55c6f4304ad382094c1a1da8a92061e10d"}, + {file = "wrapt-2.3.0-cp312-cp312-win32.whl", hash = "sha256:fb8e2e6704a1e0b1b989546c69e2688371ef4a07fa5f61bde3eb6211186f5ac1"}, + {file = "wrapt-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:cdc021cb0b62471d6aac7f2bd92f3b4658073775f9ee7fcd325c511129e7bcc8"}, + {file = "wrapt-2.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:67bfe2485f50368c3fcd2275fc1fd100e350d601e0058921a7c82678a465aeab"}, + {file = "wrapt-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0d3fb71e65b001adfc42684522eeccd9c21d8ba679945abc993439567b66e59f"}, + {file = "wrapt-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:51a7a4181c1295774812271fbcd7c909df372bc25579d4ed9eb875caaf0ae86f"}, + {file = "wrapt-2.3.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9045917809c63fdf7abe3a2ceaed3d670b8ee4500ddd9291192d30aeb34467c5"}, + {file = "wrapt-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54ca1d5573f69b5fe1d74f1f65799c68015e82f685efec9fd8cfa40a094c44d0"}, + {file = "wrapt-2.3.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:242b60c21e30866e6a2fa606c612b47c553fa60c0eaeeeb7797fb842ac0ce609"}, + {file = "wrapt-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3f3d7ec0a51fbfe00d3aef047641ff2c58b25565b4717fc1f90e050be01cba8"}, + {file = "wrapt-2.3.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:261f53870cd4fb2bf38f9f972c56c728fd224cb7c65721307de59d9e7e6741ae"}, + {file = "wrapt-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8159ec0b0cb7608175eb150de94c19e34f4d47ac655f5ca9baf45df6b688ffd3"}, + {file = "wrapt-2.3.0-cp313-cp313-win32.whl", hash = "sha256:10461884b3014fbfc8eb7d09a93c5f246363e6711d9d881f95eb8c27fdef049f"}, + {file = "wrapt-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:ac870cc97b73bb00ac353329e9559a4bebc47c4c86792ed9b23b58c15b6ad838"}, + {file = "wrapt-2.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:a65e8db2b4e90c2e7ade931086351c98ef420bf7a94ee08c95ac8a3cbbc43579"}, + {file = "wrapt-2.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:fd1f2f557dd3491fe75905e578f4db967393d40d1a8f468edc4d40ac7f2d5944"}, + {file = "wrapt-2.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9f5d2aec29dfc76c37e23897dee92766a3fd4f3bff3ae7fc9c6b4bf37d8c1360"}, + {file = "wrapt-2.3.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:646d20d413ffcd1b0a2f700076e2d0252d872dcb7754860a73e45a59ea883614"}, + {file = "wrapt-2.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:379f670f45b7bb8993edd9f6fc36c6cc65edb81cffa0b504be34acb0303fff0a"}, + {file = "wrapt-2.3.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6208f302f110295d64b22a7ac96500c791bf492dce4366e622e4912b077c9687"}, + {file = "wrapt-2.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ed635a9ca4f3a5a2b900c10c69e823373bc00ebc114b459383596d3487da3570"}, + {file = "wrapt-2.3.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:e3b9eaa742ae7a0aaaaad4ca4b69469d757af2d6e6663ef1dadc47adec0aeb41"}, + {file = "wrapt-2.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d0f7284f88f4833705132d06d3b425a43095c2cbd07c58166aac3ab646ba12a4"}, + {file = "wrapt-2.3.0-cp313-cp313t-win32.whl", hash = "sha256:7ebb274aba688b043429eb1500ff8a76ce0cb8ac0812ca3e301f06247b8722b3"}, + {file = "wrapt-2.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:c4bded758ad6f03b965830944a2f0bc5b2eb3767fe5a7310134315d1a6610e98"}, + {file = "wrapt-2.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:d2cc64539da63e39ffb9c7ede849b6e8ddaaf7b3876b5cfb04efd85a5f3f4eb6"}, + {file = "wrapt-2.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ea52a0d0f08c584943d5764be0e84efa912c8da23c23e1e285ff2f5641c18fcc"}, + {file = "wrapt-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd85b0aa88efdb189d6ae2f35f4526943a8f091c38599c9c31478241c819e6a1"}, + {file = "wrapt-2.3.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:141ed6211286a9660d8d6702de598b43f0934b4f0eda16393f100a80f501d945"}, + {file = "wrapt-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e49885a62ec4ee854d1b9e6371fda6afd219917225752abf729a3f36d4df9a5"}, + {file = "wrapt-2.3.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d6159c9b2fefec02314e1332dbbbfaf960e369dfd26bcf7f8b258b5732065b3"}, + {file = "wrapt-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:24da48596326ef8e448cfa837b454f638713d3531262375f00e5a9681682fc07"}, + {file = "wrapt-2.3.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cd3a2edf0427013736b8127955cec62608c56e53ea47e82812ea32059cda407f"}, + {file = "wrapt-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4fa0df3bff4e7ce45759f33fd39335fe2f60477bb9ecf7b8aa41e7d07ee36a23"}, + {file = "wrapt-2.3.0-cp314-cp314-win32.whl", hash = "sha256:2935d5454b3f179a29b12cf390ee47246740ba2c3a7545b1b46ba31a5f2a4a0b"}, + {file = "wrapt-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:cc2cea812e5cb179a796b766747e7d3b21088760d8deb95676d482b8c8e6fa7d"}, + {file = "wrapt-2.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:22cc5c0a717bd4da87018ae0bffd4c19c6fb679d3ff357216ba566ab26c76cab"}, + {file = "wrapt-2.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a6b5984cd65dd639546f0eb4b8eacf1c31cb2fe9fb5c27bffe240987cdb2cf84"}, + {file = "wrapt-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c88abcf53daef80e01a75c7530e727fa6e2c1888fe83e3dcdba4c96216a1f5c7"}, + {file = "wrapt-2.3.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:85de890ff968196e92dd1ae73a9fb8970495e7650a457b1c9ef0ac3dd550bce2"}, + {file = "wrapt-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50f416b74d092bb9f41b424e90dd457f365f7ba4b11de62a23679769a21bd85c"}, + {file = "wrapt-2.3.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:39febbee6d77301d31da6996b152ce52452da7c7ef72aba10c2fa976dff9c295"}, + {file = "wrapt-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:93513bec052c6cd987f9f580c3df068c8bc4ebae6543736be3ca7ec5959cafcd"}, + {file = "wrapt-2.3.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:729126e667da34d251b8ebf8a45ef0c5ddadc21542b3d6e1abf4259ece6508df"}, + {file = "wrapt-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:626b69db2021aa01671ec7bbc9740e558522bd44c18cf2ce69bf3d666a014109"}, + {file = "wrapt-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:629d73378082c00a8173031f9fb30a3ac6abbc894a5bfdfae71fabc60642d501"}, + {file = "wrapt-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:42869085687f0aefd57c0f636c3f9354f8ffb321a8ba9cb52d19beb796e561c5"}, + {file = "wrapt-2.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b1e5aa486e269b00ed35e64771c7d0ab8096cfd2643405ca8cd60ebedc099a51"}, + {file = "wrapt-2.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c3b476ae63b4a3b4da681aafcb25ff3542d289fbda8b5da7caf76aaffafafdbb"}, + {file = "wrapt-2.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:932dced0a7b2950ed58a3325536a1dcb7b58e7330af54e8552d2e566b5328b99"}, + {file = "wrapt-2.3.0-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0db083387d6e75ec0be8173ecbf0e811cf60bae1cc75a815feb104167ea10d4d"}, + {file = "wrapt-2.3.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:abc71504669d126d91f89fc0e388c6295d8fbd2439be884f175133fda8aa403c"}, + {file = "wrapt-2.3.0-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b767a9566f165dd14decf8f4194c6bb0ce3a8420cec213824e05a99400c9260a"}, + {file = "wrapt-2.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:73d0b10b64620a2cf4bc3d31775c4d9527e309a5549e4379e3bf71e8d2dc193e"}, + {file = "wrapt-2.3.0-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:e31734c5077f29f892b2565eee5106d610278151ad49fc6a9d69a647cd5730e2"}, + {file = "wrapt-2.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:628f3ba8ec793a5b10a6cd8c6c6b7b55eb552abd1f3bd301336acb74c7a82dfe"}, + {file = "wrapt-2.3.0-cp39-cp39-win32.whl", hash = "sha256:3873c3c5ca9f4ef91f693602eca19d1f1e7c410338df82a4ff11d826b5896a8f"}, + {file = "wrapt-2.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:8f8a1c6472675956cece9a8f403f43c3594f1681319eed2dd56f60877397c636"}, + {file = "wrapt-2.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:c8858d8ff9822a081e3cc49ae1b3b22f0f789c14001cdac8f94564010d9c9d66"}, + {file = "wrapt-2.3.0-py3-none-any.whl", hash = "sha256:d8c7ed08477429752b8c44991f40ad7838b18332a160698740a6bfbc10d998a2"}, + {file = "wrapt-2.3.0.tar.gz", hash = "sha256:681a2d0eefd721998f90642762b8e75c2159ec531b20ad5e437245ea7b06a107"}, +] + +[package.extras] +dev = ["pytest", "setuptools"] + [[package]] name = "xlrd" version = "2.0.2" @@ -6521,6 +7211,27 @@ httpx = ">=0.21.2" pydantic = ">=1.9.2" typing_extensions = ">=4.0.0" +[[package]] +name = "zipp" +version = "4.1.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "python_version >= \"3.14\" and (extra == \"otel\" or extra == \"all\")" +files = [ + {file = "zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f"}, + {file = "zipp-4.1.0.tar.gz", hash = "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.14)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=3.4)"] +test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +type = ["pytest-mypy (>=1.0.1) ; platform_python_implementation != \"PyPy\""] + [[package]] name = "zstandard" version = "0.23.0" @@ -6636,10 +7347,11 @@ cffi = {version = ">=1.11", markers = "platform_python_implementation == \"PyPy\ cffi = ["cffi (>=1.11)"] [extras] -all = ["alibabacloud-oss-v2", "cachetools", "chonkie", "datasketch", "jieba", "langchain-text-splitters", "markitdown", "neo4j", "nltk", "pika", "pymilvus", "pymysql", "qdrant-client", "rake-nltk", "rank-bm25", "redis", "schedule", "sentence-transformers", "tavily-python", "torch", "volcengine-python-sdk"] +all = ["alibabacloud-oss-v2", "cachetools", "chonkie", "datasketch", "jieba", "langchain-text-splitters", "markitdown", "neo4j", "nltk", "opentelemetry-api", "opentelemetry-exporter-otlp-proto-grpc", "opentelemetry-exporter-otlp-proto-http", "opentelemetry-instrumentation-fastapi", "opentelemetry-sdk", "opentelemetry-semantic-conventions", "pika", "pymilvus", "pymysql", "qdrant-client", "rake-nltk", "rank-bm25", "redis", "schedule", "sentence-transformers", "tavily-python", "torch", "volcengine-python-sdk"] mem-reader = ["chonkie", "langchain-text-splitters", "markitdown"] mem-scheduler = ["pika", "redis"] mem-user = ["pymysql"] +otel = ["opentelemetry-api", "opentelemetry-exporter-otlp-proto-grpc", "opentelemetry-exporter-otlp-proto-http", "opentelemetry-instrumentation-fastapi", "opentelemetry-sdk", "opentelemetry-semantic-conventions"] pref-mem = ["datasketch", "pymilvus"] skill-mem = ["alibabacloud-oss-v2"] tavily = ["tavily-python"] @@ -6648,4 +7360,4 @@ tree-mem = ["neo4j", "schedule"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "cfab703c19f5f87caf537b68d30174695c44c6fc228e42c2c8e71b00b81f5790" +content-hash = "04c4bb4d2dc54da6122cf4c6cb67acbe6984b4a6b34df3faeeb1257e64de3a25" diff --git a/pyproject.toml b/pyproject.toml index 3eaf00ca3..3d3a2de62 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,6 +70,16 @@ dream = "memos.dream:CommunityDreamPlugin" # Developers install: `poetry install --extras `. e.g., `poetry install --extras general-mem` # Users install: `pip install MemoryOS[]`. e.g., `pip install MemoryOS[general-mem]` +# OpenTelemetry instrumentation (memory-semconv v0.1.0) +otel = [ + "opentelemetry-api (>=1.27.0)", + "opentelemetry-sdk (>=1.27.0)", + "opentelemetry-exporter-otlp-proto-grpc (>=1.27.0)", + "opentelemetry-exporter-otlp-proto-http (>=1.27.0)", + "opentelemetry-semantic-conventions (>=0.48b0)", + "opentelemetry-instrumentation-fastapi (>=0.48b0)", +] + # TreeTextualMemory tree-mem = [ "neo4j (>=5.28.1,<6.0.0)", # Graph database @@ -116,6 +126,12 @@ tavily = [ # Allow users to install with `pip install MemoryOS[all]` all = [ # Exist in the above optional groups + "opentelemetry-api (>=1.27.0)", + "opentelemetry-sdk (>=1.27.0)", + "opentelemetry-exporter-otlp-proto-grpc (>=1.27.0)", + "opentelemetry-exporter-otlp-proto-http (>=1.27.0)", + "opentelemetry-semantic-conventions (>=0.48b0)", + "opentelemetry-instrumentation-fastapi (>=0.48b0)", "neo4j (>=5.28.1,<6.0.0)", "schedule (>=1.2.2,<2.0.0)", "redis (>=6.2.0,<7.0.0)", diff --git a/src/memos/api/mcp_serve.py b/src/memos/api/mcp_serve.py index 9cfa02820..8af3882d8 100644 --- a/src/memos/api/mcp_serve.py +++ b/src/memos/api/mcp_serve.py @@ -1,4 +1,5 @@ import asyncio +import logging import os from typing import Any @@ -7,6 +8,7 @@ from fastmcp import FastMCP # Assuming these are your imports +from memos import telemetry as _telemetry from memos.mem_os.main import MOS from memos.mem_os.utils.default_config import get_default from memos.mem_user.user_manager import UserRole @@ -14,6 +16,54 @@ load_dotenv() +logger = logging.getLogger(__name__) + + +def _bootstrap_telemetry() -> None: + """ + Bootstrap the OTel SDK for the MCP server entry-point. + + The ``mos_core.add/search/update/delete`` calls invoked by the MCP tools below + are already instrumented (memos.telemetry.memory_span / instrument_op), but + those spans/metrics/logs only reach a collector when a real (exporting) + TracerProvider is installed. The REST server (server_api.py) bootstraps this, + but the MCP entry-point previously did not — so when MemOS was driven over MCP + (e.g. the memory-benchmark driver) it emitted NO telemetry and the "memos" + service never appeared in the end-to-end trace (ISI-1918 board report). + + configure_from_env() is a no-op unless OTEL_EXPORTER_OTLP_ENDPOINT is set, so + local stdio runs without a collector stay silent. + + We then best-effort enable server-side MCP instrumentation so an incoming + W3C traceparent (injected by an instrumented MCP client) is extracted and the + memory.* spans nest under the caller's trace instead of starting a new root. + """ + configured = _telemetry.configure_from_env() + if configured: + logger.info( + "[MCP_SERVE] OTel telemetry configured -> %s (service=%s)", + os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT"), + os.getenv("OTEL_SERVICE_NAME", "memos"), + ) + # Extract incoming trace context from MCP requests so memory.* spans join + # the caller's trace (end-to-end). Optional: warn loudly if the package is + # absent rather than silently losing cross-service correlation. + try: + from opentelemetry.instrumentation.mcp import MCPInstrumentor + + MCPInstrumentor().instrument() + logger.info( + "[MCP_SERVE] MCP instrumentation active — trace context propagates over MCP" + ) + except ImportError: + logger.warning( + "[MCP_SERVE] opentelemetry-instrumentation-mcp NOT installed: memos spans " + "will export but start a NEW trace instead of nesting under the caller. " + "Install it (client + server) for end-to-end cross-service traces." + ) + else: + logger.info("[MCP_SERVE] OTel telemetry not configured (OTEL_EXPORTER_OTLP_ENDPOINT unset)") + def load_default_config(user_id="default_user"): """ @@ -126,6 +176,9 @@ class MOSMCPServer: """MCP Server that accepts an existing MOS instance.""" def __init__(self, mos_instance: MOS | None = None): + # Bootstrap OTel BEFORE building MOS so instrumentation binds to the real + # (exporting) TracerProvider rather than the global no-op default. + _bootstrap_telemetry() self.mcp = FastMCP("MOS Memory System") if mos_instance is None: # Fall back to creating from default config diff --git a/src/memos/api/server_api.py b/src/memos/api/server_api.py index 1c6d93b9f..44075291f 100644 --- a/src/memos/api/server_api.py +++ b/src/memos/api/server_api.py @@ -9,6 +9,15 @@ from fastapi.exceptions import RequestValidationError from starlette.staticfiles import StaticFiles +# OTel: bootstrap the SDK (TracerProvider + OTLP exporters) from the environment, +# then instrument FastAPI so agent HTTP calls produce end-to-end traces. +# +# configure_from_env() MUST run before FastAPIInstrumentor.instrument_app() so the +# instrumentor binds to the real (exporting) TracerProvider rather than the global +# no-op default. It is a no-op unless OTEL_EXPORTER_OTLP_ENDPOINT is set, so local +# runs without a collector stay silent. In the memory-benchmark cluster we set +# OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector.observability:4317. +from memos import telemetry as _telemetry from memos.api.exceptions import APIExceptionHandler from memos.api.lifecycle import shutdown_components from memos.api.middleware.request_context import RequestContextMiddleware @@ -16,6 +25,14 @@ from memos.plugins.manager import plugin_manager +try: + from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor as _FastAPIInstrumentor + + _OTEL_FASTAPI_AVAILABLE = True +except ImportError: + _OTEL_FASTAPI_AVAILABLE = False + + load_dotenv() plugin_manager.discover() @@ -46,6 +63,26 @@ async def lifespan(_app: FastAPI) -> AsyncIterator[None]: app.mount("/download", StaticFiles(directory=os.getenv("FILE_LOCAL_PATH")), name="static_mapping") app.add_middleware(RequestContextMiddleware, source="server_api") + +# Bootstrap the OTel SDK from env (no-op unless OTEL_EXPORTER_OTLP_ENDPOINT is set), +# then attach FastAPI instrumentation. Ordering matters: instrument_app() binds to +# whatever TracerProvider is global at call time, so configure_from_env() runs first. +_OTEL_CONFIGURED = _telemetry.configure_from_env() +if _OTEL_CONFIGURED: + logger.info( + "[SERVER_API] OTel telemetry configured -> %s (service=%s)", + os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT"), + os.getenv("OTEL_SERVICE_NAME", "memos"), + ) +else: + logger.info("[SERVER_API] OTel telemetry not configured (OTEL_EXPORTER_OTLP_ENDPOINT unset)") + +# Attach OTel FastAPI instrumentation. This creates server-side spans for each +# request and propagates W3C traceparent headers, so downstream memory.* spans +# appear nested under the agent's root span. +if _OTEL_FASTAPI_AVAILABLE: + _FastAPIInstrumentor.instrument_app(app) + # Include routers app.include_router(server_router_module.router) diff --git a/src/memos/mem_os/core.py b/src/memos/mem_os/core.py index 3ede965d3..3f926f794 100644 --- a/src/memos/mem_os/core.py +++ b/src/memos/mem_os/core.py @@ -1,4 +1,5 @@ import json +import logging import os import time @@ -28,6 +29,18 @@ from memos.memories.parametric.item import ParametricMemoryItem from memos.memories.textual.item import TextualMemoryItem, TextualMemoryMetadata from memos.memos_tools.thread_safe_dict_segment import OptimizedThreadSafeDict +from memos.telemetry import ( + MEMORY_RESULT_COUNT, + MEMORY_SESSION_ID, + MEMORY_TOP_K, + MEMORY_USER_ID, + TIER_PREFERENCE, + TIER_TEXTUAL, + emit_op_log, + instrument_op, + memory_span, + record_result_count, +) from memos.templates.mos_prompts import QUERY_REWRITING_PROMPT from memos.types import ChatHistory, MessageList, MOSSearchResult @@ -571,116 +584,147 @@ def search( target_session_id = session_id if session_id is not None else self.session_id target_user_id = user_id if user_id is not None else self.user_id - self._validate_user_exists(target_user_id) - # Get all cubes accessible by the target user - accessible_cubes = self.user_manager.get_user_cubes(target_user_id) - user_cube_ids = [cube.cube_id for cube in accessible_cubes] - - logger.info( - f"User {target_user_id} has access to {len(user_cube_ids)} cubes: {user_cube_ids}" - ) - if target_user_id not in self.chat_history_manager: - self._register_chat_history(target_user_id) - chat_history = self.chat_history_manager[target_user_id] - - # Create search filter if session_id is provided - search_filter = None - if session_id is not None: - search_filter = {"session_id": session_id} - - result: MOSSearchResult = { - "text_mem": [], - "act_mem": [], - "para_mem": [], - "pref_mem": [], + _span_attrs = { + MEMORY_USER_ID: str(target_user_id or ""), + MEMORY_SESSION_ID: str(target_session_id or ""), + MEMORY_TOP_K: top_k if top_k is not None else self.config.top_k, + # Truncate query at 256 chars to respect cardinality / PII guidance + "memory.query.text": (query or "")[:256], } - if install_cube_ids is None: - install_cube_ids = user_cube_ids - # create exist dict in mem_cubes and avoid one search slow - tmp_mem_cubes = {} - time_start_cube_get = time.time() - for mem_cube_id in install_cube_ids: - if mem_cube_id in self.mem_cubes: - tmp_mem_cubes[mem_cube_id] = self.mem_cubes.get(mem_cube_id) - logger.info( - f"time search: transform cube time user_id: {target_user_id} time is: {time.time() - time_start_cube_get}" - ) - - for mem_cube_id, mem_cube in tmp_mem_cubes.items(): - # Define internal functions for parallel search execution - def search_textual_memory(cube_id, cube): - if ( - (cube_id in install_cube_ids) - and (cube.text_mem is not None) - and self.config.enable_textual_memory - ): - time_start = time.time() - memories = cube.text_mem.search( - query, - top_k=top_k if top_k else self.config.top_k, - mode=mode, - manual_close_internet=not internet_search, - info={ - "user_id": target_user_id, - "session_id": target_session_id, - "chat_history": chat_history.chat_history, - }, - moscube=moscube, - search_filter=search_filter, - ) - search_time_end = time.time() - logger.info( - f"🧠 [Memory] Searched memories from {cube_id}:\n{self._str_memories(memories)}\n" - ) - logger.info( - f"time search graph: search graph time user_id: {target_user_id} time is: {search_time_end - time_start}" - ) - return {"cube_id": cube_id, "memories": memories} - return None - - def search_preference_memory(cube_id, cube): - if ( - (cube_id in install_cube_ids) - and (cube.pref_mem is not None) - and self.config.enable_preference_memory - ): - time_start = time.time() - memories = cube.pref_mem.search( - query, - top_k=top_k if top_k else self.config.top_k, - info={ - "user_id": target_user_id, - "session_id": self.session_id, - "chat_history": chat_history.chat_history, - }, - ) - search_time_end = time.time() - logger.info( - f"🧠 [Memory] Searched preferences from {cube_id}:\n{self._str_memories(memories)}\n" - ) - logger.info( - f"time search pref: search pref time user_id: {target_user_id} time is: {search_time_end - time_start}" - ) - return {"cube_id": cube_id, "memories": memories} - return None - # Execute both search functions in parallel - with ContextThreadPoolExecutor(max_workers=2) as executor: - text_future = executor.submit(search_textual_memory, mem_cube_id, mem_cube) - pref_future = executor.submit(search_preference_memory, mem_cube_id, mem_cube) + with memory_span("search", TIER_TEXTUAL, _span_attrs) as span: + self._validate_user_exists(target_user_id) + # Get all cubes accessible by the target user + accessible_cubes = self.user_manager.get_user_cubes(target_user_id) + user_cube_ids = [cube.cube_id for cube in accessible_cubes] - # Wait for both tasks to complete and collect results - text_result = text_future.result() - pref_result = pref_future.result() + logger.info( + f"User {target_user_id} has access to {len(user_cube_ids)} cubes: {user_cube_ids}" + ) + if target_user_id not in self.chat_history_manager: + self._register_chat_history(target_user_id) + chat_history = self.chat_history_manager[target_user_id] + + # Create search filter if session_id is provided + search_filter = None + if session_id is not None: + search_filter = {"session_id": session_id} + + result: MOSSearchResult = { + "text_mem": [], + "act_mem": [], + "para_mem": [], + "pref_mem": [], + } + if install_cube_ids is None: + install_cube_ids = user_cube_ids + # create exist dict in mem_cubes and avoid one search slow + tmp_mem_cubes = {} + time_start_cube_get = time.time() + for mem_cube_id in install_cube_ids: + if mem_cube_id in self.mem_cubes: + tmp_mem_cubes[mem_cube_id] = self.mem_cubes.get(mem_cube_id) + logger.info( + f"time search: transform cube time user_id: {target_user_id} time is: {time.time() - time_start_cube_get}" + ) - # Add results to the main result dictionary - if text_result is not None: - result["text_mem"].append(text_result) - if pref_result is not None: - result["pref_mem"].append(pref_result) + for mem_cube_id, mem_cube in tmp_mem_cubes.items(): + # Define internal functions for parallel search execution + def search_textual_memory(cube_id, cube): + if ( + (cube_id in install_cube_ids) + and (cube.text_mem is not None) + and self.config.enable_textual_memory + ): + time_start = time.time() + memories = cube.text_mem.search( + query, + top_k=top_k if top_k else self.config.top_k, + mode=mode, + manual_close_internet=not internet_search, + info={ + "user_id": target_user_id, + "session_id": target_session_id, + "chat_history": chat_history.chat_history, + }, + moscube=moscube, + search_filter=search_filter, + ) + search_time_end = time.time() + logger.info( + f"🧠 [Memory] Searched memories from {cube_id}:\n{self._str_memories(memories)}\n" + ) + logger.info( + f"time search graph: search graph time user_id: {target_user_id} time is: {search_time_end - time_start}" + ) + return {"cube_id": cube_id, "memories": memories} + return None + + def search_preference_memory(cube_id, cube): + if ( + (cube_id in install_cube_ids) + and (cube.pref_mem is not None) + and self.config.enable_preference_memory + ): + time_start = time.time() + memories = cube.pref_mem.search( + query, + top_k=top_k if top_k else self.config.top_k, + info={ + "user_id": target_user_id, + "session_id": self.session_id, + "chat_history": chat_history.chat_history, + }, + ) + search_time_end = time.time() + logger.info( + f"🧠 [Memory] Searched preferences from {cube_id}:\n{self._str_memories(memories)}\n" + ) + logger.info( + f"time search pref: search pref time user_id: {target_user_id} time is: {search_time_end - time_start}" + ) + return {"cube_id": cube_id, "memories": memories} + return None + + # Execute both search functions in parallel + with ContextThreadPoolExecutor(max_workers=2) as executor: + text_future = executor.submit(search_textual_memory, mem_cube_id, mem_cube) + pref_future = executor.submit(search_preference_memory, mem_cube_id, mem_cube) + + # Wait for both tasks to complete and collect results + text_result = text_future.result() + pref_result = pref_future.result() + + # Add results to the main result dictionary + if text_result is not None: + result["text_mem"].append(text_result) + if pref_result is not None: + result["pref_mem"].append(pref_result) + + # Record aggregate result metrics + total_text = sum(len(r.get("memories", [])) for r in result["text_mem"]) + total_pref = sum(len(r.get("memories", [])) for r in result["pref_mem"]) + total_results = total_text + total_pref + span.set_attribute(MEMORY_RESULT_COUNT, total_results) + span.set_attribute("memory.result.text_count", total_text) + span.set_attribute("memory.result.pref_count", total_pref) + record_result_count(total_text, TIER_TEXTUAL) + record_result_count(total_pref, TIER_PREFERENCE) + emit_op_log( + logging.INFO, + "search", + TIER_TEXTUAL, + "memory.search completed", + { + MEMORY_USER_ID: str(target_user_id or ""), + MEMORY_RESULT_COUNT: total_results, + "memory.cubes_searched": len(tmp_mem_cubes), + }, + ) - return result + return result + @instrument_op("add", TIER_TEXTUAL) def add( self, messages: MessageList | None = None, @@ -714,6 +758,7 @@ def add( target_session_id = session_id if session_id else self.session_id target_user_id = user_id if user_id is not None else self.user_id + if mem_cube_id is None: # Try to find a default cube for the user accessible_cubes = self.user_manager.get_user_cubes(target_user_id) @@ -994,6 +1039,7 @@ def get_all( ) return result + @instrument_op("update", TIER_TEXTUAL) def update( self, mem_cube_id: str, @@ -1033,6 +1079,7 @@ def update( f" {self.mem_cubes[mem_cube_id].config.text_mem.backend} does not support update memory" ) + @instrument_op("delete", TIER_TEXTUAL) def delete(self, mem_cube_id: str, memory_id: str, user_id: str | None = None) -> None: """ Delete a textual memory from a MemCube by memory_id. diff --git a/src/memos/multi_mem_cube/single_cube.py b/src/memos/multi_mem_cube/single_cube.py index 27399259a..284cb61fd 100644 --- a/src/memos/multi_mem_cube/single_cube.py +++ b/src/memos/multi_mem_cube/single_cube.py @@ -23,6 +23,16 @@ from memos.memories.textual.item import TextualMemoryItem from memos.multi_mem_cube.views import MemCubeView from memos.search import resolve_filter_for_cube, search_text_memories +from memos.telemetry import ( + MEMORY_CUBE_ID, + MEMORY_QUERY_TEXT, + MEMORY_SESSION_ID, + MEMORY_USER_ID, + TIER_TEXTUAL, + get_current_span, + instrument_op, + record_result_count, +) from memos.templates.mem_reader_prompts import PROMPT_MAPPING from memos.types.general_types import ( FINE_STRATEGY, @@ -55,6 +65,7 @@ class SingleCubeView(MemCubeView): feedback_server: Any | None = None deepsearch_agent: Any | None = None + @instrument_op("add", TIER_TEXTUAL) @timed def add_memories(self, add_req: APIADDRequest) -> list[dict[str, Any]]: """ @@ -73,6 +84,13 @@ def add_memories(self, add_req: APIADDRequest) -> list[dict[str, Any]]: project_id=add_req.project_id, ) + # Enrich the instrument_op span with real resource attributes (the + # decorator only sees the positional add_req, so it defaults to ""). + span = get_current_span() + span.set_attribute(MEMORY_USER_ID, str(add_req.user_id or "")) + span.set_attribute(MEMORY_CUBE_ID, str(self.cube_id or "")) + span.set_attribute(MEMORY_SESSION_ID, str(add_req.session_id or "default_session")) + target_session_id = add_req.session_id or "default_session" self.logger.info( f"[SingleCubeView] cube={self.cube_id} " @@ -85,6 +103,7 @@ def add_memories(self, add_req: APIADDRequest) -> list[dict[str, Any]]: return all_memories + @instrument_op("search", TIER_TEXTUAL) @timed def search_memories(self, search_req: APISearchRequest) -> dict[str, Any]: """ @@ -104,6 +123,14 @@ def search_memories(self, search_req: APISearchRequest) -> dict[str, Any]: mem_cube_id=self.cube_id, session_id=search_req.session_id or "default_session", ) + + # Enrich the instrument_op span with real resource attributes + query. + span = get_current_span() + span.set_attribute(MEMORY_USER_ID, str(search_req.user_id or "")) + span.set_attribute(MEMORY_CUBE_ID, str(self.cube_id or "")) + span.set_attribute(MEMORY_SESSION_ID, str(search_req.session_id or "default_session")) + span.set_attribute(MEMORY_QUERY_TEXT, str(search_req.query or "")) + self.logger.info("Search request summary: %s", summarize_search_request(search_req)) memories_result: MOSSearchResult = { @@ -129,6 +156,12 @@ def search_memories(self, search_req: APISearchRequest) -> dict[str, Any]: self.cube_id, ) + # Emit memory-semconv result-count metric (flattened textual items). + result_count = sum( + entry.get("total_nodes", 0) for entry in memories_result.get("text_mem", []) + ) + record_result_count(result_count, TIER_TEXTUAL, "search") + self.logger.info("Search result summary: %s", summarize_search_results(memories_result)) return memories_result diff --git a/src/memos/telemetry.py b/src/memos/telemetry.py new file mode 100644 index 000000000..675bb8ac9 --- /dev/null +++ b/src/memos/telemetry.py @@ -0,0 +1,427 @@ +""" +OpenTelemetry instrumentation for MemOS (memory-semconv v0.1.0). + +Memory tier mapping: + text_mem → memory.tier = "textual" (episodic / semantic long-term) + act_mem → memory.tier = "activation" (KV-cache / short-term activation) + para_mem → memory.tier = "parametric" (model-weights layer) + pref_mem → memory.tier = "preference" (user preference store) + +Signals emitted: + Traces — spans on add / search / update / delete with rich attributes + Metrics — counters, histograms, and up-down counters for utilization + Logs — structured log records correlated with active span context +""" + +from __future__ import annotations + +import functools +import logging +import os +import time + +from contextlib import contextmanager +from typing import TYPE_CHECKING, Any + + +# OpenTelemetry is an optional dependency (the ``otel`` extra). Import it lazily so +# that ``import memos.telemetry`` — and therefore ``import memos.mem_os.core`` — keeps +# working on a default install *without* the extra; the instrumentation then quietly +# degrades to a no-op. configure()/get_tracer()/memory_span() all guard on +# ``_OTEL_AVAILABLE`` so nothing here references the SDK when it is absent. +try: + from opentelemetry import metrics, propagate, trace + from opentelemetry._logs import set_logger_provider + from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter + from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter + from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter + from opentelemetry.propagators.composite import CompositeHTTPPropagator + from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler + from opentelemetry.sdk._logs.export import BatchLogRecordProcessor + from opentelemetry.sdk.metrics import MeterProvider + from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader + from opentelemetry.sdk.resources import Resource + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import BatchSpanProcessor + from opentelemetry.semconv.resource import ResourceAttributes + from opentelemetry.trace import Status, StatusCode + from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator + + _OTEL_AVAILABLE = True +except ImportError: # pragma: no cover - exercised only without the [otel] extra + _OTEL_AVAILABLE = False + + +if TYPE_CHECKING: + from collections.abc import Callable + + +# --------------------------------------------------------------------------- +# Semconv attribute constants (memory-semconv v0.1.0) +# --------------------------------------------------------------------------- +MEMORY_TIER = "memory.tier" +MEMORY_OPERATION = "memory.operation" +MEMORY_ITEM_COUNT = "memory.item.count" +MEMORY_RESULT_COUNT = "memory.result.count" +MEMORY_QUERY_TEXT = "memory.query.text" +MEMORY_USER_ID = "memory.user.id" +MEMORY_CUBE_ID = "memory.cube.id" +MEMORY_SESSION_ID = "memory.session.id" +MEMORY_BACKEND = "memory.backend" +MEMORY_TOP_K = "memory.top_k" +MEMORY_HIT_COUNT = "memory.hit.count" +MEMORY_LATENCY_MS = "memory.latency.ms" +MEMORY_INDEX_SIZE = "memory.index.size" +MEMORY_UTILIZATION = "memory.utilization" + +# Tier name constants +TIER_TEXTUAL = "textual" +TIER_ACTIVATION = "activation" +TIER_PARAMETRIC = "parametric" +TIER_PREFERENCE = "preference" + +# Instrument name +INSTRUMENT_NAME = "memos" + +_initialized = False +_tracer: trace.Tracer | None = None +_meter: metrics.Meter | None = None +_otel_logger: logging.Logger | None = None + +# Metric instruments (created after meter is available) +_op_counter: metrics.Counter | None = None +_latency_histogram: metrics.Histogram | None = None +_item_count_gauge: metrics.ObservableGauge | None = None +_result_count_histogram: metrics.Histogram | None = None + + +def configure( + service_name: str = "memos", + service_version: str = "0.0.0", + otlp_endpoint: str = "http://localhost:4317", + export_interval_ms: int = 5000, +) -> None: + """ + Configure OTel providers and install them globally. + + Call once at application startup (e.g., from MOSConfig or CLI entry-point). + Safe to call multiple times; subsequent calls are no-ops. + + Args: + service_name: OTel service.name resource attribute. + service_version: OTel service.version resource attribute. + otlp_endpoint: gRPC OTLP collector endpoint (e.g. http://otel-collector:4317). + export_interval_ms: Metric export interval in milliseconds. + """ + global _initialized, _tracer, _meter, _otel_logger + global _op_counter, _latency_histogram, _result_count_histogram + + if _initialized: + return + if not _OTEL_AVAILABLE: + # OpenTelemetry not installed (no [otel] extra) — stay a pure no-op. + return + + resource = Resource.create( + { + ResourceAttributes.SERVICE_NAME: service_name, + ResourceAttributes.SERVICE_VERSION: service_version, + } + ) + + # Install W3C TraceContext propagator so traceparent/tracestate headers are + # extracted from incoming requests and injected into outgoing calls. + propagate.set_global_textmap(CompositeHTTPPropagator([TraceContextTextMapPropagator()])) + + # --- Traces --- + tracer_provider = TracerProvider(resource=resource) + tracer_provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(endpoint=otlp_endpoint))) + trace.set_tracer_provider(tracer_provider) + _tracer = trace.get_tracer(INSTRUMENT_NAME) + + # --- Metrics --- + metric_reader = PeriodicExportingMetricReader( + OTLPMetricExporter(endpoint=otlp_endpoint), + export_interval_millis=export_interval_ms, + ) + meter_provider = MeterProvider(resource=resource, metric_readers=[metric_reader]) + metrics.set_meter_provider(meter_provider) + _meter = metrics.get_meter(INSTRUMENT_NAME) + + _op_counter = _meter.create_counter( + name="memory.operations", + unit="{operation}", + description="Total number of memory operations", + ) + _latency_histogram = _meter.create_histogram( + name="memory.operation.duration", + unit="ms", + description="Duration of memory operations in milliseconds", + ) + _result_count_histogram = _meter.create_histogram( + name="memory.result.count", + unit="{item}", + description="Number of results returned per search operation", + ) + + # --- Logs --- + logger_provider = LoggerProvider(resource=resource) + logger_provider.add_log_record_processor( + BatchLogRecordProcessor(OTLPLogExporter(endpoint=otlp_endpoint)) + ) + set_logger_provider(logger_provider) + + otel_handler = LoggingHandler(level=logging.DEBUG, logger_provider=logger_provider) + root_logger = logging.getLogger("memos") + root_logger.addHandler(otel_handler) + _otel_logger = logging.getLogger(f"{INSTRUMENT_NAME}.ops") + + _initialized = True + + +def _resolve_service_version(explicit: str | None = None) -> str: + """Best-effort service.version: explicit env value, else installed pkg, else 0.0.0.""" + if explicit: + return explicit + from importlib.metadata import PackageNotFoundError, version + + for dist in ("MemoryOS", "memoryos", "memos"): + try: + return version(dist) + except PackageNotFoundError: + continue + return "0.0.0" + + +def configure_from_env() -> bool: + """ + Bootstrap OTel from environment variables at process startup. + + This is the server-side bootstrap that makes the ``memory.*`` spans/metrics/logs + emitted by the instrumentation in ``memos.mem_os.core`` actually reach a + collector. Without it, ``get_tracer()``/``get_meter()`` return the global + no-op providers and nothing is exported. + + Telemetry is enabled only when an OTLP endpoint is configured, so local/dev + runs without a collector stay a pure no-op. Recognised variables: + + OTEL_EXPORTER_OTLP_ENDPOINT gRPC OTLP endpoint (enables telemetry when set), + e.g. http://otel-collector.observability:4317 + OTEL_SERVICE_NAME service.name resource attribute (default: memos) + OTEL_SERVICE_VERSION service.version (default: installed package version) + OTEL_METRIC_EXPORT_INTERVAL metric export interval in ms (default: 5000) + MEMOS_OTEL_ENABLED set to a falsy value to force-disable even when + an endpoint is present + + Returns True when telemetry was configured, False when it was skipped. + """ + if not _OTEL_AVAILABLE: + return False + + enabled_flag = os.getenv("MEMOS_OTEL_ENABLED") + if enabled_flag is not None and enabled_flag.strip().lower() in { + "0", + "false", + "no", + "off", + "", + }: + return False + + endpoint = os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT") + if not endpoint: + # No collector configured — leave the SDK as a no-op so local runs are silent. + return False + + try: + interval_ms = int(os.getenv("OTEL_METRIC_EXPORT_INTERVAL", "5000")) + except ValueError: + interval_ms = 5000 + + configure( + service_name=os.getenv("OTEL_SERVICE_NAME", "memos"), + service_version=_resolve_service_version(os.getenv("OTEL_SERVICE_VERSION")), + otlp_endpoint=endpoint, + export_interval_ms=interval_ms, + ) + return _initialized + + +def get_tracer(): + """Return the global tracer (None if OpenTelemetry is not installed).""" + if not _OTEL_AVAILABLE: + return None + if _tracer is not None: + return _tracer + return trace.get_tracer(INSTRUMENT_NAME) + + +def get_meter(): + """Return the global meter (None if OpenTelemetry is not installed).""" + if not _OTEL_AVAILABLE: + return None + if _meter is not None: + return _meter + return metrics.get_meter(INSTRUMENT_NAME) + + +def get_current_span(): + """ + Return the active span so callers can enrich it with attributes. + + Yields a no-op stand-in when OpenTelemetry is not installed, so call sites + can unconditionally ``span.set_attribute(...)`` without importing the SDK + directly (keeps ``opentelemetry`` out of module-level imports for the + optional-dependency check). + """ + if not _OTEL_AVAILABLE: + return _NoopSpan() + return trace.get_current_span() + + +# --------------------------------------------------------------------------- +# Low-level span helpers +# --------------------------------------------------------------------------- + + +class _NoopSpan: + """Span stand-in yielded by ``memory_span`` when OpenTelemetry is absent.""" + + def set_attribute(self, *args: Any, **kwargs: Any) -> None: + pass + + def set_status(self, *args: Any, **kwargs: Any) -> None: + pass + + def record_exception(self, *args: Any, **kwargs: Any) -> None: + pass + + +@contextmanager +def memory_span( + operation: str, + tier: str, + attributes: dict[str, Any] | None = None, +): + """ + Context manager that wraps a memory operation in an OTel span. + + Args: + operation: One of add / search / update / delete. + tier: Memory tier (textual / activation / parametric / preference). + attributes: Extra span attributes to merge in. + + Yields the span so callers can add result attributes after the operation. + """ + if not _OTEL_AVAILABLE: + # No SDK installed — yield an inert span so instrumented call sites are + # unaffected on a default install. + yield _NoopSpan() + return + + tracer = get_tracer() + span_attrs: dict[str, Any] = { + MEMORY_OPERATION: operation, + MEMORY_TIER: tier, + } + if attributes: + span_attrs.update(attributes) + + with tracer.start_as_current_span( + f"memory.{operation}", + kind=trace.SpanKind.INTERNAL, + attributes=span_attrs, + ) as span: + t0 = time.perf_counter() + try: + yield span + span.set_status(Status(StatusCode.OK)) + except Exception as exc: + span.set_status(Status(StatusCode.ERROR, str(exc))) + span.record_exception(exc) + raise + finally: + elapsed_ms = (time.perf_counter() - t0) * 1000 + span.set_attribute(MEMORY_LATENCY_MS, round(elapsed_ms, 2)) + _record_op_metrics(operation, tier, elapsed_ms, span_attrs) + + +def _record_op_metrics( + operation: str, + tier: str, + latency_ms: float, + attrs: dict[str, Any], +) -> None: + """Record counter + latency histogram for a completed operation.""" + if _op_counter is None or _latency_histogram is None: + return + label = {MEMORY_OPERATION: operation, MEMORY_TIER: tier} + _op_counter.add(1, label) + _latency_histogram.record(latency_ms, label) + + +def record_result_count(count: int, tier: str, operation: str = "search") -> None: + """Record the number of items returned by a search.""" + if _result_count_histogram is not None: + _result_count_histogram.record(count, {MEMORY_TIER: tier, MEMORY_OPERATION: operation}) + + +def instrument_op(operation: str, tier: str) -> Callable: + """ + Decorator that wraps a MemOS method in an OTel span + metrics. + + Injects ``user_id`` and ``mem_cube_id`` attributes from matching keyword + arguments when present. Does NOT capture query text (use ``memory_span`` + directly in ``search`` for that). + """ + + def decorator(fn: Callable) -> Callable: + @functools.wraps(fn) + def wrapper(*args, **kwargs): + attrs: dict[str, Any] = { + MEMORY_USER_ID: str(kwargs.get("user_id") or ""), + MEMORY_CUBE_ID: str(kwargs.get("mem_cube_id") or ""), + MEMORY_SESSION_ID: str(kwargs.get("session_id") or ""), + } + with memory_span(operation, tier, attrs) as span: + result = fn(*args, **kwargs) + # Annotate item count for add operations + if operation == "add": + messages = kwargs.get("messages") + count = len(messages) if messages is not None else 1 + span.set_attribute(MEMORY_ITEM_COUNT, count) + emit_op_log( + logging.INFO, + operation, + tier, + f"memory.{operation} completed", + { + MEMORY_CUBE_ID: str(kwargs.get("mem_cube_id") or ""), + MEMORY_ITEM_COUNT: count, + }, + ) + else: + emit_op_log(logging.INFO, operation, tier, f"memory.{operation} completed") + return result + + return wrapper + + return decorator + + +def emit_op_log( + level: int, + operation: str, + tier: str, + message: str, + extra: dict[str, Any] | None = None, +) -> None: + """Emit a structured log record correlated with the current span context.""" + logger = _otel_logger or logging.getLogger(f"{INSTRUMENT_NAME}.ops") + record_extra = { + MEMORY_OPERATION: operation, + MEMORY_TIER: tier, + } + if extra: + record_extra.update(extra) + logger.log(level, message, extra=record_extra) diff --git a/tests/test_mcp_serve_telemetry.py b/tests/test_mcp_serve_telemetry.py new file mode 100644 index 000000000..d260af329 --- /dev/null +++ b/tests/test_mcp_serve_telemetry.py @@ -0,0 +1,178 @@ +""" +Regression guard for ISI-1918: the MCP server entry-point MUST bootstrap OTel. + +Root cause of the board report ("trace has only crewai spans, no memos service"): +`memos.api.mcp_serve` served MemOS over MCP but never called +`telemetry.configure_from_env()`. The `mos_core.add/search/...` calls are already +instrumented, but without an exporting TracerProvider they run against the global +no-op provider, so the "memos" service emitted ZERO telemetry over the MCP path. + +These tests pin two contracts: + 1. `telemetry.configure_from_env()` is env-gated (no-op unless an OTLP endpoint + is set) and stamps service.name=memos when it is. + 2. Constructing the MCP server triggers the telemetry bootstrap. + +The MCP server is loaded with lightweight stubs for fastmcp / MOS so the test +needs neither a collector nor the heavy model stack. +""" + +import importlib.util +import pathlib +import sys +import types + +import pytest + + +# OpenTelemetry is an optional (``otel``) extra — skip cleanly if it's absent. +pytest.importorskip("opentelemetry") + +from opentelemetry import trace +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import SimpleSpanProcessor +from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter + + +_REPO = pathlib.Path(__file__).parent.parent +_TEL_PATH = _REPO / "src" / "memos" / "telemetry.py" +_MCP_PATH = _REPO / "src" / "memos" / "api" / "mcp_serve.py" + + +def _load_telemetry(): + spec = importlib.util.spec_from_file_location("memos.telemetry", _TEL_PATH) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + + +# --------------------------------------------------------------------------- +# 1. configure_from_env is env-gated +# --------------------------------------------------------------------------- +def test_configure_from_env_noop_without_endpoint(monkeypatch): + monkeypatch.delenv("OTEL_EXPORTER_OTLP_ENDPOINT", raising=False) + tel = _load_telemetry() + assert tel.configure_from_env() is False + assert tel._initialized is False + + +def test_configure_from_env_disabled_flag(monkeypatch): + monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4317") + monkeypatch.setenv("MEMOS_OTEL_ENABLED", "false") + tel = _load_telemetry() + assert tel.configure_from_env() is False + assert tel._initialized is False + + +# --------------------------------------------------------------------------- +# 2. Constructing the MCP server bootstraps telemetry +# --------------------------------------------------------------------------- +@pytest.fixture() +def stubbed_mcp_serve(monkeypatch): + """Load mcp_serve.py with fastmcp / MOS stubs and a recording telemetry stub.""" + calls = {"configure_from_env": 0} + + fake_tel = types.ModuleType("memos.telemetry") + + def _configure_from_env(): + calls["configure_from_env"] += 1 + # Mirror the real env gate so the test exercises both branches. + import os as _os + + return bool(_os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT")) + + fake_tel.configure_from_env = _configure_from_env + + fake_memos = types.ModuleType("memos") + fake_memos.telemetry = fake_tel + + class _FakeFastMCP: + def __init__(self, name): + self.name = name + + def tool(self, *a, **k): + def deco(fn): + return fn + + return deco + + fake_fastmcp = types.ModuleType("fastmcp") + fake_fastmcp.FastMCP = _FakeFastMCP + + fake_dotenv = types.ModuleType("dotenv") + fake_dotenv.load_dotenv = lambda *a, **k: None + + fake_main = types.ModuleType("memos.mem_os.main") + fake_main.MOS = type("MOS", (), {}) + fake_default_cfg = types.ModuleType("memos.mem_os.utils.default_config") + fake_default_cfg.get_default = lambda *a, **k: ({}, object()) + fake_user_mgr = types.ModuleType("memos.mem_user.user_manager") + fake_user_mgr.UserRole = type("UserRole", (), {"ADMIN": "ADMIN", "USER": "USER"}) + + stubs = { + "memos": fake_memos, + "memos.telemetry": fake_tel, + "fastmcp": fake_fastmcp, + "dotenv": fake_dotenv, + "memos.mem_os.main": fake_main, + "memos.mem_os.utils.default_config": fake_default_cfg, + "memos.mem_user.user_manager": fake_user_mgr, + } + saved = {k: sys.modules.get(k) for k in stubs} + sys.modules.update(stubs) + try: + spec = importlib.util.spec_from_file_location("_mcp_serve_under_test", _MCP_PATH) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + yield mod, calls + finally: + for k, v in saved.items(): + if v is None: + sys.modules.pop(k, None) + else: + sys.modules[k] = v + sys.modules.pop("_mcp_serve_under_test", None) + + +def test_mcp_server_bootstraps_telemetry(stubbed_mcp_serve, monkeypatch): + mod, calls = stubbed_mcp_serve + monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4317") + + # Pass an existing MOS instance so __init__ skips load_default_config(). + mod.MOSMCPServer(mos_instance=object()) + + assert calls["configure_from_env"] == 1, ( + "MCP server __init__ must call telemetry.configure_from_env() so the " + "'memos' service actually exports spans/metrics/logs over the MCP path" + ) + + +def test_bootstrap_helper_is_env_gated(stubbed_mcp_serve, monkeypatch): + mod, calls = stubbed_mcp_serve + monkeypatch.delenv("OTEL_EXPORTER_OTLP_ENDPOINT", raising=False) + mod._bootstrap_telemetry() + assert calls["configure_from_env"] == 1 # still called; returns False internally + + +# --------------------------------------------------------------------------- +# 3. End-to-end: once configured, a memory span carries service.name=memos +# (this is what makes the "memos" service appear in the backend) +# --------------------------------------------------------------------------- +def test_memory_span_service_name_is_memos(): + tel = _load_telemetry() + from opentelemetry.sdk.resources import Resource + from opentelemetry.semconv.resource import ResourceAttributes + + exporter = InMemorySpanExporter() + provider = TracerProvider(resource=Resource.create({ResourceAttributes.SERVICE_NAME: "memos"})) + provider.add_span_processor(SimpleSpanProcessor(exporter)) + trace.set_tracer_provider(provider) + tel._tracer = provider.get_tracer(tel.INSTRUMENT_NAME) + tel._initialized = True + + with tel.memory_span("add", tel.TIER_TEXTUAL, {tel.MEMORY_USER_ID: "u1"}): + pass + + spans = exporter.get_finished_spans() + assert len(spans) == 1 + assert spans[0].name == "memory.add" + assert spans[0].resource.attributes[ResourceAttributes.SERVICE_NAME] == "memos" diff --git a/tests/test_telemetry.py b/tests/test_telemetry.py new file mode 100644 index 000000000..b3355f321 --- /dev/null +++ b/tests/test_telemetry.py @@ -0,0 +1,234 @@ +""" +Validate MemOS OTel instrumentation (memory-semconv v0.1.0). + +Uses in-memory exporters — no external collector required. +""" + +import importlib.util +import pathlib + +import pytest + + +# OpenTelemetry is an optional (``otel``) extra — skip this module cleanly when a +# default install doesn't have it, rather than failing at import time. +pytest.importorskip("opentelemetry") + +from opentelemetry import metrics, trace +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.export import InMemoryMetricReader +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import SimpleSpanProcessor +from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter + + +# Load telemetry directly to avoid memos heavy __init__ chain +_tel_path = pathlib.Path(__file__).parent.parent / "src" / "memos" / "telemetry.py" +_spec = importlib.util.spec_from_file_location("memos.telemetry", _tel_path) +tel = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(tel) + + +@pytest.fixture(autouse=True) +def reset_telemetry(): + """Reset telemetry module globals between tests.""" + tel._initialized = False + tel._tracer = None + tel._meter = None + tel._otel_logger = None + tel._op_counter = None + tel._latency_histogram = None + tel._result_count_histogram = None + yield + tel._initialized = False + + +@pytest.fixture() +def in_memory_providers(): + """Wire up in-memory OTel providers and return exporters for assertions.""" + span_exporter = InMemorySpanExporter() + tracer_provider = TracerProvider() + tracer_provider.add_span_processor(SimpleSpanProcessor(span_exporter)) + trace.set_tracer_provider(tracer_provider) + tel._tracer = tracer_provider.get_tracer(tel.INSTRUMENT_NAME) + + metric_reader = InMemoryMetricReader() + meter_provider = MeterProvider(metric_readers=[metric_reader]) + metrics.set_meter_provider(meter_provider) + tel._meter = meter_provider.get_meter(tel.INSTRUMENT_NAME) + + tel._op_counter = tel._meter.create_counter("memory.operations") + tel._latency_histogram = tel._meter.create_histogram("memory.operation.duration") + tel._result_count_histogram = tel._meter.create_histogram("memory.result.count") + tel._initialized = True + + return span_exporter, metric_reader + + +def test_memory_span_creates_span_with_semconv_attrs(in_memory_providers): + span_exporter, _ = in_memory_providers + + with tel.memory_span("search", tel.TIER_TEXTUAL, {"memory.query.text": "test query"}): + pass + + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + span = spans[0] + + assert span.name == "memory.search" + assert span.attributes[tel.MEMORY_OPERATION] == "search" + assert span.attributes[tel.MEMORY_TIER] == tel.TIER_TEXTUAL + assert span.attributes["memory.query.text"] == "test query" + assert tel.MEMORY_LATENCY_MS in span.attributes + assert span.attributes[tel.MEMORY_LATENCY_MS] >= 0 + + +def test_memory_span_records_error_on_exception(in_memory_providers): + from opentelemetry.trace import StatusCode + + span_exporter, _ = in_memory_providers + + with pytest.raises(ValueError), tel.memory_span("add", tel.TIER_TEXTUAL): + raise ValueError("boom") + + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + assert spans[0].status.status_code == StatusCode.ERROR + + +def test_record_result_count_emits_metric(in_memory_providers): + _, metric_reader = in_memory_providers + + tel.record_result_count(5, tel.TIER_TEXTUAL) + + data = metric_reader.get_metrics_data() + names = { + metric.name + for rm in data.resource_metrics + for sm in rm.scope_metrics + for metric in sm.metrics + } + assert "memory.result.count" in names + + +def test_instrument_op_decorator_emits_span(in_memory_providers): + span_exporter, _ = in_memory_providers + + class FakeMemOS: + @tel.instrument_op("add", tel.TIER_TEXTUAL) + def add(self, messages=None, mem_cube_id=None, user_id=None, session_id=None): + return "done" + + obj = FakeMemOS() + result = obj.add( + messages=[{"role": "user", "content": "hi"}], user_id="u1", mem_cube_id="cube1" + ) + assert result == "done" + + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + span = spans[0] + assert span.name == "memory.add" + assert span.attributes[tel.MEMORY_OPERATION] == "add" + assert span.attributes[tel.MEMORY_TIER] == tel.TIER_TEXTUAL + assert span.attributes[tel.MEMORY_USER_ID] == "u1" + assert span.attributes[tel.MEMORY_CUBE_ID] == "cube1" + + +def test_tier_constants_match_semconv(): + """Tier names must be stable identifiers for downstream consumers.""" + assert tel.TIER_TEXTUAL == "textual" + assert tel.TIER_ACTIVATION == "activation" + assert tel.TIER_PARAMETRIC == "parametric" + assert tel.TIER_PREFERENCE == "preference" + + +def test_context_propagation_traceparent(in_memory_providers): + """Memory spans become children of a parent context from a traceparent header.""" + from opentelemetry import context as otel_context + from opentelemetry.propagate import extract + + span_exporter, _ = in_memory_providers + + # Simulate an agent sending a W3C traceparent header + agent_trace_id = "0af7651916cd43dd8448eb211c80319c" + agent_span_id = "b7ad6b7169203331" + carrier = {"traceparent": f"00-{agent_trace_id}-{agent_span_id}-01"} + parent_ctx = extract(carrier) + + token = otel_context.attach(parent_ctx) + try: + with tel.memory_span("search", tel.TIER_TEXTUAL, {"memory.query.text": "agent query"}): + pass + finally: + otel_context.detach(token) + + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + span = spans[0] + # Parent trace ID must match the agent's traceparent + assert format(span.context.trace_id, "032x") == agent_trace_id + # The span must have a parent (the agent's span) + assert span.parent is not None + assert format(span.parent.span_id, "016x") == agent_span_id + + +def test_instrument_op_product_api_positional_arg(in_memory_providers): + """ + ISI-2031: the product-API entry layer (SingleCubeView.add_memories / + search_memories) takes a single positional ``*_req`` argument, so the + decorator's kwargs-based user_id/cube_id/session_id extraction falls back + to empty strings. The span + counter must still fire (resource-level + memory.sut.name=memos is the DoD requirement), and record_result_count + must emit the result-count histogram. + """ + span_exporter, metric_reader = in_memory_providers + + class FakeReq: + user_id = "u1" + session_id = "sess1" + + class FakeProductView: + @tel.instrument_op("search", tel.TIER_TEXTUAL) + def search_memories(self, search_req): # positional, like SingleCubeView + # Mirror the result-count call added in single_cube.search_memories + tel.record_result_count(3, tel.TIER_TEXTUAL, "search") + return {"text_mem": [{"total_nodes": 3}]} + + view = FakeProductView() + result = view.search_memories(FakeReq()) + assert result["text_mem"][0]["total_nodes"] == 3 + + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + span = spans[0] + assert span.name == "memory.search" + assert span.attributes[tel.MEMORY_OPERATION] == "search" + assert span.attributes[tel.MEMORY_TIER] == tel.TIER_TEXTUAL + # Positional-arg fallback: attrs default to empty strings (documented). + assert span.attributes[tel.MEMORY_USER_ID] == "" + assert span.attributes[tel.MEMORY_CUBE_ID] == "" + + # Counter + result-count histogram must be present. + data = metric_reader.get_metrics_data() + names = { + metric.name + for rm in data.resource_metrics + for sm in rm.scope_metrics + for metric in sm.metrics + } + assert "memory.operations" in names + assert "memory.result.count" in names + + +def test_memory_span_all_operations(in_memory_providers): + span_exporter, _ = in_memory_providers + + for op in ("add", "search", "update", "delete"): + with tel.memory_span(op, tel.TIER_TEXTUAL): + pass + + spans = span_exporter.get_finished_spans() + assert len(spans) == 4 + ops = {s.attributes[tel.MEMORY_OPERATION] for s in spans} + assert ops == {"add", "search", "update", "delete"}