RousselTMLEARNING

Observabilité

Grail et DQL avancés : performance, parse, jointures, séries temporelles, entités et corrélation

Écrire des requêtes DQL rapides et peu coûteuses, analyser du texte avec DPL et JSON, combiner des sources avec lookup, join et append, calculer des séries temporelles, interroger les entités et corréler logs, traces et métriques. Exemples complets et exercices corrigés.

1. Écrire des requêtes efficaces #

Le coût dépend des données lues.

Comment une requête s’exécute #

Grail n'a pas d'index préparé : une requête lit les données de la période et de la source demandées, puis applique les commandes dans l'ordre. Ce qui compte le plus est donc la quantité de données lue : la fenêtre de temps, la source et le premier filtre. Avec la licence DPS, ce volume est aussi ce qui est facturé (voir Licences Dynatrace).

Ordre recommandé des commandes DQL.
// À éviter : toute la rétention, filtre tardif, champs inutiles
fetch logs
| fieldsAdd msg = lower(content)
| filter contains(msg, "timeout")
| summarize count(), by: { host.name }
// Mieux : fenêtre courte, filtre en premier, champs utiles seulement
fetch logs, from: -2h, scanLimitGBytes: 50
| filter k8s.namespace.name == "boutique" and contains(content, "timeout", caseSensitive: false)
| fields timestamp, host.name, content
| summarize timeouts = count(), by: { host.name }
| sort timeouts desc
Option de fetchEffet
from: / to:limite la période lue (prioritaire sur le sélecteur de l’interface)
scanLimitGBytes:plafonne le volume lu : protège d’une requête trop large
samplingRatio:lit un échantillon (estimations, tendances sur de gros volumes)

Les anti-patterns #

À éviterPourquoiÀ faire
fetch sans période sur toute la rétentionlit toutfrom: -2h, puis élargir
transformer avant de filtrercalcule sur des lignes inutilesfiltrer dès la 2e ligne
contains sur tout le texte quand un champ existeanalyse de texte coûteusefiltrer sur le champ (loglevel, k8s.namespace.name)
requête lourde rafraîchie chaque minute dans un tableau de bordcoût multiplié par les visitespériode courte, agrégation, métrique extraite par OpenPipeline
Bonne pratique : Pour un indicateur consulté en permanence (taux d'erreur d'un service), extrayez une métrique avec OpenPipeline à l'ingestion plutôt que de recalculer à partir des logs à chaque affichage (voir Logs avancés avec OpenPipeline).

2. Analyser du texte : parse, DPL et JSON #

Transformer un message en champs.

Les motifs DPL #

parse extrait des champs avec un motif DPL (Dynatrace Pattern Language) : on décrit la ligne avec des éléments typés (IPADDR, INT, WORD, TIMESTAMP…) et des littéraux entre apostrophes. LD (line data) absorbe tout jusqu'au littéral suivant.

fetch logs, from: -1h
| filter log.source == "/var/log/nginx/access.log"
| parse content, "IPADDR:client SPACE LD '"' WORD:methode SPACE NSPACE:url LD '" ' INT:statut SPACE INT:octets"
| filter statut >= 500
| summarize erreurs = count(), by: { url, statut }
| sort erreurs desc
| limit 20
Élément DPLCorrespond à
INT, LONG, DOUBLEnombres
WORD, NSPACEmot, texte sans espace
IPADDRadresse IPv4 ou IPv6
TIMESTAMP('yyyy-MM-dd HH:mm:ss')date au format donné
LDtout jusqu’au littéral suivant
JSONobjet JSON complet
SPACEespaces
Bonne pratique : Mettez au point un motif dans un notebook sur une dizaine de lignes (| limit 10), puis généralisez. Si l'analyse sert à chaque requête, déplacez-la dans OpenPipeline : les champs seront stockés une fois pour toutes.

Les logs JSON #

Pour un log JSON, parse content, "JSON:evt" crée un objet ; on accède aux sous-champs avec la notation evt[cle][souscle] et on convertit les types (toLong, toDouble, toTimestamp).

fetch logs, from: -1h
| filter startsWith(content, "{")
| parse content, "JSON:evt"
| fieldsAdd utilisateur = evt[user][id], duree_ms = toLong(evt[duration_ms])
| filter duree_ms > 2000
| fields timestamp, utilisateur, duree_ms

