Blog

OpenTelemetry sur OpenShift avec Helm : exporters Kafka et Splunk, filtrage par namespace

OpenShift
OpenTelemetry
Observabilité
Kafka
Splunk
Helm

Déploiement d'OpenTelemetry Collector sur OpenShift via Helm. Configuration des pipelines de logs et traces avec exporters Kafka et Splunk, filtrage des logs par labels de namespace et routage intelligent des données d'observabilité.

10 mars 2025

OpenTelemetry : standard universel pour l’observabilité

OpenTelemetry (OTel) est le standard CNCF pour la collecte de logs, métriques et traces distribuées. Il unifie des agents auparavant disparates (Fluentd, Jaeger, Prometheus exporters) en un seul pipeline configurable.

L’OpenTelemetry Collector est le composant central : il reçoit des données de multiples sources, les transforme (filtrage, enrichissement, routage), puis les exporte vers plusieurs backends simultanément.

Architecture visée dans cet article :

Pods (OTLP) ──────────────────────────┐
Nodes (kubelet, journal) ─────────────┤

                            OTel Collector (DaemonSet + Deployment)

                       ┌───────────────┴──────────────────┐
                       ▼                                  ▼
                  Kafka Topic                        Splunk HEC
              (logs applicatifs                  (logs critiques,
               par namespace)                     audit, sécurité)

Installation via Helm

Ajouter le dépôt OpenTelemetry

helm repo add open-telemetry https://open-telemetry.github.io/opentelemetry-helm-charts
helm repo update

# Voir les versions disponibles
helm search repo open-telemetry/opentelemetry-collector --versions | head -5

Créer le namespace et les secrets

oc new-project opentelemetry

# Secret pour Splunk HEC
oc create secret generic splunk-hec-secret \
  --from-literal=token=<votre-hec-token> \
  -n opentelemetry

# Secret pour les credentials Kafka (si SASL activé)
oc create secret generic kafka-credentials \
  --from-literal=username=otel-producer \
  --from-literal=password=<mot-de-passe-kafka> \
  -n opentelemetry

SCC pour le DaemonSet (lecture des logs nœuds)

# Le DaemonSet doit lire /var/log sur les nœuds
oc adm policy add-scc-to-user privileged \
  -z opentelemetry-collector \
  -n opentelemetry

Configuration du Collector : values.yaml

Le fichier values.yaml est le cœur du déploiement. Il configure les receivers, processors et exporters.

# values.yaml — OpenTelemetry Collector sur OpenShift

mode: daemonset          # DaemonSet pour la collecte sur chaque nœud
                         # Utiliser "deployment" pour un collector centralisé

image:
  repository: otel/opentelemetry-collector-contrib
  tag: "0.113.0"

serviceAccount:
  create: true
  name: opentelemetry-collector

# Monter les logs des nœuds
extraVolumes:
  - name: varlog
    hostPath:
      path: /var/log
  - name: varlibdockercontainers
    hostPath:
      path: /var/lib/docker/containers

extraVolumeMounts:
  - name: varlog
    mountPath: /var/log
    readOnly: true
  - name: varlibdockercontainers
    mountPath: /var/lib/docker/containers
    readOnly: true

# Ressources du Collector
resources:
  limits:
    cpu: 500m
    memory: 512Mi
  requests:
    cpu: 100m
    memory: 256Mi

