Πιθανότατα, σήμερα κανείς δεν έχει καμία ερώτηση σχετικά με το γιατί είναι απαραίτητη η συλλογή μετρήσεων υπηρεσιών. Το επόμενο λογικό βήμα είναι να ρυθμίσετε ειδοποιήσεις για τις μετρήσεις που συλλέγονται, οι οποίες θα σας ειδοποιούν για τυχόν αποκλίσεις στα δεδομένα μέσω καναλιών που σας βολεύουν (mail, Slack, Telegram). Στην υπηρεσία online κρατήσεων ξενοδοχείων Όλες οι μετρήσεις των υπηρεσιών μας μεταδίδονται στο InfluxDB και εμφανίζονται στο Grafana, όπου έχει επίσης ρυθμιστεί η βασική ειδοποίηση. Για εργασίες όπως "πρέπει να υπολογίσουμε κάτι και να το συγκρίνουμε με αυτό" χρησιμοποιούμε το Kapacitor.

Το Kapacitor είναι μέρος της στοίβας TICK, η οποία μπορεί να επεξεργάζεται μετρήσεις από το InfluxDB. Μπορεί να συνδέσει πολλές διαστάσεις μεταξύ τους (να ενώσει), να υπολογίσει κάτι χρήσιμο από τα δεδομένα που έλαβε, να γράψει το αποτέλεσμα πίσω στο InfluxDB, να στείλει μια ειδοποίηση στο Slack/Telegram/mail.
Ολόκληρη η στοίβα έχει μια ωραία και λεπτομερή , αλλά πάντα υπάρχουν χρήσιμα πράγματα που δεν αναφέρονται ρητά στα εγχειρίδια. Σε αυτό το άρθρο αποφάσισα να συγκεντρώσω μια σειρά από τέτοιες χρήσιμες, μη προφανείς συμβουλές (περιγράφεται η βασική σύνταξη του TICKscipt) ) και δείξτε πώς μπορούν να εφαρμοστούν χρησιμοποιώντας το παράδειγμα της επίλυσης ενός από τα προβλήματά μας.
Πάμε!
float & int, σφάλματα υπολογισμού
Ένα απολύτως τυπικό πρόβλημα, που λύνεται μέσω χύτευσης:
var alert_float = 5.0
var alert_int = 10
data|eval(lambda: float("value") > alert_float OR float("value") < float("alert_int"))
Χρησιμοποιώντας την default()
Εάν η ετικέτα/πεδίο δεν συμπληρωθεί, θα προκύψουν σφάλματα υπολογισμού:
|default()
.tag('status', 'empty')
.field('value', 0)
συμπλήρωση σύνδεσης (εσωτερική έναντι εξωτερικής)
Από προεπιλογή, η συνάρτηση join θα απορρίψει σημεία όπου δεν υπάρχουν δεδομένα (εσωτερικά).
Όταν εκτελεστεί η συνάρτηση fill('null'), θα εκτελεστεί μια εξωτερική ένωση (outer join), μετά την οποία θα πρέπει να εκτελέσετε μια συνάρτηση default() και να συμπληρώσετε τις κενές τιμές:
var data = res1
|join(res2)
.as('res1', 'res2)
.fill('null')
|default()
.field('res1.value', 0.0)
.field('res2.value', 100.0)
Υπάρχει ακόμα μια απόχρωση εδώ. Στο παραπάνω παράδειγμα, εάν μία από τις σειρές (res1 ή res2) είναι κενή, η σειρά (δεδομένα) που προκύπτει θα είναι επίσης κενή. Υπάρχουν πολλά εισιτήρια για αυτό το θέμα στο GitHub (, , ) – περιμένουμε διορθώσεις και υποφέρουμε λίγο.
Χρήση συνθηκών στους υπολογισμούς (αν είναι σε λάμδα)
|eval(lambda: if("value" > 0, true, false)
Τα τελευταία πέντε λεπτά από τον αγωγό για την περίοδο
Για παράδειγμα, πρέπει να συγκρίνετε τις τιμές των τελευταίων πέντε λεπτών με την προηγούμενη εβδομάδα. Μπορείτε να λάβετε δύο δέσμες δεδομένων σε δύο ξεχωριστές δέσμες ή να εξαγάγετε μέρος των δεδομένων από μια μεγαλύτερη περίοδο:
|where(lambda: duration((unixNano(now()) - unixNano("time"))/1000, 1u) < 5m)
Μια εναλλακτική λύση για τα τελευταία πέντε λεπτά είναι η χρήση ενός κόμβου BarrierNode, ο οποίος διακόπτει τα δεδομένα πριν από την καθορισμένη ώρα:
|barrier()
.period(5m)
Παραδείγματα χρήσης προτύπων Go σε μηνύματα
Τα πρότυπα αντιστοιχούν στη μορφή από το πακέτο , παρακάτω αναφέρονται ορισμένα προβλήματα που συναντώνται συχνά.
αν-αλλιώς
Ας βάλουμε τα πράγματα σε μια τάξη και ας μην ενεργοποιούμε τους ανθρώπους με κείμενο άσκοπα:
|alert()
...
.message(
'{{ if eq .Level "OK" }}It is ok now{{ else }}Chief, everything is broken{{end}}'
)
Δύο ψηφία μετά την υποδιαστολή στο μήνυμα
Βελτίωση της αναγνωσιμότητας του μηνύματος:
|alert()
...
.message(
'now value is {{ index .Fields "value" | printf "%0.2f" }}'
)
Αναδίπλωση μεταβλητών σε μήνυμα
Εμφανίζουμε περισσότερες πληροφορίες στο μήνυμα για να απαντήσουμε στην ερώτηση "Γιατί ουρλιάζει;"
var warnAlert = 10
|alert()
...
.message(
'Today value less then '+string(warnAlert)+'%'
)
Μοναδικό αναγνωριστικό ειδοποίησης
Κάτι απαραίτητο όταν υπάρχουν περισσότερες από μία ομάδες στα δεδομένα, διαφορετικά θα δημιουργηθεί μόνο μία ειδοποίηση:
|alert()
...
.id('{{ index .Tags "myname" }}/{{ index .Tags "myfield" }}')
Προσαρμοσμένοι χειριστές
Η μεγάλη λίστα χειριστών περιλαμβάνει το exec, το οποίο σας επιτρέπει να εκτελέσετε το σενάριό σας με τις παραμέτρους που έχουν περάσει (stdin) – δημιουργικότητα και τίποτα περισσότερο!
Ένα από τα προσαρμοσμένα μας είναι ένα μικρό σενάριο python για την αποστολή ειδοποιήσεων στο Slack.
Αρχικά θέλαμε να στείλουμε μια φωτογραφία από τη Grafana σε μήνυμα, προστατευμένη από εξουσιοδότηση. Στη συνέχεια, γράψτε OK στο νήμα της προηγούμενης ειδοποίησης από την ίδια ομάδα και όχι σε ξεχωριστό μήνυμα. Λίγο αργότερα, προσθέστε στο μήνυμα το πιο συνηθισμένο σφάλμα των τελευταίων X λεπτών.
Ένα ξεχωριστό θέμα είναι η σύνδεση με άλλες υπηρεσίες και τυχόν ενέργειες που ξεκινούν από την ειδοποίηση (μόνο εάν η παρακολούθησή σας λειτουργεί αρκετά καλά).
Ένα παράδειγμα περιγραφής χειριστή, όπου το slack_handler.py είναι το προσαρμοσμένο μας σενάριο:
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"]
Πώς να εντοπίσετε σφάλματα;
Επιλογή με έξοδο για καταγραφή
|log()
.level("error")
.prefix("something")
Παρακολούθηση (cli): πυκνωτής -url :9092 αρχεία καταγραφής επιπέδου=σφάλμα
Επιλογή με httpOut
Εμφανίζει δεδομένα στην τρέχουσα διοχέτευση:
|httpOut('something')
Παρακολουθήστε (πάρτε): :9092/kapacitor/v1/tasks/task_name/κάτι
Σχέδιο εκτέλεσης
- Κάθε εργασία επιστρέφει ένα δέντρο εκτέλεσης με χρήσιμους αριθμούς στη μορφή .
- Παίρνουμε ένα μπλοκ .
- Επικόλληση στο πρόγραμμα προβολής, .
Πού αλλού μπορείς να σε πάρουν με το τσουγκράνα;
χρονική σήμανση στο influxdb κατά την αντιγραφή
Για παράδειγμα, ορίζουμε μια ειδοποίηση για το άθροισμα των αιτημάτων ανά ώρα (groupBy(1h)) και θέλουμε να καταγράψουμε την ειδοποίηση που προέκυψε στο influxdb (για να δείξουμε με ακρίβεια το γεγονός του προβλήματος στο γράφημα στο grafana).
Η influxDBOut() θα γράψει την τιμή χρόνου από την ειδοποίηση στη χρονική σήμανση, έτσι ώστε το σημείο στο γράφημα να γραφτεί νωρίτερα/αργότερα από την άφιξη της ειδοποίησης.
Όταν απαιτείται ακρίβεια: παρακάμπτουμε αυτό το πρόβλημα καλώντας έναν προσαρμοσμένο χειριστή, ο οποίος θα γράψει δεδομένα στο influxdb με την τρέχουσα χρονική σήμανση.
docker, δημιουργία και ανάπτυξη
Κατά την εκκίνηση, το kapacitor μπορεί να φορτώσει εργασίες, πρότυπα και χειριστές από τον κατάλογο που καθορίζεται στη διαμόρφωση, στο μπλοκ [load].
Για να δημιουργήσετε σωστά μια εργασία, χρειάζεστε τα εξής:
- Όνομα αρχείου - επεκτείνεται σε όνομα id/script
- Τύπος – ροή/παρτίδα
- dbrp – λέξη-κλειδί για τον καθορισμό της βάσης δεδομένων + πολιτικής σε ποια λειτουργία λειτουργεί το σενάριο (dbrp "supplier"."autogen")
Εάν κάποια εργασία δέσμης δεν έχει γραμμή με το dbrp, ολόκληρη η υπηρεσία θα αρνηθεί να ξεκινήσει και θα γράψει ειλικρινά γι' αυτήν στο αρχείο καταγραφής.
Στο chronograf, αντίθετα, αυτή η γραμμή δεν θα έπρεπε να υπάρχει. Δεν γίνεται δεκτό μέσω της διεπαφής και επιστρέφει σφάλμα.
Hack δημιουργίας κοντέινερ: Το Dockerfile τερματίζεται με -1 εάν υπάρχουν γραμμές με //.+dbrp, κάτι που θα σας επιτρέψει να κατανοήσετε αμέσως τον λόγο της αποτυχίας δημιουργίας.
ενώνω ένα με πολλά
Παράδειγμα εργασίας: πρέπει να λάβετε υπόψη το 95ο εκατοστημόριο του χρόνου λειτουργίας της υπηρεσίας για μια εβδομάδα, συγκρίνοντας κάθε λεπτό των τελευταίων 10 με αυτήν την τιμή.
Δεν μπορείτε να κάνετε μια ένωση ένα-προς-πολλά, η συνάρτηση last/mean/median σε μια ομάδα σημείων μετατρέπει τον κόμβο σε ροή και θα επιστρέψει το σφάλμα "cannot add child mismatched edges: batch -> stream".
Το αποτέλεσμα της παρτίδας, ως μεταβλητή στην έκφραση λάμδα, επίσης δεν αντικαθίσταται.
Υπάρχει η επιλογή να αποθηκεύσετε τους απαιτούμενους αριθμούς από την πρώτη παρτίδα σε ένα αρχείο μέσω udf και να φορτώσετε αυτό το αρχείο μέσω sideload.
Τι λύναμε με αυτό;
Έχουμε περίπου 100 προμηθευτές ξενοδοχείων, καθένας από τους οποίους μπορεί να έχει αρκετές συνδέσεις, ας το ονομάσουμε κανάλι. Υπάρχουν περίπου 300 από αυτά τα κανάλια, καθένα από τα οποία μπορεί να πέσει. Από όλες τις καταγεγραμμένες μετρήσεις, θα παρακολουθούμε το ποσοστό σφάλματος (αιτήματα και σφάλματα).
Γιατί όχι και η Γραφάνα;
Οι ειδοποιήσεις σφάλματος που έχουν ρυθμιστεί στο Grafana έχουν αρκετά μειονεκτήματα. Κάποια είναι κρίσιμα, κάποια μπορούν να αγνοηθούν, ανάλογα με την περίσταση.
Το Grafana δεν μπορεί να κάνει διαδιάστατους υπολογισμούς + ειδοποιήσεις, αλλά χρειαζόμαστε έναν ρυθμό (αιτήματα-σφάλματα)/αιτήματα.
Τα σφάλματα φαίνονται δυσάρεστα:

Και λιγότερο κακό αν κοιτάξετε επιτυχημένα ερωτήματα:

Εντάξει, μπορούμε να υπολογίσουμε εκ των προτέρων την τιμή στην υπηρεσία πριν από την Grafana, και σε ορισμένες περιπτώσεις αυτό θα λειτουργήσει. Αλλά όχι στο δικό μας, επειδή για κάθε κανάλι η δική του αναλογία θεωρείται «φυσιολογική» και οι ειδοποιήσεις λειτουργούν σύμφωνα με στατικές τιμές (κοιτάμε με τα μάτια μας, το αλλάζουμε αν ειδοποιεί συχνά).
Αυτά είναι παραδείγματα "κανονικών" για διαφορετικά κανάλια:


Ας αγνοήσουμε το προηγούμενο σημείο και ας υποθέσουμε ότι η «κανονική» εικόνα είναι παρόμοια για όλους τους προμηθευτές. Τώρα όλα είναι καλά, και μπορούμε να τα βγάλουμε πέρα με ειδοποιήσεις στην Γκραφάνα;
Μπορούμε, αλλά πραγματικά δεν θέλουμε, επειδή πρέπει να επιλέξουμε μία από τις επιλογές:
α) να φτιάξετε πολλά γραφήματα για κάθε κανάλι ξεχωριστά (και να τα συντηρήσετε με κόπο)
β) αφήστε ένα γράφημα με όλα τα κανάλια (και χαθείτε σε πολύχρωμες γραμμές και προσαρμοσμένες ειδοποιήσεις)

