Rất có thể, ngày nay không ai hỏi tại sao cần thu thập số liệu dịch vụ. Bước hợp lý tiếp theo là thiết lập cảnh báo cho các số liệu đã thu thập, cảnh báo này sẽ thông báo về mọi sai lệch trong dữ liệu trong các kênh thuận tiện cho bạn (thư, Slack, Telegram). Trong dịch vụ đặt phòng khách sạn trực tuyến tất cả số liệu về dịch vụ của chúng tôi đều được đưa vào InfluxDB và hiển thị trong Grafana, đồng thời cảnh báo cơ bản cũng được định cấu hình ở đó. Đối với các nhiệm vụ như “bạn cần tính toán một cái gì đó và so sánh với nó”, chúng tôi sử dụng Kapacitor.

Kapacitor là một phần của ngăn xếp TICK có thể xử lý các số liệu từ InfluxDB. Nó có thể kết nối nhiều phép đo với nhau (nối), tính toán điều gì đó hữu ích từ dữ liệu nhận được, ghi kết quả trở lại InfluxDB, gửi cảnh báo tới Slack/Telegram/mail.
Toàn bộ ngăn xếp rất thú vị và chi tiết , nhưng sẽ luôn có những điều hữu ích mà không được nêu rõ ràng trong sách hướng dẫn. Trong bài viết này, tôi quyết định thu thập một số mẹo hữu ích, không rõ ràng (cú pháp cơ bản của TICKscipt được mô tả ) và chỉ ra cách áp dụng chúng bằng cách sử dụng một ví dụ để giải một trong các bài toán của chúng ta.
Chúng ta hãy đi!
float & int, lỗi tính toán
Một vấn đề hoàn toàn tiêu chuẩn, được giải quyết thông qua các diễn viên:
var alert_float = 5.0
var alert_int = 10
data|eval(lambda: float("value") > alert_float OR float("value") < float("alert_int"))
Sử dụng mặc định()
Nếu thẻ/trường không được điền, lỗi tính toán sẽ xảy ra:
|default()
.tag('status', 'empty')
.field('value', 0)
điền vào tham gia (bên trong và bên ngoài)
Theo mặc định, phép nối sẽ loại bỏ các điểm không có dữ liệu (bên trong).
Với fill('null'), phép nối ngoài sẽ được thực hiện, sau đó bạn cần thực hiện default() và điền vào các giá trị trống:
var data = res1
|join(res2)
.as('res1', 'res2)
.fill('null')
|default()
.field('res1.value', 0.0)
.field('res2.value', 100.0)
Vẫn còn một sắc thái ở đây. Trong ví dụ trên, nếu một trong các chuỗi (res1 hoặc res2) trống thì chuỗi (dữ liệu) kết quả cũng sẽ trống. Có một số vé về chủ đề này trên Github (, , ) – chúng tôi đang chờ sửa chữa và chịu đựng một chút.
Sử dụng điều kiện trong tính toán (nếu ở lambda)
|eval(lambda: if("value" > 0, true, false)
Năm phút cuối cùng từ đường ống trong khoảng thời gian
Ví dụ: bạn cần so sánh giá trị của năm phút cuối với tuần trước. Bạn có thể lấy hai lô dữ liệu thành hai lô riêng biệt hoặc trích xuất một phần dữ liệu từ khoảng thời gian lớn hơn:
|where(lambda: duration((unixNano(now()) - unixNano("time"))/1000, 1u) < 5m)
Một giải pháp thay thế trong năm phút cuối cùng là sử dụng BarrierNode, tính năng này sẽ cắt dữ liệu trước thời gian đã chỉ định:
|barrier()
.period(5m)
Ví dụ về sử dụng mẫu Go trong tin nhắn
Mẫu tương ứng với định dạng từ gói Dưới đây là một số câu đố thường gặp.
nếu khác
Chúng tôi sắp xếp mọi thứ theo thứ tự và không kích động mọi người bằng tin nhắn nữa:
|alert()
...
.message(
'{{ if eq .Level "OK" }}It is ok now{{ else }}Chief, everything is broken{{end}}'
)
Hai chữ số sau dấu thập phân trong tin nhắn
Cải thiện khả năng đọc của tin nhắn:
|alert()
...
.message(
'now value is {{ index .Fields "value" | printf "%0.2f" }}'
)
Mở rộng các biến trong tin nhắn
Chúng ta hiển thị thêm thông tin trong tin nhắn để trả lời câu hỏi “Tại sao lại la hét”?
var warnAlert = 10
|alert()
...
.message(
'Today value less then '+string(warnAlert)+'%'
)
Mã định danh cảnh báo duy nhất
Đây là điều cần thiết khi có nhiều hơn một nhóm trong dữ liệu, nếu không sẽ chỉ có một cảnh báo được tạo:
|alert()
...
.id('{{ index .Tags "myname" }}/{{ index .Tags "myfield" }}')
Trình xử lý tùy chỉnh
Danh sách lớn các trình xử lý bao gồm exec, cho phép bạn thực thi tập lệnh của mình với các tham số đã truyền (stdin) - sự sáng tạo và không có gì hơn thế!
Một trong những phong tục của chúng tôi là một tập lệnh Python nhỏ để gửi thông báo tới Slack.
Lúc đầu, chúng tôi muốn gửi một bức ảnh grafana được ủy quyền bảo vệ trong tin nhắn. Sau đó, viết OK trong chuỗi cho cảnh báo trước đó từ cùng một nhóm chứ không phải dưới dạng một tin nhắn riêng biệt. Một lát sau - thêm vào tin nhắn lỗi phổ biến nhất trong X phút vừa qua.
Một chủ đề riêng là liên lạc với các dịch vụ khác và bất kỳ hành động nào do cảnh báo bắt đầu (chỉ khi hoạt động giám sát của bạn hoạt động đủ tốt).
Một ví dụ về mô tả trình xử lý, trong đó Slack_handler.py là tập lệnh tự viết của chúng tôi:
topic: slack_graph
id: slack_graph.alert
match: level() != INFO AND changed() == TRUE
kind: exec
options:
prog: /sbin/slack_handler.py
args: ["-c", "CHANNELID", "--graph", "--search"]
Làm thế nào để gỡ lỗi?
Tùy chọn với đầu ra nhật ký
|log()
.level("error")
.prefix("something")
Xem (cli): kapacitor -url :9092 nhật ký lvl=lỗi
Tùy chọn với httpOut
Hiển thị dữ liệu trong đường dẫn hiện tại:
|httpOut('something')
Xem (nhận): :9092/kapacitor/v1/tasks/tên_nhiệm_vụ/cái_gì_đó
kế hoạch thực hiện
- Mỗi tác vụ trả về một cây thực thi với các số hữu ích ở định dạng .
- Lấy một khối .
- Dán nó vào trình xem, .
Bạn có thể lấy một cái cào ở đâu khác?
dấu thời gian trong Influxdb khi viết lại
Ví dụ: chúng tôi thiết lập cảnh báo cho tổng số yêu cầu mỗi giờ (groupBy(1h)) và muốn ghi lại cảnh báo xảy ra trong Influxdb (để thể hiện rõ ràng thực tế của sự cố trên biểu đồ trong grafana).
InfluxDBOut() sẽ ghi giá trị thời gian từ cảnh báo vào dấu thời gian; theo đó, điểm trên biểu đồ sẽ được ghi sớm hơn/muộn hơn so với cảnh báo đến.
Khi cần độ chính xác: chúng tôi giải quyết vấn đề này bằng cách gọi một trình xử lý tùy chỉnh, trình xử lý này sẽ ghi dữ liệu vào Influxdb với dấu thời gian hiện tại.
docker, xây dựng và triển khai
Khi khởi động, kapacitor có thể tải các tác vụ, mẫu và trình xử lý từ thư mục được chỉ định trong cấu hình trong khối [load].
Để tạo chính xác một tác vụ, bạn cần những điều sau:
- Tên tệp - được mở rộng thành id/tên tập lệnh
- Loại – luồng/đợt
- dbrp – từ khóa để chỉ ra cơ sở dữ liệu + chính sách mà tập lệnh chạy trong đó (dbrp “nhà cung cấp.” “autogen”)
Nếu một số tác vụ hàng loạt không chứa dòng có dbrp, toàn bộ dịch vụ sẽ từ chối khởi động và sẽ ghi lại nó một cách trung thực vào nhật ký.
Ngược lại, trong chronograf, dòng này không tồn tại, nó không được chấp nhận qua giao diện và tạo ra lỗi.
Hack khi xây dựng vùng chứa: Dockerfile thoát với -1 nếu có các dòng có //.+dbrp, điều này sẽ cho phép bạn hiểu ngay lý do lỗi khi lắp ráp bản dựng.
tham gia một đến nhiều
Nhiệm vụ ví dụ: bạn cần lấy phân vị thứ 95 về thời gian hoạt động của dịch vụ trong một tuần, so sánh từng phút của 10 phút cuối với giá trị này.
Bạn không thể thực hiện nối một-nhiều, cuối/trung bình/trung vị trên một nhóm điểm sẽ biến nút thành một luồng, lỗi “không thể thêm các cạnh không khớp con: lô -> luồng” sẽ được trả về.
Kết quả của một lô, dưới dạng một biến trong biểu thức lambda, cũng không được thay thế.
Có một tùy chọn để lưu các số cần thiết từ lô đầu tiên vào một tệp qua udf và tải tệp này qua sideload.
Chúng ta đã giải quyết được gì với điều này?
Chúng tôi có khoảng 100 nhà cung cấp khách sạn, mỗi nhà cung cấp có thể có nhiều kết nối, hãy gọi đó là một kênh. Có khoảng 300 kênh như vậy, mỗi kênh có thể bị đứt. Trong tất cả các số liệu được ghi lại, chúng tôi sẽ theo dõi tỷ lệ lỗi (yêu cầu và lỗi).
Tại sao không phải là grafana?
Cảnh báo lỗi được định cấu hình trong Grafana có một số nhược điểm. Một số rất quan trọng, một số bạn có thể nhắm mắt làm ngơ, tùy theo tình huống.
Grafana không biết cách tính toán giữa các phép đo + cảnh báo, nhưng chúng tôi cần tỷ lệ (yêu cầu-lỗi)/yêu cầu.
Các lỗi trông khó chịu:

Và bớt ác hơn khi được xem với những yêu cầu thành công:

Được rồi, chúng ta có thể tính toán trước giá dịch vụ trước grafana và trong một số trường hợp, điều này sẽ hiệu quả. Nhưng không phải ở chúng ta, bởi vì... đối với mỗi kênh, tỷ lệ riêng của nó được coi là “bình thường” và các cảnh báo hoạt động theo các giá trị tĩnh (chúng tôi tìm kiếm chúng bằng mắt, thay đổi chúng nếu có cảnh báo thường xuyên).
Đây là những ví dụ về “bình thường” cho các kênh khác nhau:


Chúng ta bỏ qua điểm trước đó và cho rằng bức tranh “bình thường” đối với tất cả các nhà cung cấp đều giống nhau. Bây giờ mọi thứ đều ổn và chúng ta có thể xử lý được bằng các cảnh báo ở grafana?
Chúng ta có thể, nhưng chúng ta thực sự không muốn, vì chúng ta phải chọn một trong các phương án:
a) tạo nhiều biểu đồ cho từng kênh riêng biệt (và kèm theo chúng một cách đau đớn)
b) để lại một biểu đồ với tất cả các kênh (và bị lạc trong các dòng đầy màu sắc và cảnh báo tùy chỉnh)

