La gestion des logs applicatifs avec l'écosystème OpenTelemetry

Mise en place du collecteur OpenTelemetry et instrumentation des logs sur une application Node.js
Cet article se focalise sur la mise en place d'un collecteur OpenTelemetry, et de l'instrumentation des logs applicatifs dans des applications Node.js, puis sur la mise en place de Loki et Grafana pour consulter les logs collectés par le collecteur.
Contexte
Rôle et définition
L'observabilité est la capacité de comprendre l'état d'un système ou d'une application informatique et d'y réagir (citation extraite du site de RedHat).
Les logs applicatifs sont un pilier de l'observabilité, au même titre que les traces HTTP et les métriques. Ils sont les messages générés par les applications lors de leur exécution, et contiennent des informations brutes — erreurs, états, événements — et constituent souvent la première source de diagnostic.
Pourquoi les logs sont essentiels ?
Ils décrivent ce qui s’est passé à un moment donné.
Ils aident à reconstituer une séquence d’événements.
Ils sont indispensables pour comprendre des erreurs fonctionnelles ou applicatives.
Objectif de l'article
Ayant plusieurs services backend utilsant Node.js, et plus précisément Express ou Nest.js, je souhaite mutualiser la collecte des logs applicatifs de ces services, et pour y parvenir, je vais utiliser les technologies suivantes:
l'API OpenTelemetry JavaScript / TypeScript, à intégrer directement dans le code de mes services
un collecteur OpenTelemetry, qui assurera la mutualisation des logs de mes services
des outils de monitoring: Loki et Grafana
NOTE : Alors que cet article décrit la mise en place de la mutualisation des logs, je souhaite également appliquer les mêmes principes pour les traces et les métriques, mais la description de leurs mises en place fera l'objet d'autres articles.
L'objectif en image:
Installation du collecteur OpenTelemetry
Rôle du collecteur
Le Collector agit comme un intermédiaire capable de :
Recevoir des données de plusieurs applications (via des
receivers)Les transformer ou les filtrer
Les exporter vers différentes plateformes (via des
exporters)
Les Receivers
Les receivers ont pour rôle de recevoir les données de télémétrie et constituent le point d’entrée des traces, métriques et logs dans l’écosystème OpenTelemetry, permettant ainsi de centraliser et d’unifier la collecte des données d’observabilité.
OpenTelemetry s’appuie largement sur le protocole OTLP (pour OpenTelemetry Protocol). Les receivers peuvent également supporter d’autres protocoles reconnus, ce qui garantit une forte interopérabilité avec les outils et infrastructures existants, mais j'utilise le protocole OTLP dans ma configuration.
Le protocole OTLP consiste en une spécification de formattage de données. Le transport de ces données peut être réalisé via les protocoles HTTP ou gRPC; j'utilise le transport HTTP dans mon infrastructure.
Les Exporters
Les exporters servent à envoyer les données collectées vers des systèmes externes. Ils permettent à OpenTelemetry de rester flexible et compatible avec de nombreux outils d’observabilité. Ici aussi, j'utilise le protocole OTLP, mais, comme dans le cas des receivers, d’autres protocoles peuvent être utilisés.
Installation
Nous créons d'abord un fichier docker-compose.yml qui contient la configuration pour déployer le collecteur OpenTelemetry sous forme de conteneur Docker:
services:
open-telemetry-collector:
image: otel/opentelemetry-collector
volumes:
- ./open-telemetry-collector-config.yaml:/etc/otelcol/config.yaml
ports:
- 4318:4318
Ici, le collecteur va écouter sur le port spécifique 4318; les services Node.js utiliseront donc ce port pour transmettre les logs via HTTP.
Avant de lancer le conteneur, nous créons un second fichier nommé open-telemetry-collector-config.yaml dans le même dossier que le fichier docker-compose.yml. Ce fichier permet de configurer le collecteur:
receivers:
otlp:
protocols:
http:
endpoint: 0.0.0.0:4318
exporters:
debug:
verbosity: detailed
service:
pipelines:
logs:
receivers: [otlp]
exporters: [debug]
On note que le collecteur est configuré pour avoir:
un
receiverqui écoute les requêtes HTTP sur le port4318.un
exporterde debug qui permet de vérifier la réception des logsun
pipelinequi envoie à l'exporter les données reçues par le receiver
Enfin, nous lançons le conteneur:
docker compose up
Mise en place de l'API OpenTelemetry sur les services Node.js
Installation
J'installe les dépendances sur mon projet Node.js:
pnpm install --save @opentelemetry/api-logs @opentelemetry/sdk-logs @opentelemetry/exporter-logs-otlp-http @opentelemetry/resources @opentelemetry/semantic-conventions
NOTE : j'utilise la commande pnpm, mais npm ou yarn conviennent également.
Concepts
Au sens de la spécification OTLP, un log est un objet qui contient principalement un body et des attributes.
Dans l'exemple ci-dessous, nous allons créer les éléments suivants, qui ajoutent des attributes communs à tous les logs qui les implémentent :
une
resource, qui ajoute desattributesde haut niveau (ex: nom du service, environnement, ...)un
scope, qui agit comme une instance du logger et qui ajoute aussi desattributespartagés (ex: section de l'application)
Le log que nous allons créer va donc utiliser une resource et un scope qui lui injecteront les attributes suivants:
nom attribute |
valeur attribute |
ajouté par |
|---|---|---|
| service.name | mon-service-nodejs | resource |
| service.version | 1.0.0 | resource |
| deployment.environment | development | resource |
| logger.section | http-controller | scope |
Exemple d'implémentation
puis nous créons un fichier exemple-open-telemetry.js à la racine du projet. Cet exemple va créer un exporter OTLP qui est utilisé pour transmettre nos logs au collecteur
import { logs } from '@opentelemetry/api-logs';
import {
LoggerProvider,
BatchLogRecordProcessor,
} from '@opentelemetry/sdk-logs';
import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http';
import { Resource } from '@opentelemetry/resources';
import { SEMRESATTRS_SERVICE_NAME } from '@opentelemetry/semantic-conventions';
// Création d'une `resource` (qui ajoute des attributs à chaque log)
const resource = new Resource({
[SEMRESATTRS_SERVICE_NAME]: 'mon-service-nodejs',
'service.version': '1.0.0',
'deployment.environment': 'development',
});
const loggerProvider = new LoggerProvider({
resource: resource,
});
// Création d'un exporter OTLP qui envoie les logs via HTTP sur le port 4318
const otlpExporter = new OTLPLogExporter({
url: 'http://localhost:4318/v1/logs', // Modifier `localhost` pour le nom de domaine ou l'adresse IP du collecteur
headers: {
// Ajouter des headers d'authentification si nécessaire
// 'Authorization': 'Bearer YOUR_TOKEN',
},
});
// Utilisation de la fonctionnalité de batch:
// Plutôt qu'envoyer une requête HTTP par log,
// il est souvent préférable d'utiliser un traitement par lot 🚀
loggerProvider.addLogRecordProcessor(
new BatchLogRecordProcessor(otlpExporter, {
maxQueueSize: 2048,
maxExportBatchSize: 512,
scheduledDelayMillis: 5000,
})
);
// Déclaration del'instance du logger
// Cette instance est considérée comme le `scope` (qui ajoute des attributs à chaque log)
logs.setGlobalLoggerProvider(loggerProvider);
const logger = logs.getLogger('logger.section', 'http-controller');
// Envoi du 1er log !! 😀
// (Ici, un log typique d'application Express ou Nest.js)
// Il implémente la structure OTLP à envoyer au collecteur
logger.emit({
severityText: 'INFO',
body: 'Requete entrante',
attributes: {
'http.method': 'GET',
'http.url': '/api/users',
'http.status_code': 200,
},
});
// Extinction du logger lorsque le traitement est terminé
setTimeout(async () => {
console.log('Fermeture...');
await loggerProvider.shutdown();
console.log('Les logs ont été exportés !!');
}, 2000);
Puis nous exécutons le script:
node exemple-open-telemetry.js
Vérification de fonctionnement
Nous allons vérifier que le collecteur a bien reçu la donnée OTLP de notre log applicatif.
D'abord, j'ai besoin de connaître le nom de notre conteneur Docker:
docker ps
Une liste des conteneurs actifs va être affichée; celui que nous cherchons dans cette liste utilise l'image otel/opentelemetry-collector, ce qui nous permet de trouver son nom en fin de ligne : setup-open-telemetry-collector-1.
Grâce à ce nom, nous pouvons afficher les logs du collecteur (de logs 🙄):
docker logs setup-open-telemetry-collector-1
et nous voyons bien dans les informations en retour que notre log a été reçu correctement 🎉 :
2026-03-23T10:29:01.917Z info ResourceLog #0
Resource SchemaURL:
Resource attributes:
-> service.name: Str(mon-service-nodejs)
-> telemetry.sdk.language: Str(nodejs)
-> telemetry.sdk.name: Str(opentelemetry)
-> telemetry.sdk.version: Str(1.23.0)
-> service.version: Str(1.0.0)
-> deployment.environment: Str(development)
ScopeLogs #0
ScopeLogs SchemaURL:
InstrumentationScope logger.section http-controller
LogRecord #0
ObservedTimestamp: 2026-03-23 10:28:59.901 +0000 UTC
Timestamp: 2026-03-23 10:28:59.901 +0000 UTC
SeverityText: INFO
SeverityNumber: Unspecified(0)
Body: Str(Requete entrante)
Attributes:
-> http.method: Str(GET)
-> http.url: Str(/api/users)
-> http.status_code: Int(200)
Trace ID:
Span ID:
Flags: 0
Installation de Loki et Grafana
Pourquoi ces outils?
Après collecte des logs, nous avons désormais besoin d'une interface pour les visualiser, les trier, les filtrer...
Il existe plusieurs solutions sur le marché, et voici un tableau comparatif des plus célèbres d'entre elles:
| Critère | Grafana Loki | Elastic Kibana | Datadog | New Relic |
|---|---|---|---|---|
| Rôle | Stockage et gestion des logs. | Analyse et visualisation des logs et métriques. | Collecte, analyse et visualisation des logs, métriques, et traces. | Surveillance des performances applicatives, logs et métriques. |
| Connectivité avec OpenTelemetry | Intégration native via OpenTelemetry Collector (OTLP) | Intégration via Elastic Agent ou OpenTelemetry Collector | Intégration native avec OpenTelemetry. | Intégration native avec OpenTelemetry. |
| Visualisation | Visualisation via Grafana. | Visualisation via Kibana (interface web). | Tableau de bord personnalisé avec de nombreuses visualisations intégrées. | Interface web riche avec visualisation de logs et de performances. |
| Coût | Open source (gratuite pour usage de base), mais des coûts peuvent s'appliquer pour l'infrastructure et les plugins payants | Gratuit pour l'usage de base, coût pour l'infrastructure et les fonctionnalités avancées | Modèle payant basé sur l’utilisation (logs, traces, métriques) avec tarification par volume | Modèle payant basé sur l’utilisation (log, métriques, traces) avec options d’abonnement par niveau. |
| Dépendance fournisseur | Faible : Solution open source, mais peut nécessiter des outils associés pour la gestion à grande échelle. | Dépendance à Elastic (en particulier pour les services managés). | Haute : Solution SaaS avec dépendance à Datadog pour la gestion des logs et des métriques. | Haute : Solution SaaS avec dépendance à New Relic pour la gestion des logs et des métriques. |
| Scalabilité | Excellente scalabilité avec un stockage optimisé pour les logs (peut être plus complexe à gérer à très grande échelle). | Très bonne scalabilité, mais peut devenir coûteuse pour les très grands volumes de données. | Scalabilité très bonne, conçu pour des environnements de grande taille. | Scalabilité très bonne, mais coûteuse à grande échelle. |
| Recherches et requêtes | Recherche rapide et efficace via Grafana, mais limité à des recherches basées sur des logs sans analyse avancée. | Recherche puissante avec support de requêtes complexes via Elasticsearch. | Recherche rapide et bien intégrée, avec une prise en charge des logs et des traces. | Recherche rapide, avec des filtres et des capacités de requêtes basées sur des logs et des traces. |
| Formats standards | Compatible avec des formats comme JSON, Logfmt, et d’autres formats personnalisés. | Compatible avec JSON, les logs structurés et semi-structurés. | Compatible avec JSON, logfmt, et autres formats courants. | Compatible avec JSON, logfmt, et autres formats courants. |
Pour ma part, je souhaite une solution 100% Open Source (si possible); et en particulier éviter les dépendances à des outils tiers. Le critère du coût est prioritaire, et je veux le plus possible avoir la main sur le traitement de mes données par ces outils. Mon choix s'est donc porté sur le couple Grafana / Loki.
Par ailleurs, Loki reconnaît nativement les données OTLP que le collecteur lui transmet par requêtes HTTP; nous restons donc sur les mêmes principes de fonctionnement que ceux présentés au début de cet article.
Installation
Nous ajoutons de nouveaux services au fichier de configuration de conteneurs docker-compose.yml (et quelques paramètres au collecteur):
services:
open-telemetry-collector:
image: otel/opentelemetry-collector
volumes:
- ./open-telemetry-collector-config.yaml:/etc/otelcol/config.yaml
ports:
- 4318:4318
depends_on:
- loki
networks:
- loki
loki:
image: grafana/loki:3.4.2
ports:
- "3100:3100"
volumes:
- ./loki-config.yaml:/etc/loki/local-config.yaml
command: -config.file=/etc/loki/local-config.yaml
networks:
- loki
grafana:
image: grafana/grafana:11.6.0
environment:
- GF_FEATURE_TOGGLES_ENABLE=grafanaManagedRecordingRules
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
- GF_AUTH_ANONYMOUS_ENABLED=true
- GF_AUTH_BASIC_ENABLED=false
ports:
- 3000:3000/tcp
entrypoint:
- sh
- -euc
- |
mkdir -p /etc/grafana/provisioning/datasources
cat <<EOF > /etc/grafana/provisioning/datasources/ds.yaml
apiVersion: 1
datasources:
- name: Loki
type: loki
access: proxy
orgId: 1
url: 'http://loki:3100'
basicAuth: false
isDefault: true
version: 1
editable: true
EOF
/run.sh
networks:
- loki
networks:
loki:
Avec cette configuration:
Loki écoute les requêtes HTTP au format OTLP sur le port 3100
L'interface visuelle Grafana sera disponible sur l'URL suivante: http://localhost:3000
Et nous configurons un nouvel exporter dans notre fichier de configuration du collecteur open-telemetry-collector-config.yaml :
receivers:
otlp:
protocols:
http:
endpoint: 0.0.0.0:4318
exporters:
debug:
verbosity: detailed
otlphttp/logs:
endpoint: "http://loki:3100/otlp"
tls:
insecure: true
service:
pipelines:
logs:
receivers: [otlp]
exporters: [debug, otlphttp/logs]
Il faut noter que l'exporter est paramétré pour envoyer les données vers le nom de domaine loki sur le port 3100; ce nom de domaine est configuré dans la configuration des conteneurs, section networks.
Loki a lui aussi besoin de son fichier de configuration loki-config.yaml, à placer dans le même dossier que les 2 fichiers précédents:
auth_enabled: false
limits_config:
allow_structured_metadata: true
volume_enabled: true
server:
http_listen_port: 3100
common:
ring:
instance_addr: 0.0.0.0
kvstore:
store: inmemory
replication_factor: 1
path_prefix: /tmp/loki
schema_config:
configs:
- from: 2020-05-15
store: tsdb
object_store: filesystem
schema: v13
index:
prefix: index_
period: 24h
storage_config:
tsdb_shipper:
active_index_directory: /tmp/loki/index
cache_location: /tmp/loki/index_cache
filesystem:
directory: /tmp/loki/chunks
pattern_ingester:
enabled: true
Nous relançons les conteneurs:
docker compose up
Nous exécutons à nouveau le script Node.js présenté en partie 1:
node exemple-open-telemetry.js
Lorsque les conteneurs sont lancés, Grafana est disponible sur l'URL suivante : http://localhost:3000.
Dans la section "Explore", nous allons créer une query. On note que Grafana dispose déjà d'une liste de labels créée à partir de notre simple exécution du script ci-dessus; on sélectionne donc le label "deployment_environment" et la valeur "development" pour ce label, puis on clique sur le bouton "Run query".
Nous trouvons bien notre log, et ses métadonnées dans les résultats de Grafana.
Industrialiser la solution
Pour mettre à l'échelle cette infrastructure, voici mes prochaines étapes
installer mes services Node.js, et instances du collecteur, de Loki et de Grafana sur des machines différentes (et donc remplacer "localhost" par des noms de domaine)
utiliser HTTPS pour les communications entre ces instances
paramétrer l'authentification sur les différents outils
factoriser le code d'exemple présenté en partie 1, de sorte à l'implémenter sous forme d'une classe TypeScript
Conclusion
Cet article, bien que focalisé sur la gestion des logs de leur production dans le code jusqu'à leur exploitation visuelle dans un écosystème OpenTelemetry, a également décrit l'installation du collecteur.
Ce collecteur est le fondement de cet écosystème, et j'écris d'ailleurs un article similaire à propos de l'instrumentation des traces HTTP. Ce nouvel article se basera lui aussi sur le collecteur et j'en reprendrai donc les éléments de configuration...