Πώς το έκανες;
Και πάλι, υπάρχει ένα καλό παράδειγμα εκκίνησης στην τεκμηρίωση (), μπορείτε να ρίξετε μια ματιά ή να το χρησιμοποιήσετε ως βάση σε παρόμοια προβλήματα.
Τι κάναμε στο τέλος:
- ενώστε δύο επεισόδια σε λίγες ώρες, ομαδοποιώντας ανά κανάλια.
- συμπληρώνουμε τη σειρά ανά ομάδες εάν δεν υπήρχαν δεδομένα.
- συγκρίνετε τη διάμεση τιμή των τελευταίων 10 λεπτών με τα προηγούμενα δεδομένα·
- φωνάξτε αν βρούμε κάτι.
- Γράφουμε τους υπολογισμένους ρυθμούς και τις ειδοποιήσεις που εμφανίστηκαν στο influxdb.
- στείλτε ένα χρήσιμο μήνυμα στο Slack.
Κατά τη γνώμη μου, καταφέραμε να πετύχουμε όλα όσα θέλαμε στο τέλος με τον πιο όμορφο τρόπο (και μάλιστα λίγο περισσότερο με προσαρμοσμένους χειριστές).
Μπορείτε να ρίξετε μια ματιά στο github.com и το ληφθέν σενάριο.
Παράδειγμα του κώδικα που προκύπτει:
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)
Ποιο είναι λοιπόν το συμπέρασμα;
Το Kapacitor είναι εξαιρετικό στην παρακολούθηση και την ειδοποίηση με μια δέσμη ομάδων, στην εκτέλεση πρόσθετων υπολογισμών με βάση ήδη καταγεγραμμένες μετρήσεις, στην εκτέλεση προσαρμοσμένων ενεργειών και στην εκτέλεση σεναρίων (udf).
Το όριο εισόδου δεν είναι πολύ υψηλό - δοκιμάστε το αν η Grafana ή άλλα εργαλεία δεν ικανοποιούν πλήρως τις επιθυμίες σας.
Πηγή: www.habr.com
