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).
// À é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 fetch | Effet |
|---|---|
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 #
| À éviter | Pourquoi | À faire |
|---|---|---|
| fetch sans période sur toute la rétention | lit tout | from: -2h, puis élargir |
| transformer avant de filtrer | calcule sur des lignes inutiles | filtrer dès la 2e ligne |
| contains sur tout le texte quand un champ existe | analyse de texte coûteuse | filtrer sur le champ (loglevel, k8s.namespace.name) |
| requête lourde rafraîchie chaque minute dans un tableau de bord | coût multiplié par les visites | pé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 DPL | Correspond à |
|---|---|
INT, LONG, DOUBLE | nombres |
WORD, NSPACE | mot, texte sans espace |
IPADDR | adresse IPv4 ou IPv6 |
TIMESTAMP('yyyy-MM-dd HH:mm:ss') | date au format donné |
LD | tout jusqu’au littéral suivant |
JSON | objet JSON complet |
SPACE | espaces |
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_ms3. Combiner des sources : lookup, join, append #
Croiser les données.
Trois commandes, trois usages #
| Commande | Résultat | Quand l’utiliser |
|---|---|---|
lookup | chaque 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) |
join | combine deux résultats selon une condition ; kind: inner, leftOuter… | rapprocher deux signaux (spans et logs d’une même trace) |
append | ajoute les lignes de la sous-requête à la suite | chronologie 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 quelookupne 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.idousummarize).
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 50Selon la version : Les noms de champs diffèrent selon la source et la version :trace.idsur les spans,trace_idsur de nombreux logs, parfois avec des types différents (convertissez avectoString()outoUid()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 desc4. 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 10makeTimeseries : 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 calculechecs[] / 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 ascSelon 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 descBonne 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 #
| Commande | Rôle |
|---|---|
fetch, timeseries | sources : enregistrements ou métriques |
filter, filterOut, dedup | sélectionner, exclure, dédupliquer |
parse, fieldsAdd, fields, fieldsRemove | extraire, calculer, choisir les champs |
summarize, makeTimeseries | agréger, construire une série |
lookup, join, append | enrichir, combiner, empiler |
expand | une ligne par élément d’un tableau |
sort, limit | trier, limiter |
Fonctions #
| Fonction | Exemple |
|---|---|
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.
