Skip to main content

Access Dynatrace data

The Dynatrace Query Language (DQL) allows you to query all types of monitoring data from Dynatrace. If you would like to explore the available data in your Dynatrace tenant, the best option is to use Dynatrace's Notebooks.

For app development, Dynatrace offers the @dynatrace-sdk/client-query package, which provides a function to execute DQL queries conveniently.

Prerequisites

As mentioned in the Introduction, this tutorial uses fictional business events. The UI uses two types of events, booking.process.started and booking.process.finished. The payload of each event looks like the following:

"values": {
"timestamp": "2022-08-09T14:31:52.270000000Z",
"amount": 869,
"arrivaldate": "2022-04-16",
"currency": "USD",
"event.id": "c40ee555-130f-445f-bd25-a5521d649595",
"event.kind": "BIZ_EVENT",
"event.provider": "booking.app.tutorial",
"event.type": "booking.process.started",
"numbertravelers": 2,
"product": "Vienna - New York",
"startdate": "2020-04-06"
}

To follow this tutorial, we're going to need some dummy data. The easiest way is to use the data command from DQL, which allows you to generate sample data during query runtime. Follow these steps to set up the data:

  • Create a new file called events.ts in the src/app directory.
  • Paste the code snippet below into the events.ts file.
  • Save it, and that's it. We'll use this later in this tutorial.
Code Snippet for fictional events
const baseEvents = [
{
specversion: '1.0',
source: 'booking.app.tutorial',
id: crypto.randomUUID().toString(),
type: 'booking.process.started',
amount: 869,
startdate: '2020-04-06',
arrivaldate: '2022-04-16',
currency: 'USD',
numbertravelers: 2,
product: 'Vienna - New York',
},
{
specversion: '1.0',
source: 'booking.app.tutorial',
id: crypto.randomUUID().toString(),
type: 'booking.process.started',
amount: 587,
startdate: '2020-06-06',
arrivaldate: '2022-05-16',
currency: 'USD',
numbertravelers: 2,
product: 'Paris - New York',
},
{
specversion: '1.0',
source: 'booking.app.tutorial',
id: crypto.randomUUID().toString(),
type: 'booking.process.started',
amount: 99,
startdate: '2020-04-23',
arrivaldate: '2022-05-01',
currency: 'USD',
numbertravelers: 2,
product: 'Washington - New York',
},
{
specversion: '1.0',
source: 'booking.app.tutorial',
id: crypto.randomUUID().toString(),
type: 'booking.process.finished',
amount: 99,
startdate: '2020-04-23',
arrivaldate: '2022-05-01',
currency: 'USD',
numbertravelers: 2,
product: 'Washington - New York',
},
{
specversion: '1.0',
source: 'booking.app.tutorial',
id: crypto.randomUUID().toString(),
type: 'booking.process.finished',
amount: 587,
startdate: '2020-06-06',
arrivaldate: '2022-05-16',
currency: 'USD',
numbertravelers: 2,
product: 'Paris - New York',
},
];

const repeat = (arr, n) => Array(n).fill(arr).flat();

export const events = JSON.stringify(repeat(baseEvents, 10));

1. Create a custom hook to abstract data fetching

To build the UI, there's the need for four different DQL queries. To not implement all the logic multiple times, create a custom hook that takes the query as a parameter, executes it, and returns the result.

To follow best practices, create a new file src/app/hooks/useDQLQuery.ts in your project.

Start with creating the function interface. Since React hooks always have to start with the keyword use, call your hook useDQLQuery. The function accepts one parameter query of type string and returns a tuple consisting of the result and a boolean variable indicating whether the query is still loading. This information can be later used to decide whether we want to display the Skeleton or not.

The function interface would then look like the following:

src/app/hooks/useDQLQuery.ts
import { queryExecutionClient, QueryResult } from '@dynatrace-sdk/client-query';

export const useDQLQuery = (query: string): [QueryResult | undefined, boolean] => {
// return values will be changed in a following step
return [undefined, false];
};

As mentioned above, the hook will return two variables, the result from the query and the isLoading variable. Therefore, create two state variables by using the State hook and put it in your function body. Make sure to also add the corresponding import for the useState hook.

src/app/hooks/useDQLQuery.ts
import { queryExecutionClient, QueryResult } from '@dynatrace-sdk/client-query';
import { useState } from 'react';

export const useDQLQuery = (query: string): [QueryResult | undefined, boolean] => {
const [result, setResult] = useState<QueryResult>();
const [isLoading, setIsLoading] = useState<boolean>(false);

// return values will be changed in a following step
return [undefined, false];
};

Now you have to implement the actual querying logic. The @dynatrace-sdk/client-query package isn't only providing types, it's also providing a queryExecute function, which you can use to query Grail from our app.

Wrap this logic into a useEffect hook to only do the data-fetching at the first render. If you want to learn more about useEffect and when to use it exactly, visit Effect hook. Don't forget to also add useEffect to your imports.

src/app/hooks/useDQLQuery.ts
useEffect(() => {
queryExecutionClient
.queryExecute({
body: {
query,
requestTimeoutMilliseconds: 30000,
},
})
.then((res) => setResult(res.result))
.catch((e) => console.error(e));
}, [query]);

The queryExecute function accepts a body object, in which you have to pass the actual query and the request timeout. In this case, the query is the query parameter from the hook itself. The response is returned as a Promise, therefore use the setResult function within the Promise chain. In case of an error, just print the error in the console.

