Repository navigation
Add otel langchain instrumentation #20
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f1ca02c
8b0c365
168738e
422b568
05ba8c8
553308d
4c93064
ec2ce21
99870eb
b9aeffb
5ada14d
570830c
bcaf1b9
01a6b63
110766e
b1cfab6
8c2d5de
f20a9f5
b42387c
c0661b7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| pytest>=8.0 | ||
| pytest-recording | ||
| 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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
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") | ||
|
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 | ||
|
rads-1996 marked this conversation as resolved.
|
||
Uh oh!
There was an error while loading. Please reload this page.