Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions .github/workflows/pr-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,35 @@ jobs:

- name: Check formatting with Black
run: tox -e black
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install -e .[dev]
python -m pip install -e "src/_langchain/opentelemetry-instrumentation-langchain[instruments,test]"

- name: Lint with Pylint
- name: Check formatting with Black
run: black --check src tests samples

- name: Lint with Pylint (root)
run: pylint src tests

- name: Lint with Pylint (langchain)
working-directory: src/_langchain/opentelemetry-instrumentation-langchain
run: tox -e pylint

- name: Typecheck with Mypy
run: tox -e mypy
- name: Typecheck with Mypy (root)
run: mypy src

- name: Typecheck with Mypy (langchain)
working-directory: src/_langchain/opentelemetry-instrumentation-langchain
run: mypy src --ignore-missing-imports --allow-untyped-defs

- name: Typecheck with Pyright (root)
run: pyright src

- name: Typecheck with Pyright (langchain)
working-directory: src/_langchain/opentelemetry-instrumentation-langchain
run: pyright src

tests:
name: Pytest (${{ matrix.python-version }})
Expand Down
1 change: 1 addition & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ disallow_untyped_defs = true
namespace_packages = true
explicit_package_bases = true
mypy_path = src
exclude = (?x)(src/_langchain/ | src/_a365/)

[mypy-microsoft.azureMonitor.*]
disallow_untyped_defs = false
Expand Down
30 changes: 30 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ target-version = ["py310"]

[tool.pylint.main]
py-version = "3.10"
ignore-paths = ["tests/azureMonitor", "src/_langchain"]

[tool.pylint.format]
max-line-length = 120
Expand All @@ -65,5 +66,34 @@ disable = [
"invalid-name",
]

[tool.mypy]
python_version = "3.10"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
namespace_packages = true
explicit_package_bases = true
mypy_path = "src"
exclude = ["src/_langchain"]

[[tool.mypy.overrides]]
module = "microsoft.azureMonitor.*"
disallow_untyped_defs = false
ignore_missing_imports = true

[[tool.mypy.overrides]]
module = [
"django.*",
"azure.core.*",
"opentelemetry.instrumentation.*",
]
ignore_missing_imports = true

[tool.pytest.ini_options]
testpaths = ["tests"]

[tool.pyright]
pythonVersion = "3.10"
typeCheckingMode = "basic"
ignore = ["src/microsoft/azureMonitor", "tests/azureMonitor"]
exclude = ["src/_langchain"]
3 changes: 1 addition & 2 deletions samples/langchain/sample_langchain_azure.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,7 @@ def find_activities(destination: str) -> str:
def main():
session_id = str(uuid4())
user_request = (
"Plan a weekend trip to Paris from Seattle next month. "
+ "Find flights, a boutique hotel, and fun activities."
"Plan a weekend trip to Paris from Seattle next month. Find flights, a boutique hotel, and fun activities."
)

print("Travel Planner (Nested Agents)")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pytest>=8.0
pytest-recording
Comment thread
rads-1996 marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "opentelemetry-instrumentation-langchain"
dynamic = ["version"]
description = "OpenTelemetry Official Langchain instrumentation"
license = "Apache-2.0"
requires-python = ">=3.9"
authors = [
{ name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" },
]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
]
dependencies = [
"opentelemetry-api ~= 1.40",
"opentelemetry-instrumentation ~= 0.61b0",
"opentelemetry-semantic-conventions ~= 0.61b0",
]

[project.optional-dependencies]
instruments = [
"langchain >= 0.3.21",
"langchain-core >= 0.3",
]
test = [
"opentelemetry-sdk ~= 1.40",
"langchain-openai >= 0.3",
"openai >= 1.0",
"PyYAML >= 6.0",
"vcrpy >= 6.0",
"pytest >= 8.0",
"pytest-recording",
]

[project.entry-points.opentelemetry_instrumentor]
langchain = "opentelemetry.instrumentation.langchain:LangChainInstrumentor"

[tool.hatch.version]
path = "src/opentelemetry/instrumentation/langchain/version.py"

[tool.hatch.build.targets.sdist]
include = [
"/src",
"/tests",
]

[tool.hatch.build.targets.wheel]
packages = ["src/opentelemetry"]

[tool.pylint.format]
max-line-length = 120

