Nomad Logs

OpenTelemetry: logs pour Nomad en utilisant Prometheus, et Grafana

Bon maintenant on va centraliser les logs. Pour cela on va déployer Loki, Vector, et Grafana pour explorer la donnée.

On crée le fichier loki.hcl.

 0job "loki" {
 1  datacenters = ["services"]
 2  type        = "service"
 3
 4  group "loki" {
 5    count = 1
 6    network {
 7      mode = "host"
 8      port "loki" {
 9        to = 3100
10      }
11    }
12    service {
13      name = "loki"
14      port = "loki"
15      provider = "nomad"
16      tags = [
17          "traefik.enable=true",
18          "traefik.http.routers.loki.service=loki",
19          "traefik.http.routers.loki.rule=Host(`loki.service.local`)",
20          "traefik.http.routers.loki.tls=true",
21      ]
22    }
23    task "loki" {
24      driver = "docker"
25      user = "root"
26      config {
27        image = "grafana/loki:2.9.7"
28        args = [
29          "-config.file",
30          "local/config.yml",
31        ]   
32        volumes = ["/opt/data/loki:/loki"]
33        ports = ["loki"]
34      }
35      template {
36        data=<<EOH
37auth_enabled: false
38server:
39  http_listen_port: 3100
40ingester:
41  lifecycler:
42    address: 127.0.0.1
43    ring:
44      kvstore:
45        store: inmemory
46      replication_factor: 1
47    final_sleep: 0s
48  chunk_idle_period: 1h
49  max_chunk_age: 1h
50  chunk_target_size: 1048576
51  chunk_retain_period: 30s
52  max_transfer_retries: 0     # Chunk transfers disabled
53  wal:
54    enabled: true
55    dir: "/loki/wal"
56schema_config:
57  configs:
58    - from: 2020-10-24
59      store: boltdb-shipper
60      object_store: filesystem
61      schema: v11
62      index:
63        prefix: index_
64        period: 24h
65storage_config:
66  boltdb_shipper:
67    active_index_directory: /loki/boltdb-shipper-active
68    cache_location: /loki/boltdb-shipper-cache
69    cache_ttl: 24h         # Can be increased for faster performance over longer query periods, uses more disk space
70    shared_store: filesystem
71  filesystem:
72    directory: /loki/chunks
73compactor:
74  working_directory: /loki/boltdb-shipper-compactor
75  shared_store: filesystem
76limits_config:
77  reject_old_samples: true
78  reject_old_samples_max_age: 168h
79chunk_store_config:
80  max_look_back_period: 0s
81table_manager:
82  retention_deletes_enabled: false
83  retention_period: 0s
84EOH
85        destination = "local/config.yml"
86        change_mode = "restart"
87      }
88      resources {
89        cpu    = 1000 #Mhz
90        memory = 1000 #MB
91      }
92    }
93  }
94}

