Check out the KubeCon 2024 Wrap-up Read More

OpenTelemetry DataDog Receiver How To

Jon Reeve November 8, 2024

In the blog, we’ll explore the OpenTelemetry DataDog Receiver kindly donated to the community by Grafana. We’ll review why you might want to convert your DataDog Metrics and Traces into OpenTelemetry (OTel) format and a step-by-step walkthrough.

No More Roach OTel!

So nobody wants a “Roach OTel” – a place where OTel can check in but never check out! One of the drivers for OpenTelemetry is to enable choice with a wide potential variety of “observability backends”.

In our case, we are assuming an existing application, already instrumented with ddtrace – DataDog’s tracing library . Maybe we’re looking to start migrating to OpenTelmetry, leveraging other observability backends, or simply getting more control over our existing telemetry. Let’s take a look.

OpenTelemetry Collector Agent Deployment Pattern

What We’re Going To Do

In this blog, we’re going to instrument an example Python application with DataDog’s tracing library ddtrace – and we’re going to intercept traces thrown off by that with an OpenTelemetry collector running a DataDog receiver. We’re then going to route those traces to a “debug exporter” so we can view them in our terminal. We’ll run the Python Flask app in a virtual Python environment, and the OTel collector as a Docker container.

A Python App instrumented with “ddtrace” sending traces to an OTel Collector

Our Application

Let’s take a simple Python application – we can use the dice roll example Python Flask app from the OpenTelmetry docs, except this time, we’re going to instrument the app with DataDog before converting it!

Set up your environment

mkdir otel-getting-started
cd otel-getting-started
python3 -m venv venv
source ./venv/bin/activate

Install Flask

pip install flask

We’re also going to want to install the DataDog APM tracing library ddtrace that we’re going to use to trace the application in this case

pip install ddtrace

Create the code in app.py

from random import randint
from flask import Flask, request
import logging

app = Flask(__name__)
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


@app.route("/rolldice")
def roll_dice():
    player = request.args.get('player', default=None, type=str)
    result = str(roll())
    if player:
        logger.warning("%s is rolling the dice: %s", player, result)
    else:
        logger.warning("Anonymous player is rolling the dice: %s", result)
    return result


def roll():
    return randint(1, 6)

Then run the app with the ddtrace library by using ddtrace-run

ddtrace-run flask run -p 8080

We can then visit http://localhost:8080/rolldice on the browser or curl like

curl 'http://127.0.0.1:8080/rolldice?player=king'

Run an OpenTelemetry Collector

The OpenTelemetry collector is one of the key pieces in the OTel project, allowing telemetry to be received with “receivers”, (optionally) processed with processors, and exported to observability backends (or other collectors..) with “exporters”. In our case, we’re going to use the DataDog receiver and a “debug” exporter, we’ll skip the processor for now but these are recommended for production deployments. The debug exporter exports telemetry to the console and is helpful to validate that things are working and flowing! You can also set different verbosity levels including emitting all the fields for each telemetry record.

Blowing Up Our OTel Collector

To run the collector, we’re going to use Docker (so you’ll need Docker installed). Looking at the docs, we can see that we can pass in a collector configuration file mounted as a volume

docker run -v $(pwd)/config.yaml:/etc/otelcol-contrib/config.yaml otel/opentelemetry-collector-contrib:0.110.0

The collector configuration file is in YAML format, and defines the various receivers, processors, exporters and other components that can be leveraged by the collector. Looking back at the docs for the DataDog receiver, we can see a minimal configuration to get the receiver going:

receivers:
  datadog:
    endpoint: 0.0.0.0:8126
    read_timeout: 60s

exporters:
  debug:

service:
  pipelines:
    metrics:
      receivers: [datadog]
      exporters: [debug]
    traces:
      receivers: [datadog]
      exporters: [debug]

Looking at the configuration, we start with the DataDog receiver – we’re going to run this on our local host and it will listen on port 8126 – the port that the DataDog agent (not running here) listens on by default and also the default port that ddtrace sends info to. We can see that our debug exporter is defined and per the docs defaults to a basic verbosity which should output “a single-line summary of received data with a total count of telemetry records for every batch of received logs, metrics or traces” – cool. Finally, we have our “pipelines” which are defined per telemetry type (logs, metrics, traces and more coming!), and tie together our inputs, processing via processors (if we had some) and where to send the telemetry via exporters (to the debug exporter in our case)

Save the above file as datadog_config.yaml or similar and then run the collector – we also want to expose port 8126 on our receiver in the container since this is the port our receiver is listening on.

docker run -v $(pwd)/datadog_config.yaml:/etc/otelcol-contrib/config.yaml -p 8126:8126 otel/opentelemetry-collector-contrib:0.110.0

Putting it all together

Now let’s run our app again and query it with curl or in the browser at http://localhost:8080/rolldice 

ddtrace-run flask run -p 8080

In your console where you ran the collector, you should see a message coming from the debug exporter like