3. Combiner des sources : lookup, join, append #

Croiser les données.

Trois commandes, trois usages #

lookup, join et append.
CommandeRésultatQuand l’utiliser
lookupchaque ligne reçoit au plus une correspondance, en champs préfixés (lookup. par défaut)enrichir avec un référentiel (client, équipe, propriétaire)
joincombine deux résultats selon une condition ; kind: inner, leftOuter…rapprocher deux signaux (spans et logs d’une même trace)
appendajoute les lignes de la sous-requête à la suitechronologie mêlant plusieurs sources

lookup #

// Enrichir les erreurs avec le niveau d'abonnement du client (référentiel en bizevents)
fetch logs, from: -1h
| filter loglevel == "ERROR" and isNotNull(customer.id)
| lookup [ fetch bizevents, from: -30d
           | filter event.type == "customer.profile"
           | fields customer.id, tier ],
  sourceField: customer.id, lookupField: customer.id, prefix: "client."
| summarize erreurs = count(), by: { client.tier }
Erreur courante : Oublier que lookup ne garde qu'une correspondance : si le référentiel contient plusieurs lignes par clé, le résultat dépend de l'ordre. Dédupliquez la sous-requête (dedup customer.id ou summarize).

join #

// Rapprocher les requêtes lentes (spans) des logs d'erreur de la même trace
fetch spans, from: -30m
| filter request.is_root_span == true and duration > 2s
| fields trace.id, span.name, duration
| join [ fetch logs, from: -30m
         | filter loglevel == "ERROR" and isNotNull(trace_id)
         | fields trace_id, content ],
  on: { left[trace.id] == right[trace_id] }, kind: inner,
  fields: { content }
| limit 50
Selon la version : Les noms de champs diffèrent selon la source et la version : trace.id sur les spans, trace_id sur de nombreux logs, parfois avec des types différents (convertissez avec toString() ou toUid() si la jointure ne renvoie rien). Vérifiez dans un notebook.

append #

// Empiler deux sources dans un même résultat
fetch events, from: -24h
| filter event.kind == "DAVIS_PROBLEM"
| fields timestamp, source = "probleme", titre = event.name
| append [ fetch events, from: -24h
           | filter event.type == "CUSTOM_DEPLOYMENT"
           | fields timestamp, source = "deploiement", titre = event.name ]
| sort timestamp desc

4. Séries temporelles #

Des courbes à partir de tout.

timeseries : interroger les métriques #

timeseries lit les métriques et renvoie une ligne par combinaison de dimensions, avec un tableau de valeurs par intervalle. Les opérations entre séries utilisent la notation [] (élément par élément), et les fonctions arrayAvg, arrayMax… résument un tableau.

// Plusieurs métriques, une ligne par hôte, puis calcul entre séries
timeseries { cpu = avg(dt.host.cpu.usage), mem = avg(dt.host.memory.usage) },
  by: { dt.entity.host }, interval: 15m, from: -24h
| fieldsAdd hote = entityName(dt.entity.host), pression = (cpu[] + mem[]) / 2
| fieldsAdd pic = arrayMax(pression)
| sort pic desc
| limit 10

makeTimeseries : construire une série #

makeTimeseries transforme des enregistrements (logs, spans, événements, bizevents) en série temporelle : utile quand aucune métrique n'existe encore.

// Taux d'erreur des requêtes, calculé à partir des spans
fetch spans, from: -6h
| filter request.is_root_span == true
| makeTimeseries { total = count(), echecs = countIf(request.is_failed == true) },
  by: { service.name }, interval: 5m
| fieldsAdd taux_erreur = echecs[] / total[] * 100
À savoir : countIf() compte les lignes qui vérifient une condition ; le calcul echecs[] / total[] divise les deux séries intervalle par intervalle.

5. Entités et corrélation #

Relier les signaux à la topologie.

Interroger les entités #

Les entités (hôtes, services, process groups…) s'interrogent avec fetch dt.entity.<type>. entityName() transforme un identifiant en nom lisible, et classicEntitySelector() réutilise la syntaxe des entity selectors (voir Maîtriser les entity selectors).