Rien de bien compliqué. Remarquez qu’on expose à nouveau Loki autravers de Traefik.

  0job "http-collection" {
  1  datacenters = ["edge"]
  2  type = "system"
  3  group "vector" {
  4    count = 1
  5    network {
  6      port "api" {
  7        to = 8686
  8      }
  9    }
 10    ephemeral_disk {
 11      size    = 500
 12      sticky  = true
 13    }
 14    task "vector" {
 15      driver = "docker"
 16      config {
 17        image = "timberio/vector:0.46.0-alpine"
 18        ports = ["api"]
 19        volumes = [
 20          "local/data/traefik:/vector/traefik",
 21        ]
 22        mounts = [
 23          {
 24            type     = "bind"
 25            target   = "/var/run/docker.sock"
 26            source   = "/var/run/docker.sock"
 27            readonly = true
 28          },
 29          {
 30            type     = "bind"
 31            target   = "/tmp/data"
 32            source   = "/opt/data"
 33            readonly = true
 34          }
 35        ]
 36      }
 37      env {
 38        VECTOR_CONFIG = "local/vector.toml"
 39        VECTOR_REQUIRE_HEALTHY = "false"
 40      }
 41      resources {
 42        cpu    = 100 # 100 MHz
 43        memory = 100 # 100MB
 44      }
 45      # template with Vector's configuration
 46      template {
 47        destination = "local/vector.toml"
 48        change_mode   = "signal"
 49        change_signal = "SIGHUP"
 50        # overriding the delimiters to [[ ]] to avoid conflicts with Vector's native templating, which also uses {{ }}
 51        left_delimiter = "[["
 52        right_delimiter = "]]"
 53        data=<<EOH
 54          data_dir = "local/data/vector/"
 55          [api]
 56            enabled = true
 57            address = "0.0.0.0:8686"
 58            playground = true
 59          [sources.docker-logs]
 60            type = "docker_logs"
 61          [sources.http-logs]
 62            type = "file"
 63            include = [ "/tmp/data/traefik/log/*.log" ]
 64            data_dir = "/vector/traefik"
 65          [transforms.traefik-logs]
 66            type = "remap"
 67            inputs = [ "http-logs" ]
 68            source = '''
 69            . = parse_json!(.message)
 70            '''
 71          [sinks.machinelogs]
 72            type = "loki"
 73            compression = "snappy"
 74            encoding.codec = "json"
 75            inputs = ["docker-logs"] 
 76            endpoint = "http://loki.service.local
 77            healthcheck.enabled = true
 78            out_of_order_action = "drop"
 79            # remove fields that have been converted to labels to avoid having the field twice
 80            remove_label_fields = true
 81              [sinks.machinelogs.labels]
 82              job = "{{label.\"com.hashicorp.nomad.job_name\" }}"
 83              task = "{{label.\"com.hashicorp.nomad.task_name\" }}"
 84              group = "{{label.\"com.hashicorp.nomad.task_group_name\" }}"
 85              namespace = "{{label.\"com.hashicorp.nomad.namespace\" }}"
 86              node = "{{label.\"com.hashicorp.nomad.node_name\" }}"
 87          [sinks.accesslogs]
 88            type = "loki"
 89            compression = "snappy"
 90            encoding.codec = "json"
 91            inputs = ["traefik-logs"] 
 92            endpoint = "http://loki.service.local
 93            healthcheck.enabled = true
 94            out_of_order_action = "drop"
 95            # remove fields that have been converted to labels to avoid having the field twice
 96            remove_label_fields = true
 97              [sinks.accesslogs.labels]
 98              service = "{{ .ServiceName}}"
 99              router = "{{ .RouterName }}"
100              status = "{{ .DownstreamStatus }}"
101              method = "{{ .RequestMethod }}"
102              job = "/var/log/traefik.log"
103        EOH
104      }
105      kill_timeout = "30s"
106    }
107  }
108}

On collecte les logs de traefik depuis les fichiers de logs générés.

61          [sources.http-logs]
62            type = "file"
63            include = [ "/tmp/data/traefik/log/*.log" ]
64            data_dir = "/vector/traefik"
65          [transforms.traefik-logs]
66            type = "remap"
67            inputs = [ "http-logs" ]
68            source = '''
69            . = parse_json!(.message)
70            '''

Lors de la collection, Vector va injecter le continue du log dans un objet. Ainsi à la ligne 68, on pase un petit script tn VRL pour renvoyer directement le contenue log (qui est un JSON pour traefik). Et on rajoute lors de l’envoie des tabels supplémentaires.

Remarquez, qu’on expose la socket docker, pour aller collecter les logs. Une autre manière serait d’aller chercher les fichiers de logs dans le répertoire de Nomad (/opt/nomad/alloc/*/logs par exemple) vant de les process et les envoyer à Loki.

On peut désormais créer une nouvelle datasource sur Grafana, pour Loki, en utilisant l’endpoint loki.service.local.

Et voila.

J’utilise ici Vector pour formater et structurer les logs avant de les envoyer à Loki. Selon le format des logs on peut changer la configuration de Vector pour les structurer et garde une cohérence sur le long terme.

On collecte tous les logs de Traefik, et comme on utilise Traefik pour assurer la redirection dynamique vers les bon services (Loki, Prometheus, Grafana), on aura des logs de toute le trafic généré par ses appels. Et peuvent rapidement venir flooder et rendre peu visible les logs de vos services. On pourrait effacer ses logs si l’on voit que le service est celui de Loki par exemple, ou créer un autre entrypoint (sur un autre port) et utiliser celui-ci pour tout le trafic non relié à nos services directement.

Enfin, Loki, comme tous les gestionnaires de logs, n’est pas fait pour exploiter rapidement de la donnée sur le long terme (12 mois). Si vous voulez utiliser ces logs pour créer des KPI, il est bien mieux d’avoir un service qui va calculer et enregistrer les valeurs des KPI comme souhaités. Vous pourrez alors avoir des 95 percentile sur les 12 derniers mois et calculer d’autres KPI, non de run mais de reporting (sur 3/6/12 mois). N’oubliez pas d’archiver les logs avant purge, pour pouvoir les réhydrater pour faire de l’archéologie si besoin.