Me tmerrësisht, sot askush nuk e diskuton më pse është e nevojshme të mbledhësh metrika të shërbimeve. Hapi tjetër logjik është të konfiguroni alertë për metrikat e mbledhura, që do t'ju njoftojnë për çdo devijim në të dhëna në kanalet tuaja të preferuara (email, Slack, Telegram). Në shërbimin e rezervimeve online të hoteleve të gjitha metrikat e shërbimeve tona derdhen në InfluxDB dhe shfaqen në Grafana, ku është vendosur gjithashtu alertimi bazë. Për detyra si "më duhet të llogaris diçka dhe ta krahasoj me këtë" ne përdorim Kapacitor.

Kapacitor është pjesë e grumbullit TICK, e cila di të përpunojë metrikat nga InfluxDB. Ai mund të lidhë disa matje mes vete (join), të llogarisë diçka të dobishme nga të dhënat e marra, ta regjistrojë rezultatin përsëri në InfluxDB, dhe të dërgojë një alert në Slack/Telgram/email.
E gjithë staku ka një dokumentacion të shkëlqyer dhe të detajuar , por gjithmonë ka gjëra të dobishme që nuk janë pararenduar në manuale. Në këtë artikull vendosa të mbledh disa këshilla të shumta që nuk janë kaq të dukshme (sintaksa kryesore e TICKscript është përshkruar ) dhe të tregoj si mund t'i aplikoni ato, duke marrë shembull zgjidhjen e një prej detyrave tona.
Fillojmë!
float & int, gabimet në llogaritje
Një problem krejtësisht standard, zgjidhet përmes kastit:
var alert_float = 5.0
var alert_int = 10
data|eval(lambda: float("value") > alert_float OR float("value") < float("alert_int"))
Përdorimi i default()
Nëse tagu/fusha nuk është plotësuar, do të ndodhin gabime në llogaritje:
|default()
.tag('status', 'empty')
.field('value', 0)
fill në join (inner vs outer)
Në mënyrë default, join do të heqë pikat ku nuk ka të dhëna (inner).
Me fill(ânullâ) do tĂ« realizohet njĂ« outer join, pas tĂ« cilit duhet tĂ« bĂ«ni default() dhe tĂ« plotĂ«soni vlerat e zbrazĂ«ta:
var data = res1
|join(res2)
.as('res1', 'res2')
.fill('null')
|default()
.field('res1.value', 0.0)
.field('res2.value', 100.0)
KĂ«tu gjithsesi ka njĂ« nuancĂ«. NĂ«se nĂ« shembullin e mĂ«sipĂ«rm njĂ«ra nga seritĂ« (res1 ose res2) Ă«shtĂ« e zbrazĂ«t, seria pĂ«rfundimtare (data) gjithashtu do tĂ« jetĂ« e zbrazĂ«t. PĂ«r kĂ«tĂ« temĂ« ekzistojnĂ« disa ticketa nĂ« GitHub (, , ) â presim pĂ«r rregullime dhe ndonjĂ« shqetĂ«sim.
Përdorimi i kushteve në llogaritje (if në lambda)
|eval(lambda: if("value" > 0, true, false)
Pesë minutat e fundit nga pipeline për periudhën
Për shembull, ju duhet të krahasoni vlerat e pesë minutave të fundit me javën e kaluar. Mund të merrni dy grupe të dhënash me dy batch të ndara ose të nxirrni një pjesë të të dhënave nga një periudhë më të madhe:
|where(lambda: duration((unixNano(now()) - unixNano("time"))/1000, 1u) < 5m)
Një alternativë për pesë minutat e fundit mund të jetë përdorimi i BarrierNode, e cila ndalon të dhënat më herët se koha e caktuar:
|barrier()
.period(5m)
Shembuj të përdorimit të template-ve Go në message
Template-t përputhen me formatin nga paketi , më poshtë disa detyra të zakonshme.
if-else
Të bëjmë rregull, pa e aktivizuar njerëzit me tekstin e panevojshëm:
|alert()
...
.message(
'{{ if eq .Level "OK" }}Tani është në rregull{{ else }}Shefi, gjithçka është e prishur{{end}}'
)
Dy numra pas presjes në mesazh
Përmirësimi i lexueshmërisë së mesazhit:
|alert()
...
.message(
'Tani vlera është {{ index .Fields "value" | printf "%0.2f" }}'
)
Rozhvillimi i variablave në mesazh
Të japim më shumë informacion në mesazh për pyetjen "Pse është në alarm?"
var warnAlert = 10
|alert()
...
.message(
'Vlera e sotme është më pak se '+string(warnAlert)+'%'
)
Identifikuesi unik i alarmin
Një gjë e nevojshme, kur në të dhëna ka më shumë se një grup, përndryshe do të gjenerohet vetëm një alarm:
|alert()
...
.id('{{ index .Tags "myname" }}/{{ index .Tags "myfield" }}')
Handler-a të personalizuara
NĂ« njĂ« listĂ« tĂ« madhe handler-a ka exec, i cili lejon ekzekutimin e skriptit tuaj me parametrat e dĂ«rguar (stdin) â krijimtaria vetĂ«m!
Një nga personalizimet tona është një skript i vogël në Python për të dërguar njoftime në Slack.
Fillimisht donim tĂ« dĂ«rgonim nĂ« mesazh njĂ« imazh nga Grafana, tĂ« mbrojtur me autorizim. MĂ« pas â tĂ« shkruanim OK nĂ« thread-in e alarmin e mĂ«parshĂ«m nga e njĂ«jta grup, jo si njĂ« mesazh tĂ« veçantĂ«. Edhe mĂ« vonĂ« â tĂ« shtonim nĂ« mesazh gabimin mĂ« tĂ« shpeshtĂ« pĂ«r minutat e fundit.
Një temë e veçantë është lidhja me shërbime të tjera dhe çdo veprim të iniciuar nga alarmi (vetëm nëse monitorimi juaj funksionon mjaft mirë).
Shembuj përshkrimi i handler-it, ku slack_handler.py është skripti jonë i shkruar vetë:
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"]
Si të debagoni?
Një opsion për të shkruajtur në log
|log()
.level("error")
.prefix("ndodhi diçka")
Shikoni (cli): kapacitor -url :9092 logs lvl=error
Një opsion me httpOut
Tregon të dhënat në pipeline aktual:
|httpOut('dicka')
Shikoni (get): :9092/kapacitor/v1/tasks/task_name/dicka
Skema e ekzekutimit
- Ădo detyrĂ« kthen njĂ« pemĂ« ekzekutimi me tĂ« dhĂ«na tĂ« dobishme nĂ« formatin .
- Merrni bllokun .
- Shtoni në shikuesin, .
Nga ku mund të merrni timestamp në influxdb gjatë regjistrimit të kundërt
timestamp në influxdb gjatë regjistrimit të kundërt
Për shembull, ne konfiguroni një alarmin për shumën e kërkesave për orë (groupBy(1h)) dhe duam të regjistrojmë alarmin e ndodhur në influxdb (për ta treguar siç duhet faktin e pranisë së problemit në grafik në grafana).
influxDBOut() do të regjistrojë në timestamp vlerën time nga alarmi, përkatësisht, pika në grafikë do të regjistrohet më herët/ më vonë se sa ka ardhur alarmi.
Kur kërkohet saktësi: e zgjidhim këtë problem përmes thirrjes së një handleri personalizues, i cili do të regjistrojë të dhënat në influxdb me timestamp-in aktual.
docker, ndërtim dhe shpërndarje
Kur fillon kapacitor, ai mund të ngarkojë detyrat, shabllonet dhe handlerët nga direktorja që është caktuar në konfigurim, në bllokun [load].
Për krijimin e saktë të një detyre ne kemi nevojë për këto gjëra:
- Emri i skedarit â shndĂ«rrohet nĂ« id/emrin e skriptit
- Tipi â stream/batch
- dbrp â fjalĂ« kyçe pĂ«r tĂ« pĂ«rcaktuar nĂ« cilĂ«n bazĂ« + politikĂ« punon skripti (dbrp "supplier"."autogen")
Nëse në ndonjë detyrë batch nuk do të ketë rreshtin me dbrp, e gjithë shërbimi do të refuzojë të fillojë dhe do ta shkruajë këtë në log.
Në chronograf, përkundrazi, ky rresht nuk duhet të jetë, ajo nuk pranohet përmes ndërfaqes dhe jep një gabim.
Haku gjatë ndërtimit të konteinerit: Dockerfile del me -1 nëse ka rreshta me //.+dbrp, që do të lejojë menjëherë të kuptoni shkakun e dështimit gjatë ndërtimit.
join një në shumë
Shembulli i detyrës: duhet të marrim percentilin e 95-të të kohës së funksionimit të shërbimit për javën, të krahasojmë çdo minutë nga 10 të fundit me këtë vlerë.
Nuk është e mundur të bësh join një në shumë, last/mean/median për grupin e pikave e shndërron noden në stream, do të kthejë gabimin "cannot add child mismatched edges: batch -> stream."
Rezultati i batch-it, si një variabël në shprehjen lambda, gjithashtu nuk vendoset.
Ka një mundësi të ruajmë numrat e nevojshëm nga batch-i i parë në skedarin përmes udf dhe ta ngarkojmë atë skedar përmes sideload.
ĂfarĂ« po zgjidhim me kĂ«tĂ«?
Kemi rreth 100 furnizues hotelesh, për secilin prej tyre mund të ketë disa lidhje, le të quajmë këtë kanal. Ka rreth 300 nga këto kanale, çdo kanal mund të dështojë. Nga të gjitha metrikat që regjistrojmë, do të monitorojmë shkallën e gabimeve (kërcimet dhe gabimet).
Pse jo grafana?
Alertet për gabimet, të konfigurura në grafana, kanë disa disavantazhe. Disa janë kritike, për disa mund të mbyllim sytë, në varësi të situatës.
Grafana nuk e di si të bëjë llogaritjet midis matjeve + alarmin, ndërsa ne kemi nevojë për shkallën (kërcimet-gabime)/kërcimet.
Gabimet duken të rënda:

Dhe më pak të rënda, nëse shikojmë me kërkesat e suksesshme:

Mirë, mund ta llogarisim paraprakisht shkallën në shërbim para grafanës, dhe në disa raste kjo do të ishte e mjaftueshme. Por jo në rastin tonë, pasi për çdo kanal llogaritet një raport "normal", ndërsa alarmin funksionon sipas vlerave statike (i kërkojmë me sy, e ndryshojmë nëse alarmon shpesh).
Këto janë shembuj "normal" për kanale të ndryshme:


Ne e injorojmë pikën e mëparshme dhe supozojmë se të gjithë furnizuesit kanë një pamje "normale" të ngjashme. Tani është gjithçka në rregull, dhe a mund të menaxhojmë me alarmin në grafana?
Mundemi, por nuk dëshirojmë aspak, sepse duhet të zgjedhim një nga opsionet:
a) të bëjmë shumë grafikë për secilin kanal veçmas (dhe t'i mirëmbajmë ato me mundim)
b) të mbajmë një grafik me të gjitha kanalet (dhe të humbim në linjat me ngjyra dhe alarmin e rregulluar)