2024-10-02T19:11:25.072Z	info	TracesExporter	{"kind": "exporter", "data_type": "traces", "name": "debug", "resource spans": 1, "spans": 9}

Yay! Looks like we got a trace…..Remember we were running the debug exporter with the default basic verbosity – let’s turn it up to 11 and a a verbosity of detailed. We can modify our collector configuration file as follows

receivers:
  datadog:
    endpoint: 0.0.0.0:8126
    read_timeout: 60s

exporters:
  debug:
    verbosity: detailed

service:
  pipelines:
    metrics:
      receivers: [datadog]
      exporters: [debug]
    traces:
      receivers: [datadog]
      exporters: [debug]

Save the configuration file, re-run your container and query your app again – you should see something like

2024-10-02T19:21:10.142Z	info	TracesExporter	{"kind": "exporter", "data_type": "traces", "name": "debug", "resource spans": 1, "spans": 9}
2024-10-02T19:21:10.143Z	info	ResourceSpans #0
Resource SchemaURL: https://opentelemetry.io/schemas/1.16.0
Resource attributes:
     -> telemetry.sdk.name: Str(Datadog)
     -> telemetry.sdk.language: Str(python)
     -> process.runtime.version: Str(3.12.6)
     -> telemetry.sdk.version: Str(Datadog-2.14.1)
     -> service.name: Str(flask)
ScopeSpans #0
ScopeSpans SchemaURL: 
InstrumentationScope Datadog 2.14.1
Span #0
    Trace ID       : 00000000000000005df358c588cf97cc
    Parent ID      : 
    ID             : 07e7cde5f7bc2378
    Name           : flask.request
    Kind           : Server
    Start time     : 2024-10-02 19:21:10.10128 +0000 UTC
    End time       : 2024-10-02 19:21:10.105011 +0000 UTC
    Status code    : Ok
    Status message : 
