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

Datadog observability driver #998

Merged
merged 20 commits into from
Jul 19, 2024
Merged
Show file tree
Hide file tree
Changes from 15 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `DummyObservabilityDriver` as a no-op Observability Driver.
- `OpenTelemetryObservabilityDriver` for sending observability data to an open telemetry collector or vendor.
- `GriptapeCloudObservabilityDriver` for sending observability data to Griptape Cloud.
- `DatadogObservabilityDriver` for sending observability data to a Datadog Agent.
- `Observability` context manager for enabling observability and configuring which Observability Driver to use.
- `@observable` decorator for selecting which functions/methods to provide observability for.

Expand Down
2 changes: 2 additions & 0 deletions griptape/drivers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@
from .observability.no_op_observability_driver import NoOpObservabilityDriver
from .observability.open_telemetry_observability_driver import OpenTelemetryObservabilityDriver
from .observability.griptape_cloud_observability_driver import GriptapeCloudObservabilityDriver
from .observability.datadog_observability_driver import DatadogObservabilityDriver

__all__ = [
"BasePromptDriver",
Expand Down Expand Up @@ -211,4 +212,5 @@
"NoOpObservabilityDriver",
"OpenTelemetryObservabilityDriver",
"GriptapeCloudObservabilityDriver",
"DatadogObservabilityDriver",
]
22 changes: 22 additions & 0 deletions griptape/drivers/observability/datadog_observability_driver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import os

from attrs import Factory, define, field
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.trace import SpanProcessor
from opentelemetry.sdk.trace.export import BatchSpanProcessor

from griptape.drivers.observability.open_telemetry_observability_driver import OpenTelemetryObservabilityDriver


@define
class DatadogObservabilityDriver(OpenTelemetryObservabilityDriver):
datadog_agent_endpoint: str = field(
default=Factory(lambda: os.getenv("DD_AGENT_ENDPOINT", "http://localhost:4318")), kw_only=True
)
span_processor: SpanProcessor = field(
default=Factory(
lambda self: BatchSpanProcessor(OTLPSpanExporter(endpoint=self.datadog_agent_endpoint + "/v1/traces")),
Copy link
Member

Choose a reason for hiding this comment

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

Use f-string instead of string concatenation.

takes_self=True,
),
kw_only=True,
)
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,28 @@

@define
class OpenTelemetryObservabilityDriver(BaseObservabilityDriver):
service_name: str = field(kw_only=True)
service_name: str = field(default="griptape", kw_only=True)
Copy link
Contributor

Choose a reason for hiding this comment

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

Small refactoring opportunity: If we have a default here, then we don't really need it in GriptapeObservabilityDriver anymore (which also mean that we need to override trace_provider there too).

Though its fine by me if you don't want to do it in this PR

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I assumed GriptapeCloud used a non-attribute trace_provider on purpose. If we are OK with GriptapeCloud using the OpenTelemetry tracer_provider, then happy to. Just not clear why it used its own trace_provider in the first place

Copy link
Contributor

@dylanholmes dylanholmes Jul 18, 2024

Choose a reason for hiding this comment

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

tracer_provider was just added GriptapeObservabilityDriver in order to work around some attrs awkwardness. The resource arg was left out because Griptape Cloud doesn't really need them, but they don't hurt either. I'd personally prefer simpler code

span_processor: SpanProcessor = field(kw_only=True)
service_version: Optional[str] = field(default=None, kw_only=True)
deployment_env: Optional[str] = field(default=None, kw_only=True)
trace_provider: TracerProvider = field(
default=Factory(
lambda self: TracerProvider(resource=Resource(attributes={"service.name": self.service_name})),
lambda self: self._trace_provider_factory(),
takes_self=True,
),
kw_only=True,
)
_tracer: Optional[Tracer] = None
_root_span_context_manager: Any = None

def _trace_provider_factory(self) -> TracerProvider:
attributes = {"service.name": self.service_name}
if self.service_version:
Copy link
Member

Choose a reason for hiding this comment

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

Use explicit is not None check

attributes["service.version"] = self.service_version
if self.deployment_env:
Copy link
Member

Choose a reason for hiding this comment

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

is not None

attributes["deployment.environment"] = self.deployment_env
return TracerProvider(resource=Resource(attributes=attributes)) # pyright: ignore[reportArgumentType]

def __attrs_post_init__(self) -> None:
self.trace_provider.add_span_processor(self.span_processor)
self._tracer = get_tracer(self.service_name, tracer_provider=self.trace_provider)
Expand Down
3 changes: 2 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,13 @@ drivers-observability-griptape-cloud = [
"opentelemetry-instrumentation-threading",
"opentelemetry-exporter-otlp-proto-http",
]
drivers-observability-datadog = [
"opentelemetry-sdk",
"opentelemetry-api",
"opentelemetry-instrumentation",
"opentelemetry-instrumentation-threading",
"opentelemetry-exporter-otlp-proto-http",
]

loaders-dataframe = ["pandas"]
loaders-pdf = ["pypdf"]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import os
from unittest import mock

from griptape.drivers import DatadogObservabilityDriver


class TestDatadogTelemetryObservabilityDriver:
def test_init(self):
driver = DatadogObservabilityDriver()

assert driver.service_name == "griptape"
assert driver.datadog_agent_endpoint == "http://localhost:4318"

@mock.patch.dict(os.environ, {"DD_AGENT_ENDPOINT": "http://griptape.ai:1234"})
def test_init_env_var_dd_agent(self):
driver = DatadogObservabilityDriver()

assert driver.datadog_agent_endpoint == "http://griptape.ai:1234"

def test_init_set_dd_agent(self):
driver = DatadogObservabilityDriver(datadog_agent_endpoint="http://griptape.ai:4321")

assert driver.datadog_agent_endpoint == "http://griptape.ai:4321"

def test_init_set_service_name(self):
driver = DatadogObservabilityDriver(service_name="test")

assert driver.service_name == "test"
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,23 @@ def span_processor(self, mock_span_exporter):

@pytest.fixture()
def driver(self, span_processor):
return OpenTelemetryObservabilityDriver(service_name="test", span_processor=span_processor)
return OpenTelemetryObservabilityDriver(span_processor=span_processor)

def test_init(self, span_processor):
OpenTelemetryObservabilityDriver(service_name="test", span_processor=span_processor)
def test_init_no_optional(self, span_processor):
driver = OpenTelemetryObservabilityDriver(span_processor=span_processor)

assert driver.service_name == "griptape"
assert driver.service_version is None
assert driver.deployment_env is None

def test_init_all_optional(self, span_processor):
driver = OpenTelemetryObservabilityDriver(
service_name="griptape", service_version="1.0", deployment_env="test", span_processor=span_processor
)

assert driver.service_name == "griptape"
assert driver.service_version == "1.0"
assert driver.deployment_env == "test"

def test_context_manager_pass(self, driver, mock_span_exporter):
expected_spans = ExpectedSpans(spans=[ExpectedSpan(name="main", parent=None, status_code=StatusCode.OK)])
Expand Down
Loading