grafana alloy discovery docker nginx

Типичный (наш) стек журналов на базе Loki состоит из трех компонентов:

  1. Agent — агент Alloy (собирает логи, превращает их в потоки, добавляя метки, и отправляет потоки в Loki через HTTP API. Можно использовать и другие агенты (Promtail, агент Grafana).
  2. Loki — основной сервер, отвечающий за прием и хранение журналов и обработку запросов. Его можно развернуть в трех различных конфигурациях, дополнительную информацию см. в режимах развертывания.
  3. Grafana  — запрашивает данные из Loki и отображает нам. Можно запрашивать данные из командной строки, используя LogCLI (используя язык LogQL на подобии PromQL) или напрямую используя API Loki.

Начинаем

Чтобы разобраться, все действия буду проводить графаной в облаке, а значит нужно:

  1. зарегистрироваться в https://grafana.com/ и получить свой адрес https://xxx.grafana.net/
  2. переходим на страницу https://xxx.grafana.net/a/grafana-collector-app/alloy/installation вкладка Installation
  3. указать Token name и получить код установки Alloy (как установить из консоли).

По факту запуска видим:

Alloy is now running!
To check the status of Alloy, run:   sudo systemctl status alloy.service
To restart Alloy, run:               sudo systemctl restart alloy.service
The config file is located at:       /etc/alloy/config.alloy

А дефолтный файл /etc/alloy/config.alloy будет с таким содержимым:

prometheus.exporter.self "integrations_alloy" { }

discovery.relabel "integrations_alloy" {
  targets = prometheus.exporter.self.integrations_alloy.targets

  rule {
    target_label = "instance"
    replacement  = constants.hostname
  }

  rule {
    target_label = "alloy_hostname"
    replacement  = constants.hostname
  }

  rule {
    target_label = "job"
    replacement  = "integrations/alloy-check"
  }
}
# 
prometheus.scrape "integrations_alloy" {
  targets    = discovery.relabel.integrations_alloy.output
  forward_to = [prometheus.relabel.integrations_alloy.receiver]  

  scrape_interval = "60s"
}

prometheus.relabel "integrations_alloy" {
  forward_to = [prometheus.remote_write.metrics_service.receiver]

  rule {
    source_labels = ["__name__"]
    regex         = "(prometheus_target_sync_length_seconds_sum|prometheus_target_scrapes_.*|prometheus_target_interval.*|prometheus_sd_discovered_targets|alloy_build.*|prometheus_remote_write_wal_samples_appended_total|process_start_time_seconds)"
    action        = "keep"
  }
}

prometheus.remote_write "metrics_service" {
  endpoint {
    url = "https://prometheus-prod-24-prod-eu-west-2.grafana.net/api/prom/push"

    basic_auth {
      username = "XXX"
      password = "XXX="
    }
  }
}

loki.write "grafana_cloud_loki" {
  endpoint {
    url = "https://logs-prod-012.grafana.net/loki/api/v1/push"

    basic_auth {
      username = "XXX"
      password = "XXX="
    }
  }
}

Проверим работу Alloy, для этого откроем в браузере http://localhost:12345 (красным показал как данные двигаются на самом деле):

Как видно, prometheus.scrape это главный компонент, который собирает в себя, а потом отправляет в место назначения:

  • targets - где взять данные
  • forward_to - куда отправить данные

А в графане на странице https://xxx.grafana.net/a/grafana-collector-app/alloy/monitoring мы видим уже собранные данные:

Пусть Alloy все делает сам

Когда изучаешь новый интрумент, тебе хочется начать с чего-то простого, и я предолженным шагам в https://xxx.grafana.net/ решил настроить Alloy для отслеживания работы самого докера конечно вместе с логами, и вот как это должно выглядеть по факту:

Для этого нужно разрешить Alloy вычивать данные из docker-a и кажется достаточно просто настроить конфигурацию, чтобы данные пошли в loki, а из loki забирать данные Графаной (инструкция есть в https://xxx.grafana.net/ - Add new connection - Docker).

Однако, я столкнулся с рядом проблем (ошибок со стороны Alloy), а именно:

  • "could not set up a wait request to the Docker client"
  • final error sending batch" component_path=/ component_id=loki.write.grafana_cloud_loki component=client host=logs-prod-012.grafana.net status=400 tenant="" error="server returned HTTP status 400 Bad Request (400): entry with timestamp 2024-04-17 16:27:14.629849105 +0000 UTC ignored, reason: 'entry too far behind, oldest acceptable timestamp is: 2024-04-17T18:45:43Z',"

Поэтому, на дев-машине я пока оставляю рабочим этот вариант (буду тестировать), а на проде буду использовать более понятный вариант сбора логов (более контролируемый и понятный, но в который все равно нужно въехать, о нем ниже).

Просто и понятно

Прежде чем начать, хочу дать совет для подключения используйте инструкции в https://xxx.grafana.net/ - Add new connection, а документацию на сайте используйте уже потом (для необходимой кастомизации), но не наоборот. Итак, что у нас будет:

1. docker (nginx) пишет логи в свои файлы (docker сам будет ротировать лог-файлы)

2. alloy читает эти лог-файлы и отправляет данные в базу данных loki, вот детали:

  • компонент loki.source.file читет файл nginx.log с помощью компонента local.file_match
  • компонент loki.source.file читает из loki.source.file и отправляет в loki.write
  • компонент loki.write отправяет данные по адресу endpoint.url

Что мы не будем делать

Мы не будем использовать UDP: вот почему не стоит отправлять логи по UDP из докера в Alloy loki.source.gelf который слушает UDP порт, так что следующий вариант не будем использовать:

docker run --log-driver syslog --log-opt syslog-address=udp://1.2.3.4:1111 alpine echo hello world

Что мы будем делать

Чтобы все это заработало, нужно следовать инструкции на странице https://xxx.grafana.net/connections/add-new-connection/nginx?page=alloy вкладка Configuration Details (такая же инструкция в документации) а именно:

1. Объявить формат логов в файле /etc/nginx/nginx.conf в секции http (чтобы nginx писал логи в json-формате):

user  nginx;
worker_processes  auto;

error_log  /dev/fd/2 error;

...

http {

    ...

    log_format json_analytics escape=json '{'
        '"msec": "$msec", ' # request unixtime in seconds with a milliseconds resolution
        '"connection": "$connection", ' # connection serial number
        '"connection_requests": "$connection_requests", ' # number of requests made in connection
        '"pid": "$pid", ' # process pid
        '"request_id": "$request_id", ' # the unique request id
        '"request_length": "$request_length", ' # request length (including headers and body)
        '"remote_addr": "$remote_addr", ' # client IP
        '"remote_user": "$remote_user", ' # client HTTP username
        '"remote_port": "$remote_port", ' # client port
        '"time_local": "$time_local", '
        '"time_iso8601": "$time_iso8601", ' # local time in the ISO 8601 standard format
        '"request": "$request", ' # full path no arguments if the request
        '"request_uri": "$request_uri", ' # full path and arguments if the request
        '"args": "$args", ' # args
        '"status": "$status", ' # response status code
        '"body_bytes_sent": "$body_bytes_sent", ' # the number of body bytes exclude headers sent to a client
        '"bytes_sent": "$bytes_sent", ' # the number of bytes sent to a client
        '"http_referer": "$http_referer", ' # HTTP referer
        '"http_user_agent": "$http_user_agent", ' # user agent
        '"http_x_forwarded_for": "$http_x_forwarded_for", ' # http_x_forwarded_for
        '"http_host": "$http_host", ' # the request Host: header
        '"server_name": "$server_name", ' # the name of the vhost serving the request
        '"request_time": "$request_time", ' # request processing time in seconds with msec resolution
        '"upstream": "$upstream_addr", ' # upstream backend server for proxied requests
        '"upstream_connect_time": "$upstream_connect_time", ' # upstream handshake time incl. TLS
        '"upstream_header_time": "$upstream_header_time", ' # time spent receiving upstream headers
        '"upstream_response_time": "$upstream_response_time", ' # time spent receiving upstream body
        '"upstream_response_length": "$upstream_response_length", ' # upstream response length
        '"upstream_cache_status": "$upstream_cache_status", ' # cache HIT/MISS where applicable
        '"ssl_protocol": "$ssl_protocol", ' # TLS protocol
        '"ssl_cipher": "$ssl_cipher", ' # TLS cipher
        '"scheme": "$scheme", ' # http or https
        '"request_method": "$request_method", ' # request method
        '"server_protocol": "$server_protocol", ' # request protocol, like HTTP/1.1 or HTTP/2.0
        '"pipe": "$pipe", ' # "p" if request was pipelined, "." otherwise
        '"gzip_ratio": "$gzip_ratio"'
    '}';

    access_log /dev/fd/1 json_analytics;

    server {
        server_name app-frontend;
        listen 8000 default_server;
        root /app;
        location / {
            try_files $uri 404;
        }
    }
}

2. Настроить слежку за всеми логами, которые пишет докер:

local.file_match "tmp" {
  path_targets = [{"__path__" = "/var/lib/docker/containers/**/*-json.log"}]
}

3. Даем Alloy права доступа на чтение лог-файлов докера.

4. Вот только nginx тоже пишет в json, поэтому получился json в json, пример:

{"log":"{\"msec\": \"1713252610.587\", \"connection\": \"1\", \"connection_requests\": \"2\", \"pid\": \"30\", \"request_id\": \"d993e679d01b173b2a19716c585b4278\", \"request_length\": \"662\", \"remote_addr\": \"172.16.54.1\", \"remote_user\": \"\", \"remote_port\": \"47238\", \"time_local\": \"16/Apr/2024:07:30:10 +0000\", \"time_iso8601\": \"2024-04-16T07:30:10+00:00\", \"request\": \"GET /favicon.ico HTTP/1.1\", \"request_uri\": \"/favicon.ico\", \"args\": \"\", \"status\": \"200\", \"body_bytes_sent\": \"15406\", \"bytes_sent\": \"15650\", \"http_referer\": \"http://localhost:8000/robots.txt\", \"http_user_agent\": \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36\", \"http_x_forwarded_for\": \"\", \"http_host\": \"localhost:8000\", \"server_name\": \"app-frontend\", \"request_time\": \"0.000\", \"upstream\": \"\", \"upstream_connect_time\": \"\", \"upstream_header_time\": \"\", \"upstream_response_time\": \"\", \"upstream_response_length\": \"\", \"upstream_cache_status\": \"\", \"ssl_protocol\": \"\", \"ssl_cipher\": \"\", \"scheme\": \"http\", \"request_method\": \"GET\", \"server_protocol\": \"HTTP/1.1\", \"pipe\": \".\", \"gzip_ratio\": \"\"}\n","stream":"stdout","time":"2024-04-16T07:30:10.588288009Z"}

Чтобы внутренний json превратить в нормальный, воспользуемся loki.process инструкцией stage.docker (или stage.cri) и в файл /etc/alloy/config.alloy добавим: 

loki.process "our_dockers" {
  forward_to = [loki.write.grafana_cloud_loki.receiver]

  stage.docker {}
}

И не забываем, что кто-то должен переместить данные из local.file_match "tmp" в loki.process "our_dockers":

loki.source.file "files" {
  targets    = local.file_match.tmp.targets
  forward_to = [loki.process.our_dockers.receiver]
}

5. Перезапускаем Alloy: sudo systemctl restart alloy.service и смотрим статус, если конфиг в порядке, то увидим:

$ sudo systemctl status alloy.service
● alloy.service - Vendor-agnostic OpenTelemetry Collector distribution with programmable pipelines
     Loaded: loaded (/lib/systemd/system/alloy.service; enabled; vendor preset: enabled)
     Active: active (running) since Tue 2024-04-16 15:09:10 MSK; 47s ago
       Docs: https://grafana.com/docs/alloy
   Main PID: 1818021 (alloy)
      Tasks: 19 (limit: 38083)
     Memory: 44.2M
        CPU: 675ms
     CGroup: /system.slice/alloy.service
             └─1818021 /usr/bin/alloy run --storage.path=/var/lib/alloy/data /etc/alloy/config.alloy

апр 16 15:09:10 server alloy[1818021]: ts=2024-04-16T12:09:10.650496356Z level=info msg="finished ...

И получаем:

6. Заходим на https://xxx.grafana.net/connections/add-new-connection/nginx?page=alloy и видим, что подключение состоялось:

7. Заходим на страницу интеграции https://xxx.grafana.net/dashboards/f/integration---nginx/integration-nginx и видим, что все хорошо:

8. Нажимаем "Loki NGINX Service Mesh - JSON version" (там где оранжевая плашка) и видим такую страницу:

Дальше можно нажать на значок троеточия и нажать Explore, в результате увидим наши логи:

Внимание: стрелочкой показал на логи, которые были созданы еще до того, как я настроил nginx писать логи в json.

Если что-то пошло не так, то заходим в https://xxx.grafana.net/connections/datasources и выбираем grafanacloud-logs и внизу страницы нажимаем кнопку Test и получаем информацию с ссылками:

Data source successfully connected.
Next, you can start to visualize data by "building a dashboard" or by querying data in "the Explore view"

На этой же странице справа вверху видим кнопку "Explore data", благодаря которой можно посмотреть на таблицу с данными.

Alloy в docker

Да, Alloy можно запустить в docker-контейнере:

docker run -d \
 --name grafana-alloy \
 --restart=unless-stopped \
 -v <DATA_FILE_PATH>:/var/lib/alloy/data \
 -v <CONFIG_FILE_PATH>:/etc/alloy/config.alloy \
 -v /var/lib/docker/containers:/var/lib/docker/containers \
 -p 12345:12345 \
 grafana/alloy:latest \
 run --server.http.listen-addr=0.0.0.0:12345 --storage.path=/var/lib/alloy/data /etc/alloy/config.alloy

А дальше можно посмотреть видосики: 1

Дебаг

Когда я только тестировал настройки, то совершал ошибки (конечно никаких логов в Grafana я не видел), поэтому начал дебажить, для этого в файл /etc/alloy/config.alloy добавил инструкцию логирования (по-умолчанию level  = "info"):

logging {
  level  = "debug"
  format = "logfmt"
}

Перезапускаем sudo systemctl restart alloy.service и проверяем что работает sudo systemctl status alloy.service

Видим, что успешный статус + последние лог-записи, которые сделал Alloy. Но, последних логов недостаточно, поэтому начинаю следить за логами (в моем случае ошибка была вначале):

journalctl -u alloy.service -f

В отдельном окне перезапускайте Alloy и смотрите записи на предмет error, у меня было так:

...
...level=error msg="failed to start reporter" err="context canceled"
...
...level=info msg="loki.source.file component shutting down, stopping readers and positions file" component_path=/ component_id=loki.source.file.files
...level=error msg="error running exporter" component_path=/ component_id=prometheus.exporter.self.integrations_alloy err="context canceled"

сноска: последние лог-записи можно посмотреть так:  journalctl -u alloy.service -n 300 --no-pager | grep loki

Еще я тестировал более простые настройки, для этого создал файл /var/www/jb/app/alloy.json в который сохранил несколько json-логов ngnix, вот какие настройки я указал:

local.file_match "tmp" {
  path_targets = [{"__path__" = "/var/www/jb/app/alloy.json"}]
}
loki.source.file "files" {
  targets    = local.file_match.tmp.targets
  forward_to = [loki.write.grafana_cloud_loki.receiver]
}

Дальше я перешел на страницу просмотра логов и у меня ничего не отображалось, в блоке Label filters:

  • клацнул по пустому полю Select label и выбрал filename
  • клацнул по пустому полю Select value и выбрал мой файл /var/www/jb/app/alloy.json

и логи отобразились:

Оказывается проблема была в том, что у Alloy не хватало прав на чтение файлов созданных из под рута (докер создает файлы из под рута и гугл говорит, что иначе докер не умеет).

А как же json

Переходим в левом меню в Explore и дальше по шагам:

  1. выбираем нашу loki-базку
  2. перключаемся на Code (чтобы применить json-фильтр)
  3. применяем запрос + json-фильтр
  4. получаем возможность фильтровать по всем json-полям

Loki фильтры

Взять любые значения и применить к ним фильтр json:

{stream=~".+", job=~".*", instance=~".*", cluster=""} | json

эквивалент:

{stream=~".+"} | json

В распарсенном json поискать значения парочки ключей:

{stream=~".+"} | json | http_host=`site.ru` | status > 299

Вот так я ищу ошибку в monolog:

{stream=~".+"} | json | message=`error message`

Найти точно по левой части и что-угодно справа:

request_uri=~"^/foo(.*)"

Найти по части совпадения строки:

{stream=~".+"}|~ "(.*)Find and save image problems(.*)"

Тоже самое, но сначала распарсить json:

{stream=~".+"} | json | message=~`(.*)Uncaught PHP Exception Doctrine(.*)`

OR условие:

... | json | scheme=`https` | status=~"301|302" [$__auto])))