Si vepruam?
Përsëri, në dokumentacion ka një shembull të mirë fillestar (), mund ta shihni ose ta merrni për bazë në detyra të ngjashme.
ĂfarĂ« bĂ«mĂ« nĂ« fund:
- join të dy serisë për disa orë, grupimi sipas kanaleve;
- plotesojmë seritë sipas grupeve, nëse nuk kishte të dhëna;
- krahasoni median e 10 minutave të fundit me të dhënat paraprake;
- kemi thirrur nëse kemi zbuluar diçka;
- shkruajmë shkallët e llogaritura dhe alarmin e ndodhur në influxdb;
- dërgojmë një mesazh të dobishëm në slack.
Sipaso mendimit tim, arritëm maksimalisht mirë në gjithçka që do të donim të merrnim si rezultat (dhe madje pak më shumë me handlerët e personalizuar).
Në github.com mund të shihni dhe të skriptit të marrë.
Shembulli i kodit të ardhur:
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')
// plotojme vlerat me zero, nese nuk ka pasur
|default()
.field('err.value', 0.0)
.field('req.value', 0.0)
// nese errors ishin, llogarisim rate
|eval(lambda: if("err.value" > 0, 100.0 * (float("req.value") - float("err.value")) / float("req.value"), 100.0))
.as('rate')
// regjistrojmë vlerat e llogaritura në influx
rates
|influxDBOut()
.quiet()
.create()
.database('kapacitor')
.retentionPolicy('autogen')
.measurement('rates')
// marrim të dhënat për 10 minutat e fundit, llogarisim median
var todayRate = rates
|where(lambda: duration((unixNano(now()) - unixNano("time")) / 1000, 1u) warnAlert)
.warnReset(lambda: ("prev.median" - "today.median") < warnReset)
.flapping(0.25, 0.5)
.stateChangesOnly()
// mbledhim linkun për grafik
.message(
'{{ .Level }}: {{ index .Tags "channel" }} err/req ratio ({{ index .Tags "supplier" }})
{{ if eq .Level "OK" }}Tani është në rregull{{ else }}
'+string(todayPeriod)+' median është {{ index .Fields "today.median" | printf "%0.2f" }}%, krahasuar me '+string(period)+' e kaluar është {{ 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=UTC0300'
)
.id('{{ index .Tags "name" }}{{ index .Tags "channel" }}')
.levelTag('level')
.messageField('message')
.durationField('duration')
.topic('slack_graph')
// "today.median" e mbledhim si "value", gjithashtu e regjistrojmë në influx fushat e tjera të alarmin
trigger
|eval(lambda: "today.median")
.as('value')
.keep()
|influxDBOut()
.quiet()
.create()
.database('kapacitor')
.retentionPolicy('autogen')
.measurement('alerts')
.tag('alertName', name)
Dhe çfarë dolen?
Kapacitor është fantastik për monitorimin dhe alarmin me shumë grupime, për të kryer llogaritje të tjera bazuar në metrika të regjistruara, për të ekzekutuar veprime të personalizuara dhe për të nisur skripte (udf).
PĂ«rkufizimi i hyrjes nuk Ă«shtĂ« shumĂ« i lartĂ« â provoni atĂ« nĂ«se Grafana apo mjete tĂ« tjera nuk pĂ«rmbushin plotĂ«sisht kĂ«rkesat tuaja.
Burimi: habr.com
