Skip to content
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

feat(llmobs): support in-code config for llmobs #9172

Merged
merged 48 commits into from
May 17, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
1e23c2b
support minimal env var conifg for llmobs
May 6, 2024
2332e99
expose explicit integration
May 7, 2024
a46041f
hide patch methods
May 7, 2024
5bd9768
flip apm_enabled default to true
May 7, 2024
3022d19
comments & renaming
May 7, 2024
adb4957
rev test change
May 7, 2024
80699a3
lint
May 8, 2024
655bed5
make sure config values r updated
May 8, 2024
524dd79
grab dd site and api key from env
May 9, 2024
6688219
fix getenv
May 10, 2024
d0cbb04
update prioriryt of env vs args
May 10, 2024
659d2e0
dd_apm_enabled shouldn't override env vars
May 13, 2024
55eb22b
fix tests
May 13, 2024
6afa4cf
llmobs no apm arg
May 13, 2024
2142be6
fix prio for env and service
May 13, 2024
63994f7
remove toggling stuff
May 13, 2024
ef9b21f
no need to toggle
May 13, 2024
6c33acc
set minimal env vars
May 13, 2024
1342865
Update ddtrace/llmobs/_llmobs.py
lievan May 14, 2024
368bf40
Update ddtrace/llmobs/_llmobs.py
lievan May 14, 2024
381fd37
args > config, simplify patching logic
May 14, 2024
6c42a15
update comment, del integration constants
May 14, 2024
07becc9
fix _llmobs_pc_sampler missing err
May 14, 2024
735d9c2
try catch patch all integrations
May 14, 2024
59c163b
Merge branch 'main' into evan.li/in-app-config
Yun-Kim May 14, 2024
c7c4305
refactor
Yun-Kim May 14, 2024
1573d62
remove LLMObs enabling from integration patch code
Yun-Kim May 14, 2024
d486ad8
disable llmobs
May 15, 2024
4418c73
Merge branch 'main' into evan.li/in-app-config
lievan May 15, 2024
492c09f
disable llmobs after bedrock tests
May 15, 2024
d21b6fe
Merge branch 'evan.li/in-app-config' of github.com:DataDog/dd-trace-p…
May 15, 2024
9ab61e5
disable llmobs after mock llmobs writer
May 15, 2024
39b236e
need to explicitly check for None
May 15, 2024
e981988
Merge branch 'main' into evan.li/in-app-config
lievan May 15, 2024
e50c11d
Merge branch 'evan.li/in-app-config' of github.com:DataDog/dd-trace-p…
May 15, 2024
3a9e810
don't uneessarily patch other integratins
May 15, 2024
db8465a
Merge branch 'main' into evan.li/in-app-config
lievan May 15, 2024
26606af
two more enable fixes
May 15, 2024
9f9adbc
Merge branch 'main' into evan.li/in-app-config
lievan May 16, 2024
af75319
Update ddtrace/llmobs/_llmobs.py
lievan May 16, 2024
45179ad
change arg names, make tracer private
May 16, 2024
d6b35e8
fix test, update comments
May 16, 2024
06963cb
Merge branch 'main' into evan.li/in-app-config
lievan May 17, 2024
c472f95
llmobs agentless enabled
May 17, 2024
90c3ba3
Merge branch 'evan.li/in-app-config' of github.com:DataDog/dd-trace-p…
May 17, 2024
d411fdc
debug log
May 17, 2024
fe73fb6
agentless enabled
May 17, 2024
edb3cd4
Merge branch 'main' into evan.li/in-app-config
Yun-Kim May 17, 2024
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
127 changes: 123 additions & 4 deletions ddtrace/llmobs/_llmobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,19 @@
import os
from typing import Any
from typing import Dict
from typing import List
from typing import Optional
from typing import Union

import ddtrace
from ddtrace import Span
from ddtrace import config
from ddtrace import patch
from ddtrace.ext import SpanTypes
from ddtrace.internal import atexit
from ddtrace.internal.logger import get_logger
from ddtrace.internal.service import Service
from ddtrace.internal.utils.formats import asbool
from ddtrace.llmobs._constants import INPUT_DOCUMENTS
from ddtrace.llmobs._constants import INPUT_MESSAGES
from ddtrace.llmobs._constants import INPUT_PARAMETERS
Expand Down Expand Up @@ -44,6 +47,25 @@ class LLMObs(Service):
_instance = None
enabled = False