Attributes:
     -> dd.span.Resource: Str(GET /rolldice)
     -> sampling.priority: Str(1.000000)
     -> datadog.span.id: Str(569650265473164152)
     -> datadog.trace.id: Str(6769852270295095244)
     -> flask.endpoint: Str(roll_dice)
     -> flask.url_rule: Str(/rolldice)
     -> _dd.p.tid: Str(66fd9d2600000000)
     -> http.method: Str(GET)
     -> http.useragent: Str(Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15)
     -> _dd.p.dm: Str(-0)
     -> component: Str(flask)
     -> flask.version: Str(3.0.3)
     -> span.kind: Str(server)
     -> language: Str(python)
     -> http.status_code: Str(200)
     -> http.route: Str(/rolldice)
     -> _dd.base_service: Str()
     -> runtime-id: Str(0c847e83f3684314a595e42a7ecd4088)
     -> http.url: Str(http://localhost:8080/rolldice)
     -> process.pid: Double(73137)
     -> _dd.measured: Double(1)
     -> _dd.top_level: Double(1)
     -> _sampling_priority_v1: Double(1)
     -> _dd.tracer_kr: Double(1)
Span #1
    Trace ID       : 00000000000000005df358c588cf97cc
    Parent ID      : 07e7cde5f7bc2378
    ID             : f438270f8c9f294f
    Name           : flask.application
    Kind           : Unspecified
    Start time     : 2024-10-02 19:21:10.102173 +0000 UTC
    End time       : 2024-10-02 19:21:10.104421 +0000 UTC
    Status code    : Ok
    Status message : 
Attributes:
     -> dd.span.Resource: Str(GET /rolldice)
     -> datadog.span.id: Str(17597858491687446863)
     -> datadog.trace.id: Str(6769852270295095244)
     -> component: Str(flask)
     -> flask.endpoint: Str(roll_dice)
     -> flask.url_rule: Str(/rolldice)
     -> _dd.base_service: Str()
Span #2
    Trace ID       : 00000000000000005df358c588cf97cc
    Parent ID      : f438270f8c9f294f
    ID             : 5b4b30645f20df84
    Name           : flask.preprocess_request
    Kind           : Unspecified
    Start time     : 2024-10-02 19:21:10.102942 +0000 UTC
    End time       : 2024-10-02 19:21:10.102986 +0000 UTC
    Status code    : Ok
    Status message : 
Attributes:
     -> dd.span.Resource: Str(flask.preprocess_request)
     -> datadog.span.id: Str(6578404888355594116)
     -> datadog.trace.id: Str(6769852270295095244)
     -> _dd.base_service: Str()
     -> component: Str(flask)
Span #3
    Trace ID       : 00000000000000005df358c588cf97cc
    Parent ID      : f438270f8c9f294f
    ID             : 6697ba8cc0fd2ee8
    Name           : flask.dispatch_request
    Kind           : Unspecified
    Start time     : 2024-10-02 19:21:10.103161 +0000 UTC
    End time       : 2024-10-02 19:21:10.103863 +0000 UTC
    Status code    : Ok
    Status message : 
Attributes:
     -> dd.span.Resource: Str(flask.dispatch_request)
     -> datadog.span.id: Str(7392582427047964392)
     -> datadog.trace.id: Str(6769852270295095244)
     -> component: Str(flask)
     -> _dd.base_service: Str()
Span #4
    Trace ID       : 00000000000000005df358c588cf97cc
    Parent ID      : 6697ba8cc0fd2ee8
    ID             : 1d3ddf848a518680
    Name           : app.roll_dice
    Kind           : Unspecified
    Start time     : 2024-10-02 19:21:10.103318 +0000 UTC
    End time       : 2024-10-02 19:21:10.10384 +0000 UTC
    Status code    : Ok
    Status message : 
Attributes:
     -> dd.span.Resource: Str(/rolldice)
     -> datadog.span.id: Str(2107085961028535936)
     -> datadog.trace.id: Str(6769852270295095244)
     -> component: Str(flask)
     -> _dd.base_service: Str()
Span #5
    Trace ID       : 00000000000000005df358c588cf97cc
    Parent ID      : f438270f8c9f294f
    ID             : 4033ff6fee71f85f
    Name           : flask.process_response
    Kind           : Unspecified
    Start time     : 2024-10-02 19:21:10.104038 +0000 UTC
    End time       : 2024-10-02 19:21:10.10406 +0000 UTC
    Status code    : Ok
    Status message : 
Attributes:
     -> dd.span.Resource: Str(flask.process_response)
     -> datadog.span.id: Str(4626322098446530655)
     -> datadog.trace.id: Str(6769852270295095244)
     -> component: Str(flask)
     -> _dd.base_service: Str()
Span #6
    Trace ID       : 00000000000000005df358c588cf97cc
    Parent ID      : f438270f8c9f294f
    ID             : a235e4ef33f1389b
    Name           : flask.do_teardown_request
    Kind           : Unspecified
    Start time     : 2024-10-02 19:21:10.104279 +0000 UTC
    End time       : 2024-10-02 19:21:10.104304 +0000 UTC
    Status code    : Ok
    Status message : 
Attributes:
     -> dd.span.Resource: Str(flask.do_teardown_request)
     -> datadog.span.id: Str(11688500123929753755)
     -> datadog.trace.id: Str(6769852270295095244)
     -> component: Str(flask)
     -> _dd.base_service: Str()
Span #7
    Trace ID       : 00000000000000005df358c588cf97cc
    Parent ID      : f438270f8c9f294f
    ID             : f51c81e921f0e5ab
    Name           : flask.do_teardown_appcontext
    Kind           : Unspecified
    Start time     : 2024-10-02 19:21:10.104358 +0000 UTC
    End time       : 2024-10-02 19:21:10.104391 +0000 UTC
    Status code    : Ok
    Status message : 
Attributes:
     -> dd.span.Resource: Str(flask.do_teardown_appcontext)
     -> datadog.span.id: Str(17662134676937041323)
     -> datadog.trace.id: Str(6769852270295095244)
     -> component: Str(flask)
     -> _dd.base_service: Str()
Span #8
    Trace ID       : 00000000000000005df358c588cf97cc
    Parent ID      : 07e7cde5f7bc2378
    ID             : 1d3f1d1da3c5a10b
    Name           : flask.response
    Kind           : Unspecified
    Start time     : 2024-10-02 19:21:10.104438 +0000 UTC
    End time       : 2024-10-02 19:21:10.104995 +0000 UTC
    Status code    : Ok
    Status message : 
Attributes:
     -> dd.span.Resource: Str(flask.response)
     -> datadog.span.id: Str(2107435163771576587)
     -> datadog.trace.id: Str(6769852270295095244)
     -> component: Str(flask)
     -> _dd.base_service: Str()
	{"kind": "exporter", "data_type": "traces", "name": "debug"}

You can read more about the OpenTelemetry trace format here, but just by scanning through the above we can see the request and response (spans) to our Python Flask app. If we wanted to, we could export these traces to something like Jaeger using OTLP by editing our configuration file further.

Summary

In this blog, we instrumented a basic Python application using DataDog (tracing) libraries, and forwarded those to an OpenTelemetry collector that converted those to OTel format using a “DataDog Receiver”, and forwarded those to a “Debug Exporter” that allowed us to view the converted traces in our console. In reality, many folks will have an existing DataDog agent currently deployed to receive and forward these traces (and other signals) – in future posts, we’ll look at how we can dual forward or “dual ship” our telemetry signals from an existing observability agent to an existing observability backend and an OTel collector in parallel. We’ll also look at placing an OTel collector “in line” so that it can process and transform existing telemetry on it’s way to an existing backend, so that existing telemetry can be reduced and optimized to minimize cost and maximize utility of that telemetry.

For more information, please contact us at info@controltheory.com

Contact

Stay in touch.

Be the first to gain control