config:
  # ── RECEIVERS ────────────────────────────────────────────
  receivers:
    # Réception OTLP depuis les applications instrumentées
    otlp:
      protocols:
        grpc:
          endpoint: 0.0.0.0:4317
        http:
          endpoint: 0.0.0.0:4318

    # Collecte des logs des conteneurs via filelog
    filelog:
      include:
        - /var/log/pods/*/*/*.log
      exclude:
        - /var/log/pods/opentelemetry_*/*/*.log   # Éviter boucle de collecte
      start_at: beginning
      include_file_path: true
      include_file_name: false
      operators:
        # Parser JSON (format CRI-O sur OpenShift)
        - type: json_parser
          id: parser-docker
          on_error: send
          timestamp:
            parse_from: attributes.time
            layout: '%Y-%m-%dT%H:%M:%S.%LZ'
        # Extraire les métadonnées du chemin de fichier
        - type: regex_parser
          id: extract-metadata-from-filepath
          regex: '^.*\/(?P<namespace>[^_]+)_(?P<pod_name>[^_]+)_(?P<uid>[a-f0-9\-]+)\/(?P<container_name>[^\._]+)\/(?P<restart_count>\d+)\.log$'
          parse_from: attributes["log.file.path"]
          cache:
            size: 128
        - type: move
          from: attributes.namespace
          to: resource["k8s.namespace.name"]
        - type: move
          from: attributes.pod_name
          to: resource["k8s.pod.name"]
        - type: move
          from: attributes.container_name
          to: resource["k8s.container.name"]

    # Métriques Kubernetes depuis le kubelet
    kubeletstats:
      collection_interval: 30s
      auth_type: serviceAccount
      endpoint: "https://${env:MY_NODE_NAME}:10250"
      insecure_skip_verify: true
      metric_groups:
        - node
        - pod
        - container

  # ── PROCESSORS ───────────────────────────────────────────
  processors:
    # Enrichissement avec les métadonnées Kubernetes (labels, annotations)
    k8sattributes:
      auth_type: serviceAccount
      passthrough: false
      filter:
        node_from_env_var: MY_NODE_NAME
      extract:
        metadata:
          - k8s.namespace.name
          - k8s.pod.name
          - k8s.pod.uid
          - k8s.node.name
          - k8s.deployment.name
          - k8s.statefulset.name
          - k8s.daemonset.name
          - k8s.container.name
          - container.image.name
          - container.image.tag
        # IMPORTANT : extraire les labels du namespace pour le filtrage
        labels:
          - tag_name: ns.environment
            key: environment
            from: namespace
          - tag_name: ns.team
            key: team
            from: namespace
          - tag_name: ns.tier
            key: tier
            from: namespace
          - tag_name: ns.log-level
            key: log-forwarding-level
            from: namespace
        annotations:
          - tag_name: ns.owner
            key: owner
            from: namespace

    # Filtrage par niveau de criticité défini sur le namespace
    filter/critical-namespaces:
      logs:
        include:
          match_type: expr
          expressions:
            # Inclure si le namespace est labellisé "critical" ou "security"
            - 'resource["ns.tier"] == "critical" or resource["ns.tier"] == "security"'

    filter/standard-namespaces:
      logs:
        include:
          match_type: expr
          expressions:
            # Inclure les namespaces applicatifs standards (pas critiques)
            - 'resource["ns.tier"] == "standard" or resource["ns.tier"] == "dev"'
        exclude:
          match_type: expr
          expressions:
            # Exclure les namespaces systèmes
            - 'resource["k8s.namespace.name"] matches "^kube-" or resource["k8s.namespace.name"] == "openshift-monitoring"'

    filter/high-severity:
      logs:
        include:
          match_type: expr
          expressions:
            # Envoyer vers Splunk uniquement les logs WARN/ERROR/CRITICAL
            - 'severity_number >= SEVERITY_NUMBER_WARN'

    # Transformation et enrichissement
    transform/add-cluster-info:
      log_statements:
        - context: resource
          statements:
            - set(attributes["k8s.cluster.name"], "production-cluster-paris")
            - set(attributes["k8s.cluster.env"], "production")

    # Batch pour optimiser l'envoi
    batch:
      send_batch_size: 1000
      timeout: 5s
      send_batch_max_size: 2000

    # Limitation de débit (éviter la saturation)
    memory_limiter:
      check_interval: 1s
      limit_mib: 400
      spike_limit_mib: 128

  # ── EXPORTERS ────────────────────────────────────────────
  exporters:
    # Export vers Kafka — logs applicatifs par namespace
    kafka/app-logs:
      brokers:
        - kafka-broker-1.kafka.svc.cluster.local:9092
        - kafka-broker-2.kafka.svc.cluster.local:9092
        - kafka-broker-3.kafka.svc.cluster.local:9092
      topic: otel.logs.applications
      encoding: otlp_json
      auth:
        sasl:
          username: "${env:KAFKA_USERNAME}"
          password: "${env:KAFKA_PASSWORD}"
          mechanism: SCRAM-SHA-512
        tls:
          insecure: false
          ca_file: /etc/ssl/kafka/ca.crt
      producer:
        max_message_bytes: 1000000
        compression: snappy
      timeout: 10s
      retry_on_failure:
        enabled: true
        initial_interval: 5s
        max_interval: 30s
        max_elapsed_time: 120s

    # Export vers Splunk HEC — logs critiques/sécurité
    splunk_hec/security-logs:
      endpoint: "https://splunk.entreprise.com:8088/services/collector"
      token: "${env:SPLUNK_HEC_TOKEN}"
      source: "opentelemetry"
      sourcetype: "kube:container:otel"
      index: "security_prod"
      tls:
        insecure_skip_verify: false
        ca_file: /etc/ssl/splunk/ca.crt
      max_content_length_logs: 2097152   # 2 MB
      disable_compression: false
      heartbeat:
        interval: 60s
      telemetry:
        enabled: true
        override_metrics_names:
          otelcol_exporter_queue_size: true

    # Export Kafka pour les métriques (topic séparé)
    kafka/metrics:
      brokers:
        - kafka-broker-1.kafka.svc.cluster.local:9092
        - kafka-broker-2.kafka.svc.cluster.local:9092
      topic: otel.metrics.cluster
      encoding: otlp_json
      producer:
        compression: lz4

    # Export de débogage (désactiver en prod)
    debug:
      verbosity: basic

  # ── PIPELINES ────────────────────────────────────────────
  service:
    pipelines:
      # Pipeline : logs applicatifs → Kafka
      logs/to-kafka:
        receivers: [filelog]
        processors:
          - memory_limiter
          - k8sattributes
          - transform/add-cluster-info
          - filter/standard-namespaces
          - batch
        exporters: [kafka/app-logs]

      # Pipeline : logs critiques/sécurité → Splunk
      logs/to-splunk:
        receivers: [filelog, otlp]
        processors:
          - memory_limiter
          - k8sattributes
          - transform/add-cluster-info
          - filter/critical-namespaces
          - filter/high-severity
          - batch
        exporters: [splunk_hec/security-logs]

      # Pipeline : métriques cluster → Kafka
      metrics/to-kafka:
        receivers: [kubeletstats, otlp]
        processors:
          - memory_limiter
          - k8sattributes
          - batch
        exporters: [kafka/metrics]

      # Pipeline : traces → OTLP (ex. Jaeger ou Tempo)
      traces:
        receivers: [otlp]
        processors:
          - memory_limiter
          - k8sattributes
          - batch
        exporters: [debug]

    telemetry:
      logs:
        level: info
      metrics:
        address: 0.0.0.0:8888

Déployer avec Helm

helm install otel-collector open-telemetry/opentelemetry-collector \
  --namespace opentelemetry \
  --values values.yaml \
  --set env[0].name=MY_NODE_NAME \
  --set env[0].valueFrom.fieldRef.fieldPath=spec.nodeName \
  --set env[1].name=SPLUNK_HEC_TOKEN \
  --set env[1].valueFrom.secretKeyRef.name=splunk-hec-secret \
  --set env[1].valueFrom.secretKeyRef.key=token \
  --set env[2].name=KAFKA_USERNAME \
  --set env[2].valueFrom.secretKeyRef.name=kafka-credentials \
  --set env[2].valueFrom.secretKeyRef.key=username \
  --set env[3].name=KAFKA_PASSWORD \
  --set env[3].valueFrom.secretKeyRef.name=kafka-credentials \
  --set env[3].valueFrom.secretKeyRef.key=password

Labelliser les namespaces pour le filtrage

Le routage vers Kafka ou Splunk est piloté par les labels du namespace. C’est le mécanisme clé de cet article.

# Namespace applicatif standard → Kafka
oc label namespace mon-app \
  tier=standard \
  team=backend \
  environment=production \
  log-forwarding-level=standard

# Namespace de sécurité/audit → Splunk
oc label namespace openshift-authentication \
  tier=security \
  team=ops \
  log-forwarding-level=critical

# Namespace de production critique → Splunk
oc label namespace payments \
  tier=critical \
  team=fintech \
  environment=production \
  log-forwarding-level=critical

# Namespace de dev → Kafka, logs moins stricts
oc label namespace dev-feature-x \
  tier=dev \
  team=frontend \
  environment=development

Vérifier les labels d’un namespace

oc get namespace mon-app --show-labels
oc describe namespace payments | grep Labels -A 10

Déploiement centralisé (Gateway Pattern)

Pour les environnements avec de nombreux clusters, ajouter un Collector Gateway centralisé :

# values-gateway.yaml
mode: deployment
replicaCount: 3

config:
  receivers:
    otlp:
      protocols:
        grpc:
          endpoint: 0.0.0.0:4317

  processors:
    batch:
      send_batch_size: 5000
      timeout: 10s

  exporters:
    kafka/all-logs:
      brokers: [kafka-broker:9092]
      topic: otel.logs.all-clusters
      encoding: otlp_json
      producer:
        compression: snappy

  service:
    pipelines:
      logs:
        receivers: [otlp]
        processors: [batch]
        exporters: [kafka/all-logs]
helm install otel-gateway open-telemetry/opentelemetry-collector \
  --namespace opentelemetry \
  --values values-gateway.yaml

Les DaemonSets des clusters satellites envoient vers le Gateway via OTLP :

exporters:
  otlp/gateway:
    endpoint: "otel-gateway.opentelemetry.svc.cluster.local:4317"
    tls:
      insecure: false

Vérification et debug

# Vérifier que les pods tournent
oc get pods -n opentelemetry

# Logs du Collector
oc logs -f daemonset/otel-collector-opentelemetry-collector -n opentelemetry

# Métriques internes du Collector (port 8888)
oc port-forward svc/otel-collector-opentelemetry-collector 8888:8888 -n opentelemetry
curl localhost:8888/metrics | grep otelcol_exporter

# Vérifier les messages dans Kafka
oc exec -it kafka-broker-0 -n kafka -- \
  kafka-console-consumer.sh \
  --bootstrap-server localhost:9092 \
  --topic otel.logs.applications \
  --max-messages 5

Mise à jour via Helm

# Mettre à jour la configuration
helm upgrade otel-collector open-telemetry/opentelemetry-collector \
  --namespace opentelemetry \
  --values values.yaml \
  --reuse-values

# Rollback si problème
helm rollback otel-collector -n opentelemetry

Conclusion

OpenTelemetry Collector sur OpenShift offre une flexibilité maximale pour router les données d’observabilité. L’utilisation des labels de namespace comme critère de filtrage est une approche élégante et déclarative : les équipes applicatives labellisent leurs namespaces, et le Collector route automatiquement vers le bon backend. Cette architecture découple la stratégie de collecte (définie par ops) des applications (ignorantes du backend d’observabilité), tout en permettant d’alimenter simultanément Kafka pour l’analyse temps-réel et Splunk pour la sécurité et la conformité.