Отрицание:

http_referer !~`(http|https)://yapro.ru(.*)`

Сформировать строку из разных значений:

| line_format "➡️ {{.request_method}} {{.request_uri}} with HTTP status: {{.status}} "

Из документации:

  • =: exactly equal, example:
  • =~: regex matches, example: {name =~ "mysql.+"}
  • |=: Log line contains string, example: {job="mysql"} |= "error" или |= "error"
  • |~: Log line contains a match to the regular expression, examples:
    {name="a"} |~ "t-ops.*io:2003"
    {name="a"} |~ `error=\w+`
  • !=: not equal, example: {instance=~"kafka-[23]"} != "kafka.server:type=ReplicaManager"
  • !~: regex does not match, example: {name !~ "mysql.+"} или {name !~ `mysql-\d+`}

Еще немного:|= "string" — ищет вхождения строки (регистрозависимо) во всей текстовой линии лога Log Queries | Grafana Loki.

{job="mysql"} |= "error" != "timeout"
{job="mysql"} |= "error" | json | line_format "{{.err}}"
{job="mysql"} | json | line_format "{{.message}}" |= "error"

Агрегация

Применить функцию подсчета записей, чтобы построить график:

sum(count_over_time({stream=~".+", job=~".*", instance=~".*", cluster=""} | json | http_host=`yapro.ru` | status > 299 [$__auto]))