// Entités : les services d'un namespace, avec leur nom et leurs tags
fetch dt.entity.service
| filter in(id, classicEntitySelector("type(SERVICE),tag(\"k8s.namespace.name:boutique\")"))
| fields id, entity.name, tags
| sort entity.name asc
Selon la version : La topologie évolue vers Smartscape sur Grail, avec de nouvelles commandes de parcours des relations : consultez la référence DQL de votre environnement avant de les utiliser.

Corréler logs, traces et métriques #

En agrégeant chaque signal par la même clé (souvent dt.entity.service), on obtient un tableau de bord de diagnostic en une requête :

// Corréler : erreurs de logs et latence des spans, par service, sur la même période
fetch spans, from: -1h
| filter request.is_root_span == true
| summarize p90 = percentile(duration, 90), requetes = count(), by: { dt.entity.service }
| lookup [ fetch logs, from: -1h
           | filter loglevel == "ERROR"
           | summarize erreurs = count(), by: { dt.entity.service } ],
  sourceField: dt.entity.service, lookupField: dt.entity.service
| fieldsAdd service = entityName(dt.entity.service), erreurs = coalesce(lookup.erreurs, 0)
| fields service, requetes, p90, erreurs
| sort erreurs desc
Bonne pratique : Agrégez avant de combiner : joindre deux résumés de quelques dizaines de lignes est instantané, joindre des millions de lignes brutes ne l'est pas.

6. Commandes et fonctions à connaître #

L’aide-mémoire.

Commandes #

CommandeRôle
fetch, timeseriessources : enregistrements ou métriques
filter, filterOut, dedupsélectionner, exclure, dédupliquer
parse, fieldsAdd, fields, fieldsRemoveextraire, calculer, choisir les champs
summarize, makeTimeseriesagréger, construire une série
lookup, join, appendenrichir, combiner, empiler
expandune ligne par élément d’un tableau
sort, limittrier, limiter

Fonctions #

FonctionExemple
count(), countIf(), avg(), percentile()percentile(duration, 90)
contains(), startsWith(), matchesPhrase()contains(content, "timeout", caseSensitive: false)
in(), isNotNull(), coalesce()coalesce(lookup.erreurs, 0)
entityName(), classicEntitySelector()entityName(dt.entity.host)
toLong(), toString(), toTimestamp()toLong(evt[duration_ms])
bin(), arrayAvg(), arrayMax()summarize count(), by: { bin(timestamp, 1h) }

7. Exercices corrigés #

Pour s’entraîner dans un notebook.

Exercice 1 : les URL en erreur #

À partir des logs d'accès Nginx de la dernière heure, listez les 10 URL qui renvoient le plus d'erreurs 5xx. Correction : la requête de la section 2 (parse DPL, filter statut >= 500, summarize par URL).

Exercice 2 : le taux d’erreur par service #

Tracez le taux d'erreur de chaque service sur 6 heures, par tranches de 5 minutes, sans utiliser de métrique existante. Correction : la requête makeTimeseries de la section 4.

Exercice 3 : le diagnostic en une requête #

Produisez un tableau qui donne pour chaque service le nombre de requêtes, le 90e centile de durée et le nombre de logs d'erreur. Correction : la requête de corrélation de la section 5 ; vérifiez ensuite que la requête lit moins de quelques Go en réduisant la période.

Pour revoir les bases : Grail : le data lakehouse de Dynatrace et DQL en pratique.

Articles liés

L'app QuickStart de Dynatrace : obtenir ses premières données en quelques minutes

Découvrir l'app QuickStart : ajouter des données avec Auto Discovery ou une tuile dédiée, lire les Findings, …

L'app Investigations de Dynatrace : mener une enquête sur les données de Grail

Découvrir l'app Investigations : requêtes DQL, extraction avec DPL Architect, enrichissement, arbre de requêt…

Site Reliability Guardian dans Dynatrace

Valider automatiquement une mise en production avec des objectifs de disponibilité, de performance et de capa…

Pour aller plus loin

Soutenir

♥ SOUTENIR

Soutenez RousselTM Learning

Les formations, articles, TP et attestations restent gratuits et sans publicité intrusive. Une contribution libre, du montant de votre choix, finance l’hébergement, les environnements de TP et l’écriture de nouveaux contenus.

Contribuer avec PayPal Montant libre · carte bancaire ou compte PayPalEn savoir plus
QR code PayPal
Ou scannez ce QR code avec votre téléphone

Merci, chaque contribution compte !