You can query all your Dynatrace data—logs, metrics, and traces—using a single, unified language: Dynatrace Query Language (DQL).

Let’s see this in action. Imagine you’re troubleshooting a specific transaction that’s performing poorly. You’ve got its trace ID, b4a3977a-5b41-4779-98f1-1f81b09d029e.

First, let’s pull the logs associated with that trace:

fetch logs
| filter trace.id == "b4a3977a-5b41-4779-98f1-1f81b09d029e"
| fields timestamp, content, log.level
| sort timestamp desc

This query fetches all log records (fetch logs), filters them down to only those matching our specific trace.id, selects the timestamp, content, and log.level fields for clarity, and then sorts them by timestamp in descending order so you see the most recent logs first.

Now, let’s layer in metrics. Perhaps you want to see the CPU usage of the service that handled this trace during the time it occurred. You’d first need to identify the service ID from your trace data, or by looking at the trace details in the UI. Let’s assume the service ID is my-frontend-service.

fetch dt.metrics.builtin:host.cpu.usage, dt.metrics.builtin:service.request.count
| filter dt.entity.service == "my-frontend-service"
| filter timestamp >= "2023-10-27T10:00:00Z" and timestamp <= "2023-10-27T10:15:00Z"
| summarize avg(value) by dt.entity.service, metric.name

This query fetches the built-in CPU usage and request count metrics for the specified service (filter dt.entity.service == "my-frontend-service"). We’re also time-boxing the query to the period when the problematic trace was active (filter timestamp >= ... and timestamp <= ...). Finally, summarize avg(value) by dt.entity.service, metric.name calculates the average value for each metric across the service.

Combining logs and traces is where DQL really shines. You can link them directly.

fetch traces, from:now()-1h
| filter trace.id == "b4a3977a-5b41-4779-98f1-1f81b09d029e"
| join logs
    on trace.id == trace.id
| fields timestamp, content, log.level, duration, service.name
| sort timestamp desc

Here, we fetch traces from the last hour (fetch traces, from:now()-1h) and immediately filter for our specific trace ID. Then, we join this trace data with log data (join logs) using the common trace.id. This allows you to see log entries chronologically alongside the trace spans, enriched with trace-specific details like duration and service.name.

The magic of DQL lies in its ability to treat different data types as first-class citizens within a single query. When you fetch logs or fetch traces, you’re not just getting raw text or a graph; you’re getting structured data with fields that can be filtered, joined, and aggregated just like metrics. This unified data model is what allows you to correlate events across the entire observability stack—from the lowest-level log message to the highest-level service metric—all within one query.

A common point of confusion is how Dynatrace models entities. When you filter by dt.entity.service or dt.entity.host, you’re using Dynatrace’s internal entity model, which is automatically populated from your OneAgent or OpenTelemetry data. This means you don’t need to manually define relationships; Dynatrace understands that a specific host is running a specific service, and traces are associated with that service. This entity-centric approach simplifies querying significantly.

The next step in mastering DQL is exploring the enrich command, which lets you pull in data from related entities without explicit joins, making your queries more concise and performant.

Want structured learning?

Take the full Dynatrace course →