Monitoring application logs is a crucial aspect of the software development and deployment lifecycle. In this post, we'll delve into the process of observing logs generated by Docker container applications operating within HashiCorp Nomad. With the aid of Grafana, Vector, and Loki, we'll explore effective strategies for log analysis and visualization, enhancing visibility and troubleshooting capabilities within your Nomad environment.
Steps:
- Install Nomad in Linux
- Install Docker
- Run Nomad in both server and client mode
- Run Loki in Nomad
- Deploy Logging app
- Deploy Vector in Nomad
- Deploy Grafana
- Observe logs in Grafana using Loki Datasource
Application structure
Install Nomad locally (Linux)
Nomad, much like Kubernetes, serves as a powerful container orchestration tool, facilitating seamless application deployment and management. In this guide, we'll walk through the installation process on a Linux machine, specifically Ubuntu 22.04 LTS. Let's dive in:
Install required packages
$ sudo apt-get update && sudo apt-get install wget gpg coreutils
Add the HashiCorp GPG key
$ wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
Add official Hasicorp Linux Repository
$ echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
Update and Install
$ sudo apt-get update && sudo apt-get install nomad
This command will install the latest Nomad binary. The installation can be confirmed by checking the version of nomad by running
$ nomad -v
Install Docker
To deploy containers, docker is essential and can be installed by the following steps. Nomad will detect the docker in a system using docker driver and use it to deploy containers.
Set up Docker's apt
repository
# Add Docker's official GPG key: sudo apt-get update sudo apt-get install ca-certificates curl sudo install -m 0755 -d /etc/apt/keyrings sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc sudo chmod a+r /etc/apt/keyrings/docker.asc # Add the repository to Apt sources: echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \ $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt-get update
Install Docker
$ sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Restart docker and update permission
$ sudo systemctl restart docker $ sudo usermod -aG docker ubuntu
Run Nomad in both server and client mode
Nomad binary can run in both server and client mode. The server manages the state of the cluster and clients are used to deploy applications. In production, multiple Nomad servers and clients running in separate machines are interconnected to form a cluster. Here, we will use a single machine to run both server and client.
Nomad require a config file to run the application in server and client mode. The config file is also required to add more capabilities to nomad such as adding docker driver, setting telemetry, autopilot and so on.
Here is the nomad configuration file which can be used to run nomad. It also contains the information required to run docker containers. Create a file nomad.hcl
and copy the below content to the file.
datacenter = "dc1" data_dir = "/opt/nomad/data" bind_addr = "0.0.0.0" log_level = "INFO" server { enabled = true bootstrap_expect = 1 search { fuzzy_enabled = true limit_query = 200 limit_results = 1000 min_term_length = 5 } } # Enable the client client { enabled = true options { "driver.raw_exec.enable" = "1" "docker.privileged.enabled" = "true" } server_join { retry_join = [ "127.0.0.1" ] } } plugin "docker" { config { endpoint = "unix:///var/run/docker.sock" extra_labels = ["job_name", "job_id", "task_group_name", "task_name", "namespace", "node_name", "node_id"] volumes { enabled = true selinuxlabel = "z" } allow_privileged = true } } telemetry { collection_interval = "15s" disable_hostname = true prometheus_metrics = true publish_allocation_metrics = true publish_node_metrics = true }
Run the following command in the terminal where the above file is located to start Nomad.
$ sudo nomad agent -config=nomad.hcl
This will start the application.
==> Loaded configuration from nomad.hcl ==> Starting Nomad agent... ==> Nomad agent configuration: Advertise Addrs: HTTP: 192.168.1.32:4646; RPC: 192.168.1.32:4647; Serf: 192.168.1.32:4648 Bind Addrs: HTTP: [0.0.0.0:4646]; RPC: 0.0.0.0:4647; Serf: 0.0.0.0:4648 Client: true Log Level: INFO Node Id: 2921dae9-99dc-a65d-1a1f-25d9822c1500 Region: global (DC: dc1) Server: true Version: 1.7.7 ==> Nomad agent started! Log data will stream in below:
The Nomad UI will be available in the advertise address. Here it is, 192.168.1.32:4646
, This will be based on the network interface you are connected and the value will be different for different machines. Going to this IP and port in web browser will open nomad UI as shown below.
You have installed both nomad and docker in the system and ready to deploy the observability applications!
Run Loki in Nomad
Loki is a scalable log aggregation system and is designed to be very cost effective and easy to operate. It does not index the contents of the logs, but rather a set of labels for each log stream. It is configured such that the logs are persisted to the file system. It can also be configured to store the data in various datastores like AWS S3, Minio and so on.
The nomad job file used to run loki is given below.
job "loki" { datacenters = ["dc1"] type = "service" group "loki" { count = 1 network { mode = "host" port "loki" { to = 3100 static = 3100 } } service { name = "loki" port = "loki" provider = "nomad" } task "loki" { driver = "docker" user = "root" config { image = "grafana/loki:2.9.7" args = [ "-config.file", "local/config.yml", ] volumes = ["/loki_data:/loki"] ports = ["loki"] } template { data=<<EOH auth_enabled: false server: http_listen_port: 3100 ingester: lifecycler: address: 127.0.0.1 ring: kvstore: store: inmemory replication_factor: 1 final_sleep: 0s # Any chunk not receiving new logs in this time will be flushed chunk_idle_period: 1h # All chunks will be flushed when they hit this age, default is 1h max_chunk_age: 1h # Loki will attempt to build chunks up to 1.5MB, flushing if chunk_idle_period or max_chunk_age is reached first chunk_target_size: 1048576 # Must be greater than index read cache TTL if using an index cache (Default index read cache TTL is 5m) chunk_retain_period: 30s max_transfer_retries: 0 # Chunk transfers disabled wal: enabled: true dir: "/loki/wal" schema_config: configs: - from: 2020-10-24 store: boltdb-shipper object_store: filesystem schema: v11 index: prefix: index_ period: 24h storage_config: boltdb_shipper: active_index_directory: /loki/boltdb-shipper-active cache_location: /loki/boltdb-shipper-cache cache_ttl: 24h # Can be increased for faster performance over longer query periods, uses more disk space shared_store: filesystem filesystem: directory: /loki/chunks compactor: working_directory: /loki/boltdb-shipper-compactor shared_store: filesystem limits_config: reject_old_samples: true reject_old_samples_max_age: 168h chunk_store_config: max_look_back_period: 0s table_manager: retention_deletes_enabled: false retention_period: 0s EOH destination = "local/config.yml" change_mode = "restart" } resources { cpu = 1000 #Mhz memory = 1000 #MB } } } }
Copy the above job configuration and add it in the nomad UI.
Deploy Logging app
The logging app is a simple docker container which emits logs randomly. It send 4 log levels, INFO, ERROR, WARNING and DEBUG to stdout. The job file is give below.
job "logger" { datacenters = ["dc1"] type = "service" group "logger" { count = 1 task "logger" { driver = "docker" config { image = "chentex/random-logger:latest" } resources { cpu = 100 # 100 MHz memory = 100 # 100MB } } } }
Copy the above configuration and deploy another job from Nomad UI.
Deploy Vector in Nomad
Vector is log collecting agent which support many source and destination for log ingestion and export. Here, vector will collect the log data using docker_logs source and send to loki. The job file is given below.
job "vector" { datacenters = ["dc1"] # system job, runs on all nodes type = "system" group "vector" { count = 1 network { port "api" { to = 8686 } } ephemeral_disk { size = 500 sticky = true } task "vector" { driver = "docker" config { image = "timberio/vector:0.30.0-debian" ports = ["api"] volumes = ["/var/run/docker.sock:/var/run/docker.sock"] } env { VECTOR_CONFIG = "local/vector.toml" VECTOR_REQUIRE_HEALTHY = "false" } resources { cpu = 100 # 100 MHz memory = 100 # 100MB } # template with Vector's configuration template { destination = "local/vector.toml" change_mode = "signal" change_signal = "SIGHUP" # overriding the delimiters to [[ ]] to avoid conflicts with Vector's native templating, which also uses {{ }} left_delimiter = "[[" right_delimiter = "]]" data=<<EOH data_dir = "alloc/data/vector/" [api] enabled = true address = "0.0.0.0:8686" playground = true [sources.logs] type = "docker_logs" [sinks.out] type = "console" inputs = [ "logs" ] encoding.codec = "json" target = "stdout" [sinks.loki] type = "loki" compression = "snappy" encoding.codec = "json" inputs = ["logs"] endpoint = "http://[[ range nomadService "loki" ]][[.Address]]:[[.Port]][[ end ]]" healthcheck.enabled = true out_of_order_action = "drop" # remove fields that have been converted to labels to avoid having the field twice remove_label_fields = true [sinks.loki.labels] # See https://vector.dev/docs/reference/vrl/expressions/#path-example-nested-path job = "{{label.\"com.hashicorp.nomad.job_name\" }}" task = "{{label.\"com.hashicorp.nomad.task_name\" }}" group = "{{label.\"com.hashicorp.nomad.task_group_name\" }}" namespace = "{{label.\"com.hashicorp.nomad.namespace\" }}" node = "{{label.\"com.hashicorp.nomad.node_name\" }}" EOH } kill_timeout = "30s" } } }
Copy the above configuration and deploy the job from Nomad UI.Once vector is deployed, it will start collecting the logs from docker logs and send it to Loki.
Deploy Grafana
Grafana is a popular tool to visualize logs, metrics and traces. Here we will use grafana and its Loki Data source connector to view and explore log data sent by docker applications running in Nomad.
The grafana nomad job file is given below.
job "grafana" { datacenters = ["dc1"] type = "service" group "grafana" { count = 1 network { mode = "host" port "grafana" { to = 3000 static = 3000 } } task "grafana" { driver = "docker" env { GF_LOG_LEVEL = "ERROR" GF_LOG_MODE = "console" GF_PATHS_DATA = "/var/lib/grafana" } user = "root" config { image = "grafana/grafana:10.4.2" ports = ["grafana"] volumes = ["/grafana_volume:/var/lib/grafana"] } resources { cpu = 2000 memory = 2000 } } } }
Deploy this job using the Nomad UI as described in previous section.
After deploying all the jobs, the Nomad UI looks like this.
Connecting to Loki Data source in Grafana
After confirming all jobs are running as expected, open the grafana UI on :3000. For me the address is my host machine's private IP which is http://192.168.1.32:3000. Login to the Dashboard, using default username admin
and password admin
.
- Select the Data sources option, press Add Data sources
- Search for loki and select it
- Update the connection URL of loki. Based on the loki nomad job file, it is listening on static port 3100. The connection url will be like this, http://(machine-ip):3100.
- keep other options default and click on Save & Test
- If the connection is successful, it shows Data source successfully connected.
Visualising Loki Logs in Grafana
- Select the Explore option in Grafana
- Select Loki
- Select the Label jobs and choose logger Nomad job.
- Click Run Query to view the logs
You have successfully installed Nomad, Docker, and run applications on top of it, and queried the logs emitted by that application in Grafana.
Top comments (0)