# Support llmobs python integrations
langchain = "langchain"
openai = "openai"
botocore = "botocore"

# APM enabled env var config:
_apm_env_config = {
"DD_LLMOBS_NO_APM": "0",
}

# APM disabled env var config:
_no_apm_env_config = {
"DD_INSTRUMENTATION_TELEMETRY_ENABLED": "0",
"DD_REMOTE_CONFIGURATION_ENABLED": "0",
"DD_OPENAI_METRICS_ENABLED": "0",
"DD_LANGCHAIN_METRICS_ENABLED": "0",
"DD_LLMOBS_NO_APM": "1",
}

def __init__(self, tracer=None):
super(LLMObs, self).__init__()
self.tracer = tracer or ddtrace.tracer
Expand Down Expand Up @@ -75,30 +97,127 @@ def _stop_service(self) -> None:
log.warning("Failed to shutdown tracer", exc_info=True)

@classmethod
def enable(cls, tracer=None):
def _toggle(cls, switch: bool) -> None:
# _toggle is a helper function to ensure the state of config._llmobs_enabled is in sync with the class attribute
# since our integrations depend on config._llmobs_enabled to set llmobs data
config._llmobs_enabled = switch
cls.enabled = switch

@classmethod
def enable(
cls,
ml_app: Optional[str] = None,
integrations: Optional[List[str]] = None,
dd_apm_enabled=True,
sabrenner marked this conversation as resolved.
Show resolved Hide resolved
dd_site: Optional[str] = None,
dd_api_key: Optional[str] = None,
dd_env: Optional[str] = None,
dd_service: Optional[str] = None,
tracer=None,
):
"""
Enable LLM Observability tracing.

:param str ml_app: The name of your ml application.
:param List[str] integrations: A list of integrations to enable auto-tracing for.
:param str dd_apm_enabled: Whether Datadog Application Performance Monitoring is enabled
:param str dd_site: Your datadog site, override by DD_SITE.
:param str dd_api_key: Your datadog api key.
:param str dd_env: Your environment name.
:param str dd_service: Your service name.
"""
if cls._instance is not None:
log.debug("%s already enabled", cls.__name__)
return

if os.getenv("DD_LLMOBS_ENABLED") and not asbool(os.getenv("DD_LLMOBS_ENABLED")):
LLMObs._toggle(False)
log.debug("LLMObs.enable() called when DD_LLMOBS_ENABLED is set to false, not starting LLMObs service")
return

# grab required values for LLMObs
# reading from environment variables is needed in case preload script wasn't called.
config._dd_site = config._dd_site or os.getenv("DD_SITE") or dd_site
lievan marked this conversation as resolved.
Show resolved Hide resolved
lievan marked this conversation as resolved.
Show resolved Hide resolved
config._dd_api_key = config._dd_api_key or os.getenv("DD_API_KEY") or dd_api_key
config._llmobs_ml_app = config._llmobs_ml_app or os.getenv("DD_LLMOBS_APP_NAME") or ml_app
lievan marked this conversation as resolved.
Show resolved Hide resolved

# validate required values for LLMObs
if not config._dd_api_key:
cls.enabled = False
Yun-Kim marked this conversation as resolved.
Show resolved Hide resolved
LLMObs._toggle(False)
lievan marked this conversation as resolved.
Show resolved Hide resolved
raise ValueError(
"DD_API_KEY is required for sending LLMObs data. "
"Ensure this configuration is set before running your application."
)
if not config._dd_site:
LLMObs._toggle(False)
lievan marked this conversation as resolved.
Show resolved Hide resolved
raise ValueError(
"DD_SITE is required for sending LLMObs data. "
"Ensure this configuration is set before running your application."
)
if not config._llmobs_ml_app:
cls.enabled = False
LLMObs._toggle(False)
lievan marked this conversation as resolved.
Show resolved Hide resolved
raise ValueError(
"DD_LLMOBS_APP_NAME is required for sending LLMObs data. "
"Ensure this configuration is set before running your application."
)

# grab optional values
config.env = dd_env or config.env
config.service = dd_service or config.service