[tool.pylint.messages_control]
disable = [
"missing-function-docstring",
"missing-module-docstring",
"import-error",
"no-name-in-module",
"too-many-positional-arguments",
]

[tool.pyright]
pythonVersion = "3.9"
typeCheckingMode = "basic"
reportMissingImports = false
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Copyright The OpenTelemetry Authors

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to copy the code over or can we take a reference to OTel instrumentation

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This instrumentation package in otel is not published yet, therefore we need to copy the code till the upstream package is released and in a good state. Until then we will maintain this internally in the distro.

#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Langchain instrumentation supporting `ChatOpenAI` and `ChatBedrock`, it can be enabled by
using ``LangChainInstrumentor``. Other providers/LLMs may be supported in the future and
telemetry for them is skipped for now.

Usage
-----
.. code:: python
from opentelemetry.instrumentation.langchain import LangChainInstrumentor
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_openai import ChatOpenAI

LangChainInstrumentor().instrument()
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0, max_tokens=1000)
messages = [
SystemMessage(content="You are a helpful assistant!"),
HumanMessage(content="What is the capital of France?"),
]
result = llm.invoke(messages)
LangChainInstrumentor().uninstrument()

API
---
"""

from typing import Any, Callable, Collection

from langchain_core.callbacks import BaseCallbackHandler
Comment thread
rads-1996 marked this conversation as resolved.
from wrapt import wrap_function_wrapper # type: ignore

from opentelemetry.instrumentation.instrumentor import BaseInstrumentor # type: ignore[attr-defined]
from opentelemetry.instrumentation.langchain.callback_handler import (
OpenTelemetryLangChainCallbackHandler,
)
from opentelemetry.instrumentation.langchain.package import _instruments
from opentelemetry.instrumentation.utils import unwrap
from opentelemetry.util.genai.handler import get_telemetry_handler


class LangChainInstrumentor(BaseInstrumentor):
"""
OpenTelemetry instrumentor for LangChain.
This adds a custom callback handler to the LangChain callback manager
to capture LLM telemetry.
"""

def instrumentation_dependencies(self) -> Collection[str]:
return _instruments # type: ignore[no-any-return]

def _instrument(self, **kwargs: Any):
"""
Enable Langchain instrumentation.
"""
tracer_provider = kwargs.get("tracer_provider")
meter_provider = kwargs.get("meter_provider")
logger_provider = kwargs.get("logger_provider")

telemetry_handler = get_telemetry_handler(
tracer_provider=tracer_provider,
meter_provider=meter_provider,
logger_provider=logger_provider,
)
otel_callback_handler = OpenTelemetryLangChainCallbackHandler(
telemetry_handler=telemetry_handler,
)

wrap_function_wrapper(
module="langchain_core.callbacks",
name="BaseCallbackManager.__init__",
wrapper=_BaseCallbackManagerInitWrapper(otel_callback_handler),
)

def _uninstrument(self, **kwargs: Any):
"""
Cleanup instrumentation (unwrap).
"""
unwrap("langchain_core.callbacks.base.BaseCallbackManager", "__init__")
# Clear the TelemetryHandler singleton so the next instrument() uses
# the provided tracer_provider/meter_provider/logger_provider instead
# of reusing the previous handler.
if getattr(get_telemetry_handler, "_default_handler", None) is not None:
delattr(get_telemetry_handler, "_default_handler")
Comment thread
rads-1996 marked this conversation as resolved.


class _BaseCallbackManagerInitWrapper: # pylint: disable=too-few-public-methods
"""
Wrap the BaseCallbackManager __init__ to insert custom callback handler in the manager's handlers list.
"""

def __init__( # pylint: disable=too-few-public-methods
self, callback_handler: OpenTelemetryLangChainCallbackHandler
):
self._otel_handler = callback_handler

def __call__(
self,
wrapped: Callable[..., None],
instance: BaseCallbackHandler,
args: tuple[Any, ...],
kwargs: dict[str, Any],
):
wrapped(*args, **kwargs)
# Ensure our OTel callback is present if not already.
for handler in instance.inheritable_handlers: # type: ignore
if isinstance(handler, type(self._otel_handler)):
break
else:
instance.add_handler(self._otel_handler, inherit=True) # type: ignore
Comment thread
rads-1996 marked this conversation as resolved.
Loading
Loading