ГЛАВНОЕ: при этом чуть ниже настройки выбрать: Type: Instant 

Сгруппировать по статусу ответа:

sum by (status) (rate({stream=~".+"} | json | scheme=`https` [$__auto]))

тоже самое (пока не понимаю чем rate от count_over_time отличается:

sum by (status) (count_over_time({stream=~".+"} | json | scheme=`https` [$__interval]))

но в мануале написано (а еще тут подробно разжовывается):

rate(unwrapped-range): вычисляет посекундную скорость всех значений в указанном интервале

sum_over_time(unwrapped-range): сумма всех значений в указанном интервале

avg_over_time(unwrapped-range): среднее значение всех точек в указанном интервале

max_over_time(unwrapped-range): максимальное значение всех точек в указанном интервале

min_over_time(unwrapped-range): минимальное значение всех точек в указанном интервале

stdvar_over_time(unwrapped-range): стандартная дисперсия значений в указанном интервале.

stddev_over_time(unwrapped-range): стандартное отклонение генеральной совокупности значений в указанном интервале.

quantile_over_time(scalar,unwrapped-range): φ-квантиль (0 ≤ φ ≤ 1) значений в указанном интервале (примеры)

Среднее значение по полю (берем значение из поля request_time за промежуток времени и делим на количество записей в этот промежуток):

avg_over_time({stream=~".+"} | json | scheme=`https` | unwrap request_time | __error__="" [$__auto]) by (cluster)

Больше примеров тут. Для внимательных - мы добавили | __error__="" чтобы исключать ошибочные логи (насколько я понимаю, не имеющие поля request_time

Надо бы разобраться в этих:

  • sum
  • sum_over_time
  • count_over_time

Регулярки:

sum by (host) (count_over_time({host=~"A.*"} |= "filter" [3h]))
/
sum by (host) (sum_over_time({host=~"A.*", units="sub-item"} |= "data" | regexp `val:(?P<myval>\d+),` | unwrap myval [3h]))

Рассчитайте p99 задержки nginx-ingress по пути:

quantile_over_time(0.99,
  {cluster="ops-tools1",container="ingress-nginx"}
    | json
    | __error__ = ""
    | unwrap request_time [1m]) by (path)

Следующий запрос показывает, как можно переформатировать строку журнала, чтобы ее было легче читать на экране.

{cluster="ops-tools1", name="querier", namespace="loki-dev"}
  |= "metrics.go" != "loki-canary"
  | logfmt
  | query != ""
  | label_format query="{{ Replace .query \"\\n\" \"\" -1 }}"
  | line_format "{{ .ts}}\t{{.duration}}\ttraceID = {{.traceID}}\t{{ printf \"%-100.100s\" .query }} "

Форматирование меток используется для очистки запроса, в то время как формат строки сокращает объем информации и создает табличный вывод.

Мои эксперименты:

sum by (row_count) (sum_over_time({stream=~".+"} | json | scheme=`https` | http_host=`yapro.ru` | request_uri=~"/articles(.*)" | http_referer =~ `(.+)(yandex\.ru)(.*)` | http_user_agent!~"(.*)(bot)(.*)"| line_format "row_count=1"| logfmt | unwrap row_count|__error__="" [$__auto])) + INSTANCE

Примеры

Количество переходов с Google (Type=Instance):

sum(count_over_time({stream=~".+"} | json | scheme = `https` | http_host = `yapro.ru` | http_referer =~ `(.+)(google\.com)(.*)` [$__auto]))

Все те же переходы, только с группировкой по remote_addr - полезно, когда выводишь таблицу:

sum by (remote_addr) (count_over_time({stream=~".+"} | json | scheme = `https` | http_host = `yapro.ru` | http_referer =~ `(.+)(google\.com)(.*)` [$__auto]))

в итоге таблица выглядит так:

А если нужно просто число уникальных пользователей, то количество IP перешедших с Google:

count(sum by (remote_addr) (count_over_time({stream=~".+"} | json | scheme = `https` | http_host = `yapro.ru` | http_referer =~ `(.+)(google\.com)(.*)` [$__auto])))

А если нужно сложить весь правый столбец и показать одно число:

sum(sum by (remote_addr) (count_over_time({stream=~".+"} | json | scheme = `https` | http_host = `yapro.ru` | http_referer =~ `(.+)(google\.com)(.*)` [$__auto])))

но если вдуматься, то зачем группировать, если можно просто сложить:

sum(count_over_time({stream=~".+"} | json | scheme = `https` | http_host = `yapro.ru` | http_referer =~ `(.+)(google\.com)(.*)` [$__auto]))

Как Alloy отправляет запросы на сервер

Можно посмотреть как он отправляет запросы, если направить конфиг на свой сайт в котором сохранить запрос в виде curl.

Поэтому просто в файл /etc/alloy/config.alloy добавляем:

local.file_match "logs_integrations_integrations_nginx" {
	path_targets = [{
		__path__    = "<path to json nginx access log>",
	}]
}

loki.source.file "logs_integrations_integrations_nginx" {
	targets    = local.file_match.logs_integrations_integrations_nginx.targets
	forward_to = [loki.write.grafana_cloud_loki.receiver]
}

используемые компоненты:

На странице интергации в облаке предлагали сконфигурировать так:

local.file_match "logs_integrations_integrations_nginx" {
	path_targets = [{
		__address__ = "localhost",
		__path__    = "<path to json nginx access log>",
		host        = "<http_hostname>",
		instance    = bestbuilder.server,
	}]
}

на странице конфигурации Alloy написан такой пример:

local.file "my_file" {
  filename = "/tmp/my-file.txt"
}
prometheus.remote_write "default" {
  endpoint {
    url = "http://localhost:9009/api/prom/push"
  }
}

Надо не забыть, что например когда контейнер еще только запускается, то пишет не в json, поэтому в файле *-json.log:

{"log":"[16-Apr-2024 07:28:43] NOTICE: fpm is running, pid 1\n","stream":"stderr","time":"2024-04-16T07:28:43.729502671Z"}
{"log":"[16-Apr-2024 07:28:43] NOTICE: ready to handle connections\n","stream":"stderr","time":"2024-04-16T07:28:43.729537216Z"}
{"log":"[16-Apr-2024 07:29:24] NOTICE: Finishing ...\n","stream":"stderr","time":"2024-04-16T07:29:24.041604778Z"}
{"log":"[16-Apr-2024 07:29:24] NOTICE: exiting, bye-bye!\n","stream":"stderr","time":"2024-04-16T07:29:24.041645623Z"}
{"log":"[16-Apr-2024 07:30:00] NOTICE: fpm is running, pid 1\n","stream":"stderr","time":"2024-04-16T07:30:00.221043605Z"}
{"log":"[16-Apr-2024 07:30:00] NOTICE: ready to handle connections\n","stream":"stderr","time":"2024-04-16T07:30:00.221060411Z"}

Лейблы

Из коробки в графане есть возможность фильтровать по нужным полям:

Однако этих фильтров недостаточно + из-за того, что я запускаю Alloy в докере, у меня они ущербны:

  • filename="/var/lib/docker/containers/c92f63bf65...-json.log"
  • service_name="unknown_service"
  • stream="stderr"

Поэтому, погуглив оказалось, называются эти фильтры - labels, которые можно переназначать и добавлять. 

Логи под капотом

Когда мы находимся на странице https://grafana/explore мы можем кликнуть на кнопку Query Inspector и в появившемся внизу блоке кликнуть на JSON, в котором увидим приблизительно следующее:

[
  {
    "schema": {
      "refId": "A",
      "meta": {
        "typeVersion": [
          0,
          0
        ],
        "custom": {
          "frameType": "LabeledTimeValues",
          "lokiQueryStatKey": "Summary: total bytes processed"
        },
        "stats": [
          {
            "displayName": "Summary: bytes processed per second",
            "unit": "Bps",
            "value": 58798060
          },
          {
            "displayName": "Summary: lines processed per second",
            "value": 528453
          },
          {
            "displayName": "Summary: total bytes processed",
            "unit": "decbytes",
            "value": 297410
          },
          {
            "displayName": "Summary: total lines processed",
            "value": 2673
          },
          {
            "displayName": "Summary: exec time",
            "unit": "s",
            "value": 0.005058
          },
          {
            "displayName": "Ingester: total reached",
            "value": 1
          },
          {
            "displayName": "Ingester: total chunks matched",
            "value": 6
          },
          {
            "displayName": "Ingester: total batches",
            "value": 2
          },
          {
            "displayName": "Ingester: total lines sent",
            "value": 2
          },
          {
            "displayName": "Ingester: head chunk bytes",
            "unit": "decbytes",
            "value": 0
          },
          {
            "displayName": "Ingester: head chunk lines",
            "value": 0
          },
          {
            "displayName": "Ingester: decompressed bytes",
            "unit": "decbytes",
            "value": 0
          },
          {
            "displayName": "Ingester: decompressed lines",
            "value": 0
          },
          {
            "displayName": "Ingester: compressed bytes",
            "unit": "decbytes",
            "value": 0
          },
          {
            "displayName": "Ingester: total duplicates",
            "value": 0
          }
        ],
        "executedQueryString": "Expr: {hostname=\"dc01-misc-pdocker01.yapro.ru\", service_name=~`(.*)`}",
        "preferredVisualisationType": "logs",
        "limit": 1000,
        "searchWords": []
      },
      "fields": [
        {
          "name": "labels",
          "type": "other",
          "typeInfo": {
            "frame": "json.RawMessage"
          },
          "config": {}
        },
        {
          "name": "Time",
          "type": "time",
          "typeInfo": {
            "frame": "time.Time"
          },
          "config": {}
        },
        {
          "name": "Line",
          "type": "string",
          "typeInfo": {
            "frame": "string"
          },
          "config": {}
        },
        {
          "name": "tsNs",
          "type": "string",
          "typeInfo": {
            "frame": "string"
          },
          "config": {}
        },
        {
          "name": "labelTypes",
          "type": "other",
          "typeInfo": {
            "frame": "json.RawMessage"
          },
          "config": {
            "custom": {
              "hidden": true
            }
          }
        },
        {
          "name": "id",
          "type": "string",
          "typeInfo": {
            "frame": "string"
          },
          "config": {}
        }
      ]
    },
    "data": {
      "values": [
        [
          {
            "detected_level": "unknown",
            "hostname": "dc01-misc-pdocker01.yapro.ru",
            "platform": "docker",
            "service_name": "scb-be-1.2.0"
          },
          {
            "detected_level": "unknown",
            "hostname": "dc01-misc-pdocker01.yapro.ru",
            "platform": "docker",
            "service_name": "scb-be-1.2.0"
          }
        ],
        [
          1768813758604,
          1768813758604
        ],
        [
          "{\"remote_user\":\"\",\"time_local\":\"19/Jan/2026:12:09:18 +0300\",\"request_uri\":\"/api/scb/asrDocuments\",\"request_uri_wo_query_string\":\"/api/scb/asrDocuments\",\"request_method\":\"POST\",\"status\":\"500\",\"status_group\":\"5xx\",\"request_time\":\"0.032\",\"upstream_response_time\":\"0.031\",\"body_bytes_sent\":\"5\",\"http_referer\":\"https://scb.ttk-svyaz.ru/documents\",\"http_user_agent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36\",\"remote_addr\":\"192.168.201.37\",\"x_forwarded_for\":\"192.168.12.26\"}",
          "{\"message\":\"Call to protected method App\\\\Controllers\\\\Documents\\\\DocumentsController::asrDocuments() from scope App\\\\Init\",\"context\":[{\"__type\":\"object\",\"__class\":\"Error\",\"message\":\"Call to protected method App\\\\Controllers\\\\Documents\\\\DocumentsController::asrDocuments() from scope App\\\\Init\",\"string\":\"\",\"code\":0,\"file\":\"/var/www/html/app/Init.php\",\"line\":45,\"trace\":[{\"file\":\"/var/www/html/public/index.php\",\"line\":5,\"function\":\"run\",\"class\":\"App\\\\Init\",\"type\":\"->\"}],\"previous\":null}],\"level\":400,\"level_name\":\"ERROR\",\"channel\":\"app\",\"datetime\":{\"__type\":\"object\",\"__class\":\"Monolog\\\\JsonSerializableDateTimeImmutable\",\"useMicroseconds\":true,\"date\":\"2026-01-19 12:09:18.603671\",\"timezone_type\":3,\"timezone\":\"Europe/Moscow\"},\"extra\":[]}"
        ],
        [
          "1768813758604808444",
          "1768813758604019307"
        ],
        [
          {
            "detected_level": "S",
            "hostname": "I",
            "platform": "I",
            "service_name": "I"
          },
          {
            "detected_level": "S",
            "hostname": "I",
            "platform": "I",
            "service_name": "I"
          }
        ],
        [
          "1768813758604808444_7b3fc3b0",
          "1768813758604019307_25750484"
        ]
      ],
      "nanos": [
        null,
        [
          808444,
          19307
        ],
        null,
        null,
        null,
        null
      ]
    }
  }
]

Тут четко видно, что сами логи идут отдельно от информации о логах (два разных массива), например:

{
  "detected_level": "unknown",
  "hostname": "dc01-misc-pdocker01.yapro.ru",
  "platform": "docker",
  "service_name": "scb-be-1.2.0"
}

На этом пока все, удачки.

p.s. self hosted решение графаны у себя на сервере настраивается так, но пока оставлю это на потом.

Полезное: 1


11.09.2021 07:08

Комментарии

Комментариев пока нет.


Copyrights © 2007 - 2026 YaPro.Ru