env_update = cls._apm_env_config if dd_apm_enabled else cls._no_apm_env_config
for k, v in env_update.items():
# don't override environment variables are are explicitly set.
if not os.getenv(k):
os.environ[k] = v
# update config object to ensure that the values are set correctly
if k == "DD_INSTRUMENTATION_TELEMETRY_ENABLED":
config._telemetry_enabled = False
if k == "DD_REMOTE_CONFIGURATION_ENABLED":
config._remote_config_enabled = False

# enable LLMObs integations
llmobs_integrations = {
LLMObs.langchain: lambda: LLMObs._patch_langchain(),
LLMObs.openai: lambda: LLMObs._patch_openai(),
LLMObs.botocore: lambda: LLMObs._patch_bedrock(),
}
if integrations:
for integration in integrations:
if integration in llmobs_integrations:
lievan marked this conversation as resolved.
Show resolved Hide resolved
llmobs_integrations[integration]()
lievan marked this conversation as resolved.
Show resolved Hide resolved
else:
log.warning(
"%s is unsupported - LLMObs currently supports %s", integration, str(llmobs_integrations)
lievan marked this conversation as resolved.
Show resolved Hide resolved
)

cls._instance = cls(tracer=tracer)
cls.enabled = True
LLMObs._toggle(True)
lievan marked this conversation as resolved.
Show resolved Hide resolved
cls._instance.start()
atexit.register(cls.disable)
log.debug("%s enabled", cls.__name__)

@classmethod
lievan marked this conversation as resolved.
Show resolved Hide resolved
def _patch_langchain(cls) -> None:
if cls._instance is None:
log.warning("%s not enabled, cannot patch langchain", cls.__name__)
return
patch(langchain=True)

@classmethod
def _patch_openai(cls) -> None:
if cls._instance is None:
log.warning("%s not enabled, cannot patch openai", cls.__name__)
return
patch(openai=True)

@classmethod
def _patch_bedrock(cls) -> None:
if cls._instance is None:
log.warning("%s not enabled, cannot patch bedrock", cls.__name__)
return
patch(bedrock=True)

@classmethod
def disable(cls) -> None:
if cls._instance is None:
Expand Down
28 changes: 28 additions & 0 deletions tests/llmobs/test_llmobs_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import mock
import pytest

from ddtrace._trace.span import Span
from ddtrace.ext import SpanTypes
from ddtrace.llmobs import LLMObs as llmobs_service
from ddtrace.llmobs._constants import INPUT_DOCUMENTS
from ddtrace.llmobs._constants import INPUT_MESSAGES
Expand Down Expand Up @@ -36,6 +38,16 @@ def mock_logs():
yield mock_logs


def run_llmobs_trace_filter(dummy_tracer):
for trace_filter in dummy_tracer._filters:
if isinstance(trace_filter, LLMObsTraceProcessor):
root_llm_span = Span(name="span1", span_type=SpanTypes.LLM)
root_llm_span.set_tag_str(SPAN_KIND, "llm")
trace1 = [root_llm_span]
return trace_filter.process_trace(trace1)
raise ValueError("LLMObsTraceProcessor not found in tracer filters.")


def test_service_enable():
with override_global_config(dict(_dd_api_key="<not-a-real-api-key>", _llmobs_ml_app="<ml-app-name>")):
dummy_tracer = DummyTracer()
Expand All @@ -45,6 +57,22 @@ def test_service_enable():
assert llmobs_service.enabled
assert llmobs_instance.tracer == dummy_tracer
assert any(isinstance(tracer_filter, LLMObsTraceProcessor) for tracer_filter in dummy_tracer._filters)
assert run_llmobs_trace_filter(dummy_tracer) is not None

llmobs_service.disable()


def test_service_enable_with_apm_disabled():
with override_global_config(dict(_dd_api_key="<not-a-real-api-key>", _llmobs_ml_app="<ml-app-name>")):
dummy_tracer = DummyTracer()
llmobs_service.enable(tracer=dummy_tracer, dd_apm_enabled=False)
llmobs_instance = llmobs_service._instance
assert llmobs_instance is not None
assert llmobs_service.enabled
assert llmobs_instance.tracer == dummy_tracer
assert any(isinstance(tracer_filter, LLMObsTraceProcessor) for tracer_filter in dummy_tracer._filters)
assert run_llmobs_trace_filter(dummy_tracer) is None

llmobs_service.disable()


Expand Down
Loading