Docker: zbieranie logów z kontenerów do Logstash
Problem
Mamy wiele kontenerów działających na jednej/wielu maszynach i chcemy zbierać logi ze wszystkich do Logstash-a (część Elastic Stack, dawniej ELK).
Możliwe rozwiązania
Jest kilka możliwości, jak “zawsze” wybieramy tą która najlepiej spełnia nasze potrzeby.
Większość jest opisana w Top 5 Docker logging methods to fit your container deployment strategy:
- Aplikacja wysyła logi prosto to serwisu zbierającego (u nas Logstash)
- Zapisujemy logi do wolumenów Docker-a
- Korzystamy z Docker-owego drivera logów
- Dedykowany kontener zbierający logi, jeden na całą maszynę
- Podejście Sidecar, każdy kontener ma swój, dedykowany kontener zbierający logi
Nie będę się tutaj rozpisywał o każdej z nich. Są dobrze opisane pod powyższym linkiem.
Jest jeden problem z każdym z tych rozwiązań: nie ma obsługi wielolinijkowych (“multiline”) logów ponieważ Docker każdą linijkę traktuje jako oddzielny log. Niestery nie zapowiada się żeby to naprawili: log driver should support multiline #22920.
Dedykowany kontener zbierający logi
Opiszę tą która według mnie jest najbardziej przyjazna dla użytkownika końcowego i wymaga stosunkowo mało konfiguracji.
Będziemy potrzebowali dwie rzeczy:
- Wszystkie serwisy w kontenerach są skonfigurowane do wysyłania logów na
STDOUT
iSTDERR
- Jeden dodatkowy kontener zawierający dostosowany do nas Logspout
Konfiguracja pierwszego punktu jest zależna od serwisu który działa w kontenerze i każdy będzie musiał sam to przygotować.
Logspout
Jest to program przekazujący logi z kontenerów Docker-a w wybrane przez nas miejsce. Sam też jest przystosowany do działania w kontenerze. Ma możliwość rozszerzania funkcjonalności z czego skorzystamy przy routownaiu logów do Logstash-a.
W momencie kiedy Logspout zostanie włączony łączy się on z Docker-em
na maszynie, pobiera informacje o wszystkich uruchomionych kontenerach i łączy
się z nimi pobierając wszystkie logi z STDOUT
oraz STDERR
. Następnie
wysyła je do skonfigurowanego miejsca docelowego.
Żeby współdziałał on z Logstash-em musimy skompilować go razem z modułem logspout-logstash.
W tym celu w folderze gdzie będziemy trzymać pliki dla naszego kontenera musimy stworzyć trzy pliki potrzebna nam do zbudowania Logspout-a z potrzebnym modułem.
Można je skopiować z folderu https://github.com/gliderlabs/logspout/tree/master/custom lub wykorzystać poniższe przykłady:
build.sh
#!/bin/sh
# DO NOT REMOVE, file used by Logspout build process
apk add --update go build-base git mercurial ca-certificates
mkdir -p /go/src/github.com/gliderlabs
cp -r /src /go/src/github.com/gliderlabs/logspout
cd /go/src/github.com/gliderlabs/logspout || exit
export GOPATH=/go
go get -v
go build -ldflags "-X main.Version=$1" -o /bin/logspout
apk del go git mercurial build-base
rm -rf /go /var/cache/apk/* /root/.glide
modules.go
// DO NOT REMOVE, file used by Logspout build process
package main
import (
_ "github.com/gliderlabs/logspout/transports/tcp"
_ "github.com/gliderlabs/logspout/transports/udp"
_ "github.com/looplab/logspout-logstash"
)
I najważniejszy dla nas
Dockerfile
FROM gliderlabs/logspout:master
ENV ROUTE_URIS=logstash+tcp://127.0.0.1:5610
RUN apk add --no-cache tini
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["/bin/logspout"]
Do plików build.sh
oraz modules.go
nie odnosimy się bezpośrednio w naszym
Dockerfile
. Są one wykorzystywane już przez sam proces budowania
Lobspout-a dzięki dyrektywie Docker-a ONBUILD
w https://github.com/gliderlabs/logspout/blob/master/Dockerfile#L9-L11.
Ustawiamy zmienną środowiskową ROUTE_URIS
która wskazuje gdzie znajduje
się Logstash oraz po jakim protokole chcemy się z nim łączyć. Ma ona format
ROUTE_URIS=logstash://host:port
i domyślnie wysyła logi za pomocą UDP
.
W tym przypadku skonfigurowałem połącznie po TCP
.
Logstash
Musimy jeszcze odpowiednio skonfigurować Logstash-a żeby czekał na logi na odpowiednim porcie.
W pliku konfiguracyjnym ustawiamy input
w następujący sposób:
input {
udp {
port => 5610
codec => json
}
tcp {
port => 5610
codec => json
}
}
Wszystkie logi zawierają teraz dane z jakiego kontenera zostały zebrane:
"docker": {
"hostname": "866e2ca94f5f",
"id": "866e2ca94f5fe11d57add5a78232c53dfb6187f04f6e150ec15f0ae1e1737731",
"image": "centos:7",
"name": "/ecstatic_murdock"
}
Możemy dodatkowo opisać kontenery dodatkowymi danymi za pomocą zmiennych
środowiskowych np. tagami za pomocą LOGSTASH_TAGS="docker,produkcja"
albo polami Logstash-a przy użyciu LOGSTASH_FIELDS="service=mysql"
.
Dokładniejszy opis opcji konfiguracyjnych można przeczytać na stronie
modułu: Available configuration options.
Jeśli chcemy możemy ustawić filtr który zmienia nam dane o kontenerze Docker-a
oraz dodatkowe tagi (np. service
) na dane w dimensions
:
filter {
ruby {
code => '
event["dimensions"] = []
event["docker"].each do |key, value|
unless event["docker"][key].nil?
event["dimensions"].push([key, value].to_s)
end
end
if event.to_hash.has_key?("service")
event["dimensions"].push(["service", event["service"]].to_s)
end
'
}
mutate {
remove_field => ["docker"]
}
}
Na koniec dobrym pomysłem jeszcze będzie wykluczenie kontenera Logspout oraz
samego Logstash-a ze zbierania logów żeby nie doszło do zapętlenia. Zrobimy
to używając zmiennej środowiskowej LOGSPOUT=ignore
dla każdego z kontenerów,
więcej info: Ignoring specific containers.
Zakończenie
Jeśli wszystko zostało dobrze skonfigurowane logi powinny zacząć spływać do Elasticsearch-a oraz być widoczne w Kibanie.
[ELK]: Elasticsearch Logstash Kibana