To set the value of isLoading correctly, set the value to true before executing the query and set it back to false in the finally clause of the Promise chain. The useEffect in your hook would then look like the following:

src/app/hooks/useDQLQuery.ts
useEffect(() => {
setIsLoading(true);
queryExecutionClient
.queryExecute({
body: {
query,
requestTimeoutMilliseconds: 30000,
},
})
.then((res) => setResult(res.result))
.catch((e) => console.error(e))
.finally(() => setIsLoading(false));
}, [query]);

After that, you have to adapt the return statement. As mentioned above, the hook returns a tuple of two variables, result and isLoading.

src/app/hooks/useDQLQuery.ts
return [result, isLoading];

As the query function is executed asynchronously, you have to make sure to cancel the request in case the component will be unmounted before the response is received. Within the useEffect hook, you can implement this within the return statement. This prevents updating the component state when the component is not rendered anymore.

src/app/hooks/useDQLQuery.ts
useEffect(() => {
const abortController = new AbortController();
const abortSignal = abortController.signal;

setIsLoading(true);
queryExecutionClient
.queryExecute({
body: {
query,
requestTimeoutMilliseconds: 30000,
},
abortSignal,
})
.then((res) => setResult(res.result))
.catch((e) => console.error(e))
.finally(() => setIsLoading(false));

return () => {
abortController.abort();
};
}, [query]);

2. Fetch data in UI

As the useDQLQuery hook is now fully implemented, you can use it in the next step to query data from Grail. To do that, the first step is to import it within the App.tsx file:

src/app/App.tsx
import { useDQLQuery } from './hooks/useDQLQuery';

As mentioned above, the useDQLQuery hook returns a tuple of two variables. The first one is the result of the query from type QueryResult, the second one is a boolean value indicating whether the query is still loading or not.

For the app you're building during this tutorial, you need four different DQL queries. You will query the number of booking.process.started and booking.process.finished events and the sum of the value field within these events. This tutorial isn't explaining DQL in detail. If you would like know more about DQL, follow this link.

To fetch the data, we need a couple of steps:

  • Import the events from events.ts at the end of the import list in App.tsx:

    src/app/App.tsx
    import { events } from './events';
  • Add the following snippet before the return statement in the App component, where we use the fictional events as the data source for our DQL queries via the data command:

    src/app/App.tsx
    const [resultStarted, isLoadingStarted] = useDQLQuery(
    `data json:"""${events}""" | filter type == "booking.process.started" | summarize value = count()`,
    );
    const [resultFinished, isLoadingFinished] = useDQLQuery(
    `data json:"""${events}""" | filter type == "booking.process.finished" | summarize value = count()`,
    );
    const [resultSumStarted, isLoadingSumStarted] = useDQLQuery(
    `data json:"""${events}""" | filter type == "booking.process.started" | summarize value = sum(amount)`,
    );
    const [resultSumFinished, isLoadingSumFinished] = useDQLQuery(
    `data json:"""${events}""" | filter type == "booking.process.finished" | summarize value = sum(amount)`,
    );

All four queries use an alias for the respective result field, which allows access to the result of all queries with the same keyword. In the next step, you'll now pass the result of the queries to the instance of the Card component.

3. Pass data to UI components

In the previous section, you already have added four instances of the Card component to the App.tsx file. The result of each query is now stored in the respective variable return from the useDQLQuery hook. You can use that variable to pass the result to the Card component via the value prop. To make your code more readable extract the values returned by the hook in dedicated variables. The respective isLoading variables, have to be passed to the isLoading prop of the component. Additionally, you have to pass meaningful values for chartLabel, chartSuffix and precision.

Add these four variables before the return statement of your App.tsx file.

src/app/App.tsx
const bookingStarted = Number(resultStarted?.records?.[0]?.value);

const bookingsFinished = Number(resultFinished?.records?.[0]?.value);

const totalRevenueBookingsStarted = Number(resultSumStarted?.records?.[0]?.value);

const totalRevenueBookingsFinished = Number(resultSumFinished?.records?.[0]?.value);

After all these considerations, the code for the Card components looks like the following:

src/app/App.tsx
<Card
value={bookingStarted}
chartLabel="Bookings Started"
chartSuffix="# of"
chartPrecision={0}
isLoading={isLoadingStarted}
/>
<Card
value={bookingsFinished}
chartLabel="Bookings Finished"
chartSuffix="# of"
chartPrecision={0}
isLoading={isLoadingFinished}
/>
<Card
value={totalRevenueBookingsStarted}
chartLabel="Total Revenue / Bookings Started"
chartSuffix="$"
chartPrecision={2}
isLoading={isLoadingSumStarted}
/>
<Card
value={totalRevenueBookingsFinished}
chartLabel="Total Revenue / Bookings Finished"
chartSuffix="$"
chartPrecision={2}
isLoading={isLoadingSumFinished}
/>

After saving your files, you should see the Cards in your UI filled with data fetched directly from Grail, with corresponding labels and skeletons during data loading.

Checkpoint

Click here to see how your project should look like at this point.

Summary and next steps

This section of the tutorial explained how you can query data from Grail with the help of a custom hook and render the result in custom components. In the next section, you'll learn how to fetch data from 3rd party APIs and render it in your UI.

Still have questions?
Find answers in the Dynatrace Community