📍 Article principal : Observabilité applicative en 2026 : OpenTelemetry, traces distribuées et stack LGTM — pour le contexte conceptuel et l’architecture d’ensemble.
Pourquoi envoyer les logs vers Loki via OTLP
Loki s’est imposé comme la solution open source de référence pour les logs orientés opérations. Sa particularité — n’indexer que les labels et stocker le contenu compressé sur stockage objet — change radicalement l’économie d’une plateforme de logs : on peut conserver des téraoctets sur plusieurs mois pour le prix d’un Elasticsearch très réduit. Mais cette efficacité demande une discipline stricte sur les labels, sous peine de faire exploser l’index.
Depuis Loki 3.0, l’endpoint OTLP natif accepte directement les LogRecords envoyés par OpenTelemetry, ce qui supprime la couche d’adaptation autrefois nécessaire (Promtail, Fluent Bit). On peut donc construire une pipeline de bout en bout en OTLP : application → SDK OTel → Collector → Loki. Ce tutoriel construit cette pipeline étape par étape et explique comment Loki transforme les attributs OTLP en labels indexés.
Prérequis
- Une instance Loki 3.0+ joignable (Docker, binaire ou Kubernetes)
- Un OpenTelemetry Collector déjà fonctionnel — voir le tutoriel Configurer un OpenTelemetry Collector
- Un service applicatif instrumenté qui produit des logs OTLP
- Une instance Grafana pour la visualisation
- Temps estimé : 30 à 45 minutes
Étape 1 — Vérifier la version et l’endpoint OTLP de Loki
L’endpoint OTLP HTTP de Loki s’expose à l’adresse /otlp/v1/logs sur son port HTTP standard (3100 par défaut en local). Cet endpoint accepte le format OTLP HTTP/protobuf ou OTLP HTTP/JSON. La présence et le comportement de cet endpoint dépendent de la configuration : il faut s’assurer qu’il est activé.
# test rapide : un endpoint vide doit répondre 405 Method Not Allowed
# (l'endpoint accepte POST uniquement)
curl -i http://127.0.0.1:3100/otlp/v1/logs
# état général de Loki
curl http://127.0.0.1:3100/ready
curl http://127.0.0.1:3100/metrics | grep loki_build_info
Si l’endpoint répond 404 Not Found, l’OTLP receiver n’est pas activé dans la configuration Loki. Si /ready renvoie ready et que loki_build_info indique une version 3.x, l’instance est saine et prête à recevoir.
Étape 2 — Configurer l’exporter Loki côté Collector
Côté Collector, on utilise l’exporter otlphttp standard pointé vers l’endpoint OTLP de Loki. Pas besoin d’un exporter spécifique « loki » : Loki accepte directement le format OTLP HTTP avec son schéma de logs natif. C’est une simplification majeure par rapport aux pipelines précédentes qui demandaient une transformation explicite vers le format JSON Loki.
# otelcol.yaml (extension)
exporters:
otlphttp/loki:
endpoint: http://loki:3100/otlp
tls:
insecure: true
# headers optionnels pour le multi-tenancy
# headers:
# X-Scope-OrgID: tenant1
service:
pipelines:
logs:
receivers: [otlp]
processors: [memory_limiter, resource, batch]
exporters: [otlphttp/loki]
L’endpoint pointe vers /otlp et non /otlp/v1/logs — le suffixe /v1/logs est ajouté automatiquement par l’exporter otlphttp selon la spécification OTLP HTTP. Pointer directement vers /otlp/v1/logs dans la configuration produit une concaténation incorrecte et un 404. C’est une erreur fréquente au premier passage.
Pour un déploiement multi-tenants Loki, ajouter l’en-tête X-Scope-OrgID qui identifie le tenant. Sur un Loki mono-tenant ou en dev, l’en-tête est optionnel ou peut prendre la valeur anonymous.
Étape 3 — Comprendre la conversion OTLP vers labels Loki
Loki applique des règles précises pour transformer les attributs OTLP en labels indexés. Sans cette compréhension, on retrouve dans Loki des séries inutiles ou des données qui n’apparaissent pas là où on s’y attend. Les règles par défaut, depuis Loki 3.x, sont :
- Les attributs de resource stables (
service.name,service.namespace,service.instance.id,cloud.region,k8s.cluster.name) deviennent des labels indexés - Les autres attributs de resource et tous les attributs de log sont stockés en structured metadata — non indexés mais filtrables via LogQL
- Le corps du log (
body) devient le contenu de la ligne severity_textdevient le labelleveloudetected_levelselon la version
Cette logique reflète exactement la philosophie de Loki : peu de labels indexés, contenu riche dans le corps. La structured metadata, ajoutée dans Loki 3.0, comble le gap entre labels indexés trop coûteux et corps libre trop fastidieux à requêter.
Étape 4 — Personnaliser le mapping labels
Le mapping par défaut peut être insuffisant si l’on veut promouvoir des attributs métier en labels (par exemple tenant_id ou environment). Loki accepte une configuration côté serveur pour étendre la liste des attributs hints qui deviennent des labels.
# loki-config.yaml (extrait)
limits_config:
otlp_config:
resource_attributes:
attributes_config:
- action: index_label
attributes:
- service.name
- service.namespace
- service.instance.id
- deployment.environment
- k8s.cluster.name
- k8s.namespace.name
log_attributes:
- action: structured_metadata
attributes:
- http.method
- http.status_code
- user_agent.original
La règle d’or rappelée plus haut s’applique avec une rigueur particulière sur Loki : on ne promeut en label indexé que ce qui a une cardinalité bornée et stable. Ajouter request_id ou trace_id dans index_label est une faute classique qui fait grimper l’index à des millions de séries en quelques heures. Ces valeurs appartiennent au corps du log ou à la structured metadata.
Étape 5 — Émettre des logs côté application
Côté application, l’émission de logs OTLP passe par les SDK qui supportent le signal logs. La maturité varie : Python, Java et JavaScript ont des LogRecord providers stables. Côté Node.js avec Pino, le transport pino-opentelemetry-transport fait le pont. Côté Python, un simple LoggingHandler branché sur le logger standard suffit.
# exemple Python — extension du bootstrap logging
import logging
from opentelemetry._logs import set_logger_provider
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter
provider = LoggerProvider()
provider.add_log_record_processor(
BatchLogRecordProcessor(OTLPLogExporter(insecure=True))
)
set_logger_provider(provider)
handler = LoggingHandler(level=logging.NOTSET, logger_provider=provider)
logging.getLogger().addHandler(handler)
# tout log émis via logging.getLogger() sera exporté en OTLP
logger = logging.getLogger("app")
logger.info("user login successful", extra={"user.id": "u-42"})
Ce bootstrap branche le module logging standard de Python sur l’export OTLP. À partir de là, chaque appel à logger.info(...) produit un LogRecord enrichi automatiquement du trace_id et span_id du contexte courant. C’est le câblage minimum pour qu’une ligne de log soit retrouvable depuis sa trace dans Grafana.
Étape 6 — Vérifier la réception côté Loki
Le moyen le plus rapide de valider la chaîne est d’émettre quelques logs de test et de les chercher avec LogQL via l’API Loki ou Grafana Explore.
# requête LogQL via l'API Loki
curl -G "http://127.0.0.1:3100/loki/api/v1/query_range" \
--data-urlencode 'query={service_name="otel-python-demo"}' \
--data-urlencode 'start='$(date -d '5 min ago' '+%s')'000000000' \
--data-urlencode 'end='$(date '+%s')'000000000' \
--data-urlencode 'limit=10' | jq
Si tout est câblé correctement, on obtient un objet JSON avec les logs récents du service. Note importante sur les noms de labels : OTLP service.name est converti en label Loki service_name — les points sont remplacés par des underscores parce que LogQL n’accepte pas les points dans les noms de labels. Cette conversion est documentée et systématique.
Étape 7 — Requêter avec LogQL
LogQL est le langage de requête de Loki. Sa syntaxe combine un sélecteur de labels (entre accolades) suivi optionnellement d’un pipeline de filtres et de transformations. Comprendre les bases permet de tirer parti de l’index labels et de la structured metadata efficacement.
# tous les logs du service
{service_name="otel-python-demo"}
# logs erreur du service
{service_name="otel-python-demo"} |= "ERROR"
# logs structurés JSON, filtrés par champ
{service_name="otel-python-demo"} | json | level="ERROR"
# logs avec un user.id particulier (via structured metadata)
{service_name="otel-python-demo"} | user_id="u-42"
# nombre de logs erreur par minute
sum by (service_name) (
rate({service_name=~".+"} |= "ERROR" [1m])
)
Le sélecteur {service_name="..."} filtre par label indexé — c’est la partie rapide. Le pipeline qui suit (|= "ERROR", | json, | user_id="u-42") opère sur les blocs ramenés en mémoire — c’est plus lent mais bon marché à la requête. Cette dichotomie est ce qui rend Loki si économique en stockage : tant que les requêtes les plus fréquentes peuvent filtrer par labels, le coût reste maîtrisé.
Étape 8 — Corrélation trace_id depuis Loki
L’intérêt majeur de l’OTLP natif est que le trace_id arrive automatiquement en structured metadata sur chaque log émis dans un contexte de span. Côté Grafana, on configure un derived field sur le datasource Loki qui transforme le trace_id en lien cliquable vers Tempo.
# grafana datasource Loki — provisioning
apiVersion: 1
datasources:
- name: Loki
type: loki
url: http://loki:3100
jsonData:
derivedFields:
- name: trace_id
matcherType: label
matcherRegex: trace_id
datasourceUid: tempo
url: '${__value.raw}'
urlDisplayLabel: 'Voir trace dans Tempo'
Avec ce derived field, chaque ligne de log dans Grafana Explore expose un bouton « Voir trace dans Tempo » qui ouvre la trace correspondante. Cette mécanique transforme une investigation laborieuse en saut direct entre signaux. Le tutoriel Corréler trace_id entre logs, métriques et traces dans Grafana creuse l’ensemble des configurations Grafana liées.
Erreurs fréquentes
Mettre trace_id ou request_id en label indexé
L’erreur fatale. Chaque trace_id étant unique, l’index gonfle de millions d’entrées par jour. Loki devient lent, le coût en stockage explose, et au pire des cas l’instance refuse de nouvelles séries. Ces identifiants doivent rester en structured metadata, où ils sont filtrables sans entrer dans l’index principal.
Pointer l’exporter Collector vers /otlp/v1/logs
L’exporter otlphttp ajoute lui-même /v1/logs à l’URL configurée. La cible doit donc être /otlp tout court, sans suffixe. Une URL avec le suffixe redondant produit un 404 silencieux qui empêche tout export sans message d’erreur clair.
Oublier que les points deviennent des underscores
Les conventions sémantiques OTel utilisent des points (service.name, http.method). LogQL ne supporte pas les points dans les noms de labels. La conversion automatique remplace par des underscores. On filtre donc par service_name, pas par service.name. Beaucoup de premières requêtes échouent sur cette nuance.
Ne pas activer la structured metadata côté Loki
Selon la version Loki et la configuration, la structured metadata peut être désactivée. Dans ce cas, les attributs OTLP non promus en labels sont perdus. Vérifier que allow_structured_metadata: true est bien positionné et que le schéma de stockage utilise au moins le format tsdb v13.
Mauvais alignement de version Loki et Grafana
Les fonctionnalités OTLP natives évoluent rapidement. Une Grafana ancienne devant un Loki 3.x peut afficher correctement les logs mais ne pas reconnaître les structured metadata, ce qui rend les derived fields sur ces champs inutilisables. Aligner Grafana et Loki sur des versions récentes et compatibles.
Tutoriels associés
- Configurer un OpenTelemetry Collector
- Envoyer les traces vers Tempo
- Corréler trace_id entre logs, métriques et traces dans Grafana
- 🔝 Retour à l’article principal — Observabilité applicative et stack LGTM
Ressources et références officielles
- Loki documentation : grafana.com/docs/loki/latest
- Endpoint OTLP de Loki : grafana.com/docs/loki/latest/send-data/otel
- LogQL — référence : grafana.com/docs/loki/latest/query
- Structured metadata : grafana.com/docs/loki/latest/get-started/labels/structured-metadata
- Spec OTLP HTTP : opentelemetry.io/docs/specs/otlp/#otlphttp
- Convention sémantique logs OTel : opentelemetry.io/docs/specs/semconv/logs
FAQ
Loki accepte-t-il aussi le push direct depuis l’application ?
Techniquement oui, l’endpoint OTLP HTTP est joignable directement depuis le SDK. Mais on perd les bénéfices du Collector (batching, sampling, redaction, enrichissement). Pour des cas particuliers (PoC local, fonction serverless qui ne tolère pas un agent), c’est une option ; pour de la production sérieuse, passer par le Collector.
Quelle volumétrie peut absorber Loki ?
Une instance modeste (4 vCPU, 8 Go RAM) absorbe 100 à 200 Go/jour confortablement. À grande échelle (TB/jour), Loki se déploie en mode microservices avec ingester, distributor, querier, query-frontend séparés et stockage objet S3. La doc Grafana détaille les profils de déploiement.
Loki vs Quickwit ?
Loki excelle pour des charges filtrer par label puis grep. Quickwit excelle pour de la recherche full-text complexe avec scoring. Ils sont complémentaires : Loki pour les logs d’opération, Quickwit pour les logs où l’on a besoin d’analyse linguistique. Voir Quickwit logs et search pour la comparaison détaillée.
Faut-il garder Promtail ou Fluent Bit ?
Pour les logs applicatifs en OTLP, non — la pipeline OTel + Loki suffit. Pour les logs d’infrastructure (système, kernel, conteneurs non instrumentés), Promtail ou Fluent Bit restent pertinents et envoient soit en push API natif Loki, soit via OTLP avec le receiver filelog du Collector.
Comment migrer depuis Elasticsearch ?
Stratégie typique en deux temps : nouveau pipeline OTel + Loki en parallèle pendant 30-60 jours, puis bascule des dashboards et alertes côté Grafana, puis arrêt progressif d’ES. Les requêtes Kibana ne se traduisent pas mécaniquement en LogQL : il faut repenser certaines analyses, ce qui est l’occasion de gagner en clarté.