Bạn đã làm nó như thế nào?
Một lần nữa, có một ví dụ khởi đầu tốt trong tài liệu (), có thể được xem qua hoặc lấy làm cơ sở trong các bài toán tương tự.
Những gì chúng tôi đã làm cuối cùng:
- tham gia hai chuỗi trong vài giờ, nhóm theo kênh;
- điền vào chuỗi theo nhóm nếu không có dữ liệu;
- so sánh giá trị trung bình của 10 phút vừa qua với dữ liệu trước đó;
- chúng ta hét lên nếu tìm thấy thứ gì đó;
- chúng tôi viết các tỷ lệ được tính toán và cảnh báo xảy ra trong Influxdb;
- gửi một thông điệp hữu ích tới Slack.
Theo ý kiến của tôi, cuối cùng chúng tôi đã cố gắng đạt được mọi thứ mình muốn (và thậm chí nhiều hơn một chút với trình xử lý tùy chỉnh) một cách đẹp đẽ nhất có thể.
Bạn có thể thấy nó trên github.com и kịch bản kết quả.
Một ví dụ về mã kết quả:
dbrp "supplier"."autogen"
var name = 'requests.rate'
var grafana_dash = 'pczpmYZWU/mydashboard'
var grafana_panel = '26'
var period = 8h
var todayPeriod = 10m
var every = 1m
var warnAlert = 15
var warnReset = 5
var reqQuery = 'SELECT sum("count") AS value FROM "supplier"."autogen"."requests"'
var errQuery = 'SELECT sum("count") AS value FROM "supplier"."autogen"."errors"'
var prevErr = batch
|query(errQuery)
.period(period)
.every(every)
.groupBy(1m, 'channel', 'supplier')
var prevReq = batch
|query(reqQuery)
.period(period)
.every(every)
.groupBy(1m, 'channel', 'supplier')
var rates = prevReq
|join(prevErr)
.as('req', 'err')
.tolerance(1m)
.fill('null')
// заполняем значения нулями, если их не было
|default()
.field('err.value', 0.0)
.field('req.value', 0.0)
// if в lambda: считаем рейт, только если ошибки были
|eval(lambda: if("err.value" > 0, 100.0 * (float("req.value") - float("err.value")) / float("req.value"), 100.0))
.as('rate')
// записываем посчитанные значения в инфлюкс
rates
|influxDBOut()
.quiet()
.create()
.database('kapacitor')
.retentionPolicy('autogen')
.measurement('rates')
// выбираем данные за последние 10 минут, считаем медиану
var todayRate = rates
|where(lambda: duration((unixNano(now()) - unixNano("time")) / 1000, 1u) < todayPeriod)
|median('rate')
.as('median')
var prevRate = rates
|median('rate')
.as('median')
var joined = todayRate
|join(prevRate)
.as('today', 'prev')
|httpOut('join')
var trigger = joined
|alert()
.warn(lambda: ("prev.median" - "today.median") > warnAlert)
.warnReset(lambda: ("prev.median" - "today.median") < warnReset)
.flapping(0.25, 0.5)
.stateChangesOnly()
// собираем в message ссылку на график дашборда графаны
.message(
'{{ .Level }}: {{ index .Tags "channel" }} err/req ratio ({{ index .Tags "supplier" }})
{{ if eq .Level "OK" }}It is ok now{{ else }}
'+string(todayPeriod)+' median is {{ index .Fields "today.median" | printf "%0.2f" }}%, by previous '+string(period)+' is {{ index .Fields "prev.median" | printf "%0.2f" }}%{{ end }}
http://grafana.ostrovok.in/d/'+string(grafana_dash)+
'?var-supplier={{ index .Tags "supplier" }}&var-channel={{ index .Tags "channel" }}&panelId='+string(grafana_panel)+'&fullscreen&tz=UTC%2B03%3A00'
)
.id('{{ index .Tags "name" }}/{{ index .Tags "channel" }}')
.levelTag('level')
.messageField('message')
.durationField('duration')
.topic('slack_graph')
// "today.median" дублируем как "value", также пишем в инфлюкс остальные филды алерта (keep)
trigger
|eval(lambda: "today.median")
.as('value')
.keep()
|influxDBOut()
.quiet()
.create()
.database('kapacitor')
.retentionPolicy('autogen')
.measurement('alerts')
.tag('alertName', name)
Và kết luận là gì?
Kapacitor rất giỏi trong việc thực hiện cảnh báo giám sát với nhiều nhóm, thực hiện các phép tính bổ sung dựa trên các số liệu đã được ghi lại, thực hiện các hành động tùy chỉnh và chạy tập lệnh (udf).
Rào cản gia nhập không cao lắm - hãy thử nếu grafana hoặc các công cụ khác không đáp ứng đầy đủ mong muốn của bạn.
Nguồn: www.habr.com
