├── .github ├── CODEOWNERS ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── workflows │ ├── lint.yml │ ├── cleanup-feature.yml │ ├── codeql-analysis.yml │ ├── swagger-validate-openapi2.yml │ ├── swagger-validate-openapi3.yml │ ├── test.yml │ ├── publish-packages.yml │ ├── swagger-delete.yml │ ├── docker-release.yml │ ├── docker-nightly.yml │ └── swagger-publish.yml └── CODE_OF_CONDUCT.md ├── .dockerignore ├── pkg ├── cli │ ├── cli.yml │ └── postinst ├── api │ ├── moira-api.service │ ├── postinst │ └── api.yml ├── filter │ ├── moira-filter.service │ ├── filter.yml │ ├── postinst │ └── storage-schemas.conf ├── checker │ ├── moira-checker.service │ ├── checker.yml │ └── postinst └── notifier │ ├── moira-notifier.service │ ├── postinst │ └── notifier.yml ├── senders ├── mattermost │ ├── doc.go │ ├── client.go │ └── sender_manual_test.go ├── telegram │ ├── emoji_provider.go │ └── handle_message.go ├── victorops │ └── api │ │ ├── client.go │ │ └── client_test.go ├── delivery │ └── check.go ├── webhook │ ├── delivery_check_request.go │ └── request.go ├── msgformat │ ├── msgformat.go │ ├── defaults_test.go │ └── defaults.go ├── discord │ ├── response.go │ └── init_test.go ├── pagerduty │ └── init.go ├── read_image_store_config.go ├── twilio │ └── voice.go ├── emoji_provider │ └── provider.go ├── selfstate │ └── selfstate.go └── opsgenie │ └── init.go ├── plotting ├── _examples │ ├── dark.expression.example.png │ ├── dark.rising.warn.example.png │ ├── light.expression.example.png │ ├── dark.falling.error.example.png │ ├── dark.falling.warn.example.png │ ├── dark.rising.error.example.png │ ├── dark.rising.stateOk.example.png │ ├── light.falling.error.example.png │ ├── light.falling.warn.example.png │ ├── light.rising.error.example.png │ ├── light.rising.warn.example.png │ ├── dark.falling.stateOk.example.png │ ├── light.falling.stateOk.example.png │ ├── light.rising.stateOk.example.png │ ├── dark.falling.warn.error.example.png │ ├── dark.rising.warn.error.example.png │ ├── light.falling.warn.error.example.png │ ├── light.rising.warn.error.example.png │ ├── dark.expression.humanized.example.png │ ├── dark.rising.warn.humanized.example.png │ ├── light.expression.humanized.example.png │ ├── dark.falling.error.humanized.example.png │ ├── dark.falling.warn.humanized.example.png │ ├── dark.rising.error.humanized.example.png │ ├── dark.rising.stateOk.humanized.example.png │ ├── light.falling.error.humanized.example.png │ ├── light.falling.warn.humanized.example.png │ ├── light.rising.error.humanized.example.png │ ├── light.rising.warn.humanized.example.png │ ├── dark.falling.stateOk.humanized.example.png │ ├── light.falling.stateOk.humanized.example.png │ ├── light.rising.stateOk.humanized.example.png │ ├── dark.falling.warn.error.humanized.example.png │ ├── dark.rising.warn.error.humanized.example.png │ ├── light.falling.warn.error.humanized.example.png │ └── light.rising.warn.error.humanized.example.png ├── theme.go └── fonts │ └── ttftogofile │ └── ttf2gofile.go ├── .gitignore ├── docs └── docs.go ├── metrics ├── dummy.go ├── index.go ├── triggers.go └── contacts.go ├── local ├── nginx.conf ├── relay.conf ├── prometheus.yml ├── cli.yml ├── filter.yml ├── checker.yml └── notifier.yml ├── filter ├── compatibility.go ├── connection │ └── handler_test.go ├── pattern_index.go ├── metrics_parser_bench_test.go └── heartbeat │ └── worker.go ├── .githooks └── pre-commit ├── .editorconfig ├── image_store └── s3 │ ├── config.go │ ├── store_test.go │ ├── store.go │ └── init.go ├── unsafe_test.go ├── notifier ├── notifications │ └── error.go ├── selfstate │ └── heartbeat │ │ ├── heartbeat.go │ │ ├── database.go │ │ ├── local_checker.go │ │ └── remote_checker.go ├── config.go └── log_common.go ├── api ├── error.go ├── constants.go ├── dto │ ├── events.go │ ├── pattern.go │ ├── notification.go │ ├── list.go │ ├── user.go │ ├── event_history_item.go │ ├── tag.go │ └── team_test.go ├── controller │ ├── paginate.go │ ├── contact_events.go │ └── notification.go ├── middleware │ ├── authorization.go │ ├── readonly_mode.go │ └── middleware_test.go ├── handler │ ├── config.go │ ├── constants.go │ └── validate.go └── authorization.go ├── index ├── bleve │ ├── delete.go │ ├── trigger_index_test.go │ ├── write.go │ └── trigger_index.go ├── mapping │ ├── mapping.go │ ├── field_data.go │ ├── field_mapping.go │ └── trigger_test.go ├── metrics.go ├── sweepper.go ├── search.go └── triggers.go ├── metric_source ├── retries │ ├── retryable_operation.go │ ├── retrier.go │ ├── config.go │ └── backoff_factory.go ├── source.go ├── remote │ ├── config.go │ ├── fetch_result.go │ ├── fetch_result_test.go │ └── response.go ├── prometheus │ ├── prometheus_api.go │ └── prometheus.go ├── local │ ├── timer.go │ ├── fetch_result.go │ └── timer_test.go └── metric_data.go ├── unsafe.go ├── AUTHORS.md ├── cmd ├── cli │ ├── errors.go │ ├── from_2.13_to_2.14.go │ ├── from_2.11_to_2.12.go │ ├── from_2.12_to_2.13.go │ ├── from_2.6_to_2.7.go │ ├── extra.go │ ├── from_2.3_to_2.4.go │ ├── config.go │ ├── metrics.go │ └── metrics_test.go ├── image_store.go └── filter │ └── compatibility.go ├── logging ├── interfaces.go └── zerolog_adapter │ └── event_builder.go ├── errors.go ├── .run ├── run moira.run.xml ├── go test moira.run.xml ├── run redis.run.xml └── generate mocks.run.xml ├── clock └── clock.go ├── database ├── redis │ ├── throttling_test.go │ ├── reply │ │ ├── subscription.go │ │ ├── contact.go │ │ ├── metric.go │ │ └── notifier.go │ ├── triggers_to_reindex.go │ ├── bot.go │ ├── config.go │ ├── throttling.go │ ├── delivery.go │ └── database_test.go ├── database.go └── stats │ ├── stats.go │ ├── stats_test.go │ ├── trigger.go │ └── contact.go ├── Dockerfile.checker ├── checker ├── config.go ├── metrics │ └── conversion │ │ └── set_helper.go └── errors.go ├── Dockerfile.cli ├── log_fields.go ├── Dockerfile.filter ├── Dockerfile.notifier ├── state_test.go ├── Dockerfile.api ├── LICENSE ├── templating └── trigger.go ├── mock ├── moira-alert │ ├── metrics │ │ └── meter.go │ └── database_stats.go └── scheduler │ └── scheduler.go └── perfomance_tests └── filter └── filter_plain_metrics_test.go /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @moira-alert/backend-developers 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .idea 3 | 4 | vendor 5 | build 6 | local 7 | 8 | **/*_test.go 9 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # PR Summary 2 | 3 | Additional information 4 | 5 | Closes/Relates #issue 6 | -------------------------------------------------------------------------------- /pkg/cli/cli.yml: -------------------------------------------------------------------------------- 1 | redis: 2 | addrs: "redis:6379" 3 | log_file: stdout 4 | log_level: info 5 | log_pretty_format: false 6 | -------------------------------------------------------------------------------- /senders/mattermost/doc.go: -------------------------------------------------------------------------------- 1 | // Package mattermost is Moira sender for Mattermost - Open Source chat. 2 | package mattermost 3 | -------------------------------------------------------------------------------- /plotting/_examples/dark.expression.example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moira-alert/moira/HEAD/plotting/_examples/dark.expression.example.png -------------------------------------------------------------------------------- /plotting/_examples/dark.rising.warn.example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moira-alert/moira/HEAD/plotting/_examples/dark.rising.warn.example.png -------------------------------------------------------------------------------- /plotting/_examples/light.expression.example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moira-alert/moira/HEAD/plotting/_examples/light.expression.example.png -------------------------------------------------------------------------------- /plotting/_examples/dark.falling.error.example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moira-alert/moira/HEAD/plotting/_examples/dark.falling.error.example.png -------------------------------------------------------------------------------- /plotting/_examples/dark.falling.warn.example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moira-alert/moira/HEAD/plotting/_examples/dark.falling.warn.example.png -------------------------------------------------------------------------------- /plotting/_examples/dark.rising.error.example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moira-alert/moira/HEAD/plotting/_examples/dark.rising.error.example.png -------------------------------------------------------------------------------- /plotting/_examples/dark.rising.stateOk.example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moira-alert/moira/HEAD/plotting/_examples/dark.rising.stateOk.example.png -------------------------------------------------------------------------------- /plotting/_examples/light.falling.error.example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moira-alert/moira/HEAD/plotting/_examples/light.falling.error.example.png -------------------------------------------------------------------------------- /plotting/_examples/light.falling.warn.example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moira-alert/moira/HEAD/plotting/_examples/light.falling.warn.example.png -------------------------------------------------------------------------------- /plotting/_examples/light.rising.error.example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moira-alert/moira/HEAD/plotting/_examples/light.rising.error.example.png -------------------------------------------------------------------------------- /plotting/_examples/light.rising.warn.example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moira-alert/moira/HEAD/plotting/_examples/light.rising.warn.example.png -------------------------------------------------------------------------------- /plotting/_examples/dark.falling.stateOk.example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moira-alert/moira/HEAD/plotting/_examples/dark.falling.stateOk.example.png -------------------------------------------------------------------------------- /plotting/_examples/light.falling.stateOk.example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moira-alert/moira/HEAD/plotting/_examples/light.falling.stateOk.example.png -------------------------------------------------------------------------------- /plotting/_examples/light.rising.stateOk.example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moira-alert/moira/HEAD/plotting/_examples/light.rising.stateOk.example.png -------------------------------------------------------------------------------- /plotting/_examples/dark.falling.warn.error.example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moira-alert/moira/HEAD/plotting/_examples/dark.falling.warn.error.example.png -------------------------------------------------------------------------------- /plotting/_examples/dark.rising.warn.error.example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moira-alert/moira/HEAD/plotting/_examples/dark.rising.warn.error.example.png -------------------------------------------------------------------------------- /plotting/_examples/light.falling.warn.error.example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moira-alert/moira/HEAD/plotting/_examples/light.falling.warn.error.example.png -------------------------------------------------------------------------------- /plotting/_examples/light.rising.warn.error.example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moira-alert/moira/HEAD/plotting/_examples/light.rising.warn.error.example.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | log 2 | build/ 3 | 4 | .DS_Store 5 | *.coverprofile 6 | vendor 7 | 8 | .vscode/ 9 | .idea/ 10 | *.exe* 11 | coverage.* 12 | 13 | /docs 14 | -------------------------------------------------------------------------------- /plotting/_examples/dark.expression.humanized.example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moira-alert/moira/HEAD/plotting/_examples/dark.expression.humanized.example.png -------------------------------------------------------------------------------- /plotting/_examples/dark.rising.warn.humanized.example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moira-alert/moira/HEAD/plotting/_examples/dark.rising.warn.humanized.example.png -------------------------------------------------------------------------------- /plotting/_examples/light.expression.humanized.example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moira-alert/moira/HEAD/plotting/_examples/light.expression.humanized.example.png -------------------------------------------------------------------------------- /docs/docs.go: -------------------------------------------------------------------------------- 1 | // Code generated by swaggo/swag. DO NOT EDIT. 2 | 3 | package docs 4 | 5 | import "github.com/swaggo/swag/v2" 6 | 7 | var SwaggerInfo = &swag.Spec{} 8 | -------------------------------------------------------------------------------- /plotting/_examples/dark.falling.error.humanized.example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moira-alert/moira/HEAD/plotting/_examples/dark.falling.error.humanized.example.png -------------------------------------------------------------------------------- /plotting/_examples/dark.falling.warn.humanized.example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moira-alert/moira/HEAD/plotting/_examples/dark.falling.warn.humanized.example.png -------------------------------------------------------------------------------- /plotting/_examples/dark.rising.error.humanized.example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moira-alert/moira/HEAD/plotting/_examples/dark.rising.error.humanized.example.png -------------------------------------------------------------------------------- /plotting/_examples/dark.rising.stateOk.humanized.example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moira-alert/moira/HEAD/plotting/_examples/dark.rising.stateOk.humanized.example.png -------------------------------------------------------------------------------- /plotting/_examples/light.falling.error.humanized.example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moira-alert/moira/HEAD/plotting/_examples/light.falling.error.humanized.example.png -------------------------------------------------------------------------------- /plotting/_examples/light.falling.warn.humanized.example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moira-alert/moira/HEAD/plotting/_examples/light.falling.warn.humanized.example.png -------------------------------------------------------------------------------- /plotting/_examples/light.rising.error.humanized.example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moira-alert/moira/HEAD/plotting/_examples/light.rising.error.humanized.example.png -------------------------------------------------------------------------------- /plotting/_examples/light.rising.warn.humanized.example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moira-alert/moira/HEAD/plotting/_examples/light.rising.warn.humanized.example.png -------------------------------------------------------------------------------- /plotting/_examples/dark.falling.stateOk.humanized.example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moira-alert/moira/HEAD/plotting/_examples/dark.falling.stateOk.humanized.example.png -------------------------------------------------------------------------------- /plotting/_examples/light.falling.stateOk.humanized.example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moira-alert/moira/HEAD/plotting/_examples/light.falling.stateOk.humanized.example.png -------------------------------------------------------------------------------- /plotting/_examples/light.rising.stateOk.humanized.example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moira-alert/moira/HEAD/plotting/_examples/light.rising.stateOk.humanized.example.png -------------------------------------------------------------------------------- /plotting/_examples/dark.falling.warn.error.humanized.example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moira-alert/moira/HEAD/plotting/_examples/dark.falling.warn.error.humanized.example.png -------------------------------------------------------------------------------- /plotting/_examples/dark.rising.warn.error.humanized.example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moira-alert/moira/HEAD/plotting/_examples/dark.rising.warn.error.humanized.example.png -------------------------------------------------------------------------------- /plotting/_examples/light.falling.warn.error.humanized.example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moira-alert/moira/HEAD/plotting/_examples/light.falling.warn.error.humanized.example.png -------------------------------------------------------------------------------- /plotting/_examples/light.rising.warn.error.humanized.example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moira-alert/moira/HEAD/plotting/_examples/light.rising.warn.error.humanized.example.png -------------------------------------------------------------------------------- /metrics/dummy.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import goMetrics "github.com/rcrowley/go-metrics" 4 | 5 | func NewDummyRegistry() Registry { 6 | return &GraphiteRegistry{goMetrics.NewRegistry()} 7 | } 8 | -------------------------------------------------------------------------------- /local/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 0.0.0.0:8080 default; 3 | 4 | location /api/ { 5 | proxy_pass http://api:8081; 6 | } 7 | 8 | location / { 9 | proxy_pass http://web:80; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /filter/compatibility.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | // Compatibility See cmd/filter/compatibility for usage examples. 4 | type Compatibility struct { 5 | AllowRegexLooseStartMatch bool 6 | AllowRegexMatchEmpty bool 7 | } 8 | -------------------------------------------------------------------------------- /.githooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Check if any of the specified files are staged for commit 4 | if git diff --name-only --cached | grep -E 'api/'; then 5 | echo "Format swaggo annotations (swag fmt)" 6 | swag fmt 7 | fi 8 | 9 | make lint -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | charset = utf-8 9 | 10 | [*.go] 11 | indent_style = tab 12 | indent_size = 2 13 | trim_trailing_whitespace = true 14 | -------------------------------------------------------------------------------- /local/relay.conf: -------------------------------------------------------------------------------- 1 | cluster local 2 | forward 3 | filter:2003 4 | graphite:2003 5 | ; 6 | 7 | statistics 8 | submit every 60 seconds 9 | reset counters after interval 10 | prefix with relay 11 | send to local 12 | stop; 13 | 14 | 15 | match * send to local; 16 | -------------------------------------------------------------------------------- /image_store/s3/config.go: -------------------------------------------------------------------------------- 1 | package s3 2 | 3 | // Config is the configuration structure for s3 image store. 4 | type Config struct { 5 | AccessKeyID string `yaml:"access_key_id"` 6 | AccessKey string `yaml:"access_key"` 7 | Region string `yaml:"region"` 8 | Bucket string `yaml:"bucket"` 9 | } 10 | -------------------------------------------------------------------------------- /unsafe_test.go: -------------------------------------------------------------------------------- 1 | package moira 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/smartystreets/goconvey/convey" 7 | ) 8 | 9 | func TestUnsafe(t *testing.T) { 10 | ShouldEqual(UnsafeBytesToString(UnsafeStringToBytes("42")), "42") 11 | ShouldEqual(UnsafeBytesToString(UnsafeStringToBytes("")), "") 12 | } 13 | -------------------------------------------------------------------------------- /notifier/notifications/error.go: -------------------------------------------------------------------------------- 1 | package notifications 2 | 3 | // notifierInBadStateError is used for ERROR state of notifier service. 4 | type notifierInBadStateError string 5 | 6 | // Error implementation with constant error message. 7 | func (err notifierInBadStateError) Error() string { 8 | return string(err) 9 | } 10 | -------------------------------------------------------------------------------- /api/error.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | // ErrInvalidRequestContent used as custom error for dto logical validations. 4 | type ErrInvalidRequestContent struct { 5 | ValidationError error 6 | } 7 | 8 | // Error is a representation of Error interface method. 9 | func (err ErrInvalidRequestContent) Error() string { 10 | return err.ValidationError.Error() 11 | } 12 | -------------------------------------------------------------------------------- /index/bleve/delete.go: -------------------------------------------------------------------------------- 1 | package bleve 2 | 3 | // Delete removes triggerIDs from TriggerIndex. 4 | func (index *TriggerIndex) Delete(triggerIDs []string) error { 5 | batch := index.index.NewBatch() 6 | defer batch.Reset() 7 | 8 | for _, triggerID := range triggerIDs { 9 | batch.Delete(triggerID) 10 | } 11 | 12 | return index.index.Batch(batch) 13 | } 14 | -------------------------------------------------------------------------------- /metric_source/retries/retryable_operation.go: -------------------------------------------------------------------------------- 1 | package retries 2 | 3 | // RetryableOperation is an action that can be retried after some time interval. 4 | // If there is an error in DoRetryableOperation that should not be retried, wrap the error with backoff.PermanentError. 5 | type RetryableOperation[T any] interface { 6 | DoRetryableOperation() (T, error) 7 | } 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 💫 Feature request 3 | about: Ask us to implement something new 4 | labels: feature 5 | --- 6 | 7 | 8 | 9 | 10 | # FEATURE 11 | 12 | ## Summary 13 | 14 | Main idea 15 | 16 | ### Details 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /pkg/api/moira-api.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Moira is alerting system based on graphite data 3 | Wants=network-online.target 4 | After=network-online.target 5 | 6 | [Service] 7 | ExecStart=/usr/bin/moira-api -config=/etc/moira/api.yml 8 | User=moira 9 | Group=moira 10 | Restart=always 11 | TimeoutStopSec=30s 12 | LimitMEMLOCK=infinity 13 | LimitNOFILE=4096 14 | 15 | [Install] 16 | WantedBy=multi-user.target 17 | -------------------------------------------------------------------------------- /pkg/filter/moira-filter.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Moira is alerting system based on graphite data 3 | Wants=network-online.target 4 | After=network-online.target 5 | 6 | [Service] 7 | ExecStart=/usr/bin/moira-filter -config=/etc/moira/filter.yml 8 | User=moira 9 | Group=moira 10 | Restart=always 11 | TimeoutStopSec=30s 12 | LimitMEMLOCK=infinity 13 | LimitNOFILE=4096 14 | 15 | [Install] 16 | WantedBy=multi-user.target 17 | -------------------------------------------------------------------------------- /pkg/checker/moira-checker.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Moira is alerting system based on graphite data 3 | Wants=network-online.target 4 | After=network-online.target 5 | 6 | [Service] 7 | ExecStart=/usr/bin/moira-checker -config=/etc/moira/checker.yml 8 | User=moira 9 | Group=moira 10 | Restart=always 11 | TimeoutStopSec=30s 12 | LimitMEMLOCK=infinity 13 | LimitNOFILE=4096 14 | 15 | [Install] 16 | WantedBy=multi-user.target 17 | -------------------------------------------------------------------------------- /pkg/notifier/moira-notifier.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Moira is alerting system based on graphite data 3 | Wants=network-online.target 4 | After=network-online.target 5 | 6 | [Service] 7 | ExecStart=/usr/bin/moira-notifier -config=/etc/moira/notifier.yml 8 | User=moira 9 | Group=moira 10 | Restart=always 11 | TimeoutStopSec=30s 12 | LimitMEMLOCK=infinity 13 | LimitNOFILE=4096 14 | 15 | [Install] 16 | WantedBy=multi-user.target 17 | -------------------------------------------------------------------------------- /unsafe.go: -------------------------------------------------------------------------------- 1 | package moira 2 | 3 | import ( 4 | "unsafe" 5 | ) 6 | 7 | // UnsafeBytesToString converts source to string without copying. 8 | func UnsafeBytesToString(b []byte) string { 9 | return unsafe.String(unsafe.SliceData(b), len(b)) 10 | } 11 | 12 | // UnsafeStringToBytes converts string to source without copying. 13 | func UnsafeStringToBytes(s string) []byte { 14 | return unsafe.Slice(unsafe.StringData(s), len(s)) 15 | } 16 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | # Maintainer 2 | Moira was originally developed and is supported by SKB Kontur (https://kontur.ru/eng/about). 3 | 4 | # Original authors 5 | - Alexandr Akulov (akulov@skbkontur.ru) 6 | - Alexey Kirpichnikov (alexkir@skbkontur.ru) 7 | - Alexey Larkov (larkov@skbkontur.ru) 8 | 9 | # Contributors 10 | - Ruslan Usifov (ruslan@playrix.com) 11 | - Vladimir Kolobaev (kolobaev.v.l@gmail.com) 12 | - Borovsky Arkady (borovskyav@list.ru) 13 | -------------------------------------------------------------------------------- /pkg/cli/postinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | # Initial installation: $1 == 1 6 | # Upgrade: $1 == 2, and configured to restart on upgrade 7 | #if [ $1 -eq 1 ] ; then 8 | if ! getent group "moira" > /dev/null 2>&1 ; then 9 | groupadd -r "moira" 10 | fi 11 | if ! getent passwd "moira" > /dev/null 2>&1 ; then 12 | useradd -r -g moira -d /usr/share/moira -s /sbin/nologin \ 13 | -c "Moira user" moira 14 | fi 15 | #fi 16 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | on: 3 | pull_request: 4 | 5 | jobs: 6 | golangci: 7 | name: lint 8 | runs-on: ubuntu-22.04 9 | steps: 10 | - uses: actions/checkout@v4 11 | 12 | - uses: actions/setup-go@v4 13 | with: 14 | go-version-file: go.mod 15 | 16 | - name: Run linter 17 | uses: golangci/golangci-lint-action@v7 18 | with: 19 | version: v2.7.2 20 | 21 | -------------------------------------------------------------------------------- /api/constants.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | // SortOrder represents the sorting order for entities. 4 | type SortOrder string 5 | 6 | const ( 7 | // NoSortOrder means that entities may be unsorted. 8 | NoSortOrder SortOrder = "" 9 | // AscSortOrder means that entities should be ordered ascending (example: from 1 to 9). 10 | AscSortOrder SortOrder = "asc" 11 | // DescSortOrder means that entities should be ordered descending (example: from 9 to 1). 12 | DescSortOrder SortOrder = "desc" 13 | ) 14 | -------------------------------------------------------------------------------- /senders/mattermost/client.go: -------------------------------------------------------------------------------- 1 | package mattermost 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/mattermost/mattermost/server/public/model" 7 | ) 8 | 9 | // Client is abstraction over model.Client4. 10 | type Client interface { 11 | SetToken(token string) 12 | CreatePost(ctx context.Context, post *model.Post) (*model.Post, *model.Response, error) 13 | UploadFile(ctx context.Context, data []byte, channelId string, filename string) (*model.FileUploadResponse, *model.Response, error) 14 | } 15 | -------------------------------------------------------------------------------- /cmd/cli/errors.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | 7 | "github.com/moira-alert/moira" 8 | ) 9 | 10 | type unknownDBError struct { 11 | database reflect.Type 12 | } 13 | 14 | func makeUnknownDBError(database moira.Database) unknownDBError { 15 | return unknownDBError{ 16 | database: reflect.TypeOf(database), 17 | } 18 | } 19 | 20 | func (err unknownDBError) Error() string { 21 | return fmt.Sprintf("Unknown implementation of moira.Database: %s", err.database.Name()) 22 | } 23 | -------------------------------------------------------------------------------- /local/prometheus.yml: -------------------------------------------------------------------------------- 1 | scrape_configs: 2 | - job_name: 'prometheus' 3 | static_configs: 4 | - targets: ['localhost:9090'] 5 | - job_name: 'moira-api' 6 | static_configs: 7 | - targets: ['api:8091'] 8 | - job_name: 'moira-checker' 9 | static_configs: 10 | - targets: ['checker:8092'] 11 | - job_name: 'moira-notifier' 12 | static_configs: 13 | - targets: ['notifier:8093'] 14 | - job_name: 'moira-filter' 15 | static_configs: 16 | - targets: ['filter:8094'] 17 | -------------------------------------------------------------------------------- /logging/interfaces.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | // EventBuilder allows to build log events with custom tags. 4 | type EventBuilder interface { 5 | String(key, value string) EventBuilder 6 | Error(err error) EventBuilder 7 | Int(key string, value int) EventBuilder 8 | Int64(key string, value int64) EventBuilder 9 | Interface(key string, value interface{}) EventBuilder 10 | Fields(fields map[string]interface{}) EventBuilder 11 | 12 | // Msg must be called after all tags were set 13 | Msg(message string) 14 | } 15 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package moira 2 | 3 | // SenderBrokenContactError means than sender has no way to send message to contact. 4 | // Maybe receive contact was deleted, blocked or archived. 5 | type SenderBrokenContactError struct { 6 | SenderError error 7 | } 8 | 9 | func NewSenderBrokenContactError(senderError error) SenderBrokenContactError { 10 | return SenderBrokenContactError{ 11 | SenderError: senderError, 12 | } 13 | } 14 | 15 | func (e SenderBrokenContactError) Error() string { 16 | return e.SenderError.Error() 17 | } 18 | -------------------------------------------------------------------------------- /pkg/filter/filter.yml: -------------------------------------------------------------------------------- 1 | #See https://moira.readthedocs.io/en/latest/installation/configuration.html for config explanation 2 | redis: 3 | addrs: "redis:6379" 4 | graphite: 5 | enabled: false 6 | runtime_stats: false 7 | uri: "localhost:2003" 8 | prefix: DevOps.Moira 9 | interval: 60s 10 | filter: 11 | listen: ":2003" 12 | retention_config: /etc/moira/storage-schemas.conf 13 | cache_capacity: 10 14 | max_parallel_matches: 0 15 | log: 16 | log_file: stdout 17 | log_level: info 18 | log_pretty_format: false 19 | -------------------------------------------------------------------------------- /.run/run moira.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /metric_source/source.go: -------------------------------------------------------------------------------- 1 | package metricsource 2 | 3 | // MetricSource implements graphite metrics source abstraction. 4 | type MetricSource interface { 5 | Fetch(target string, from int64, until int64, allowRealTimeAlerting bool) (FetchResult, error) 6 | GetMetricsTTLSeconds() int64 7 | IsAvailable() (bool, error) 8 | } 9 | 10 | // FetchResult implements moira metric sources fetching result format. 11 | type FetchResult interface { 12 | GetMetricsData() []MetricData 13 | GetPatterns() ([]string, error) 14 | GetPatternMetrics() ([]string, error) 15 | } 16 | -------------------------------------------------------------------------------- /clock/clock.go: -------------------------------------------------------------------------------- 1 | package clock 2 | 3 | import "time" 4 | 5 | // SystemClock is struct clock-component. 6 | type SystemClock struct{} 7 | 8 | // NewSystemClock is construct for clock-component. 9 | func NewSystemClock() *SystemClock { 10 | return &SystemClock{} 11 | } 12 | 13 | // NowUTC returns now time.Time with UTC location. 14 | func (t *SystemClock) NowUTC() time.Time { 15 | return time.Now().UTC() 16 | } 17 | 18 | // NowUnix returns current time in a Unix time format. 19 | func (t *SystemClock) NowUnix() int64 { 20 | return time.Now().Unix() 21 | } 22 | -------------------------------------------------------------------------------- /local/cli.yml: -------------------------------------------------------------------------------- 1 | redis: 2 | addrs: "redis:6379" 3 | metrics_ttl: 3h 4 | log_file: stdout 5 | log_level: debug 6 | log_pretty_format: true 7 | cleanup: 8 | # Default cleanup duration according to max TTL for metrics = 7 days 9 | cleanup_metrics_duration: "-168h" 10 | # Specifies the time from which metrics written to the future will be deleted 11 | # Defaults to 1 hour 12 | cleanup_future_metrics_duration: "60m" 13 | # Default notification cleanup ttl (according to max ttl of notification history = 48h) 14 | cleanup_notification_history_duration: "48h" 15 | -------------------------------------------------------------------------------- /senders/telegram/emoji_provider.go: -------------------------------------------------------------------------------- 1 | package telegram 2 | 3 | import "github.com/moira-alert/moira" 4 | 5 | var emojiStates = map[moira.State]string{ 6 | moira.StateOK: "\xe2\x9c\x85", 7 | moira.StateWARN: "\xe2\x9a\xa0", 8 | moira.StateERROR: "\xe2\xad\x95", 9 | moira.StateNODATA: "\xf0\x9f\x92\xa3", 10 | moira.StateTEST: "\xf0\x9f\x98\x8a", 11 | } 12 | 13 | type telegramEmojiProvider struct{} 14 | 15 | // GetStateEmoji returns emoji suitable for moira.State. 16 | func (telegramEmojiProvider) GetStateEmoji(subjectState moira.State) string { 17 | return emojiStates[subjectState] 18 | } 19 | -------------------------------------------------------------------------------- /senders/victorops/api/client.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import "net/http" 4 | 5 | // Client for the victorops API. 6 | type Client struct { 7 | httpClient *http.Client 8 | routingURL string 9 | } 10 | 11 | // NewClient returns a new victorops API client for the given routing URL 12 | // and http client (Uses http.DefaultClient if httpClient is nil). 13 | func NewClient(routingURL string, httpClient *http.Client) *Client { 14 | if httpClient == nil { 15 | httpClient = http.DefaultClient 16 | } 17 | 18 | return &Client{ 19 | httpClient: httpClient, 20 | routingURL: routingURL, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.run/go test moira.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /local/filter.yml: -------------------------------------------------------------------------------- 1 | #See https://moira.readthedocs.io/en/latest/installation/configuration.html for config explanation 2 | redis: 3 | addrs: "redis:6379" 4 | metrics_ttl: 3h 5 | telemetry: 6 | graphite: 7 | enabled: true 8 | runtime_stats: true 9 | uri: "relay:2003" 10 | prefix: moira 11 | interval: 60s 12 | pprof: 13 | enabled: true 14 | listen: ":8094" 15 | filter: 16 | listen: ":2003" 17 | retention_config: /etc/moira/storage-schemas.conf 18 | cache_capacity: 10 19 | max_parallel_matches: 0 20 | log: 21 | log_file: stdout 22 | log_level: debug 23 | log_pretty_format: true 24 | -------------------------------------------------------------------------------- /.github/workflows/cleanup-feature.yml: -------------------------------------------------------------------------------- 1 | name: Delete skipped docker-feature runs 2 | on: 3 | schedule: 4 | - cron: '0 0 * * 1' 5 | 6 | jobs: 7 | del_runs: 8 | runs-on: ubuntu-22.04 9 | permissions: 10 | actions: write 11 | steps: 12 | - name: Delete workflow runs 13 | uses: Mattraks/delete-workflow-runs@v2 14 | with: 15 | token: ${{ github.token }} 16 | repository: ${{ github.repository }} 17 | retain_days: 0 18 | keep_minimum_runs: 0 19 | delete_workflow_pattern: "docker-feature.yml" 20 | delete_run_by_conclusion_pattern: "skipped" -------------------------------------------------------------------------------- /api/dto/events.go: -------------------------------------------------------------------------------- 1 | // nolint 2 | package dto 3 | 4 | import ( 5 | "net/http" 6 | 7 | "github.com/moira-alert/moira" 8 | ) 9 | 10 | type EventsList struct { 11 | Page int64 `json:"page" example:"0" format:"int64" binding:"required"` 12 | Size int64 `json:"size" example:"100" format:"int64" binding:"required"` 13 | Total int64 `json:"total" example:"10" format:"int64" binding:"required"` 14 | List []moira.NotificationEvent `json:"list" binding:"required"` 15 | } 16 | 17 | func (*EventsList) Render(w http.ResponseWriter, r *http.Request) error { 18 | return nil 19 | } 20 | -------------------------------------------------------------------------------- /.run/run redis.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /metric_source/remote/config.go: -------------------------------------------------------------------------------- 1 | package remote 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/moira-alert/moira/metric_source/retries" 7 | ) 8 | 9 | // Config represents config from remote storage. 10 | type Config struct { 11 | URL string `validate:"required,url"` 12 | CheckInterval time.Duration 13 | MetricsTTL time.Duration 14 | Timeout time.Duration `validate:"required,gt=0s"` 15 | User string 16 | Password string 17 | HealthcheckTimeout time.Duration `validate:"required,gt=0s"` 18 | Retries retries.Config 19 | HealthcheckRetries retries.Config 20 | } 21 | -------------------------------------------------------------------------------- /senders/delivery/check.go: -------------------------------------------------------------------------------- 1 | package delivery 2 | 3 | import "github.com/moira-alert/moira" 4 | 5 | // LogFieldPrefix is the recommended prefix for log fields, written to log, when performing delivery checks. 6 | const LogFieldPrefix = "delivery.check." 7 | 8 | // NotificationDeliveryChecker represents action that is performed to check the delivery of notifications. 9 | type NotificationDeliveryChecker interface { 10 | // CheckNotificationsDelivery should check notifications delivery state and return 11 | // data to schedule again. 12 | CheckNotificationsDelivery(fetchedDeliveryChecks []string) ([]string, moira.DeliveryTypesCounter) 13 | } 14 | -------------------------------------------------------------------------------- /pkg/api/postinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | # Initial installation: $1 == 1 6 | # Upgrade: $1 == 2, and configured to restart on upgrade 7 | #if [ $1 -eq 1 ] ; then 8 | if ! getent group "moira" > /dev/null 2>&1 ; then 9 | groupadd -r "moira" 10 | fi 11 | if ! getent passwd "moira" > /dev/null 2>&1 ; then 12 | useradd -r -g moira -d /usr/share/moira -s /sbin/nologin \ 13 | -c "Moira user" moira 14 | fi 15 | 16 | if [ -x /bin/systemctl ] ; then 17 | /bin/systemctl daemon-reload 18 | /bin/systemctl enable moira-api.service 19 | elif [ -x /sbin/chkconfig ] ; then 20 | /sbin/chkconfig --add moira-api 21 | fi 22 | #fi 23 | -------------------------------------------------------------------------------- /pkg/checker/checker.yml: -------------------------------------------------------------------------------- 1 | #See https://moira.readthedocs.io/en/latest/installation/configuration.html for config explanation 2 | redis: 3 | addrs: "redis:6379" 4 | graphite: 5 | enabled: false 6 | runtime_stats: false 7 | uri: "localhost:2003" 8 | prefix: DevOps.Moira 9 | interval: 60s 10 | remote: 11 | enabled: false 12 | check_interval: 60s 13 | timeout: 60s 14 | checker: 15 | nodata_check_interval: 60s 16 | check_interval: 10s 17 | metrics_ttl: 3h 18 | stop_checking_interval: 30s 19 | metric_event_pop_delay: 0s 20 | metric_event_pop_batch_size: 100 21 | log: 22 | log_file: stdout 23 | log_level: info 24 | log_pretty_format: false 25 | -------------------------------------------------------------------------------- /api/dto/pattern.go: -------------------------------------------------------------------------------- 1 | // nolint 2 | package dto 3 | 4 | import ( 5 | "net/http" 6 | ) 7 | 8 | type PatternList struct { 9 | List []PatternData `json:"list" binding:"required"` 10 | } 11 | 12 | func (*PatternList) Render(w http.ResponseWriter, r *http.Request) error { 13 | return nil 14 | } 15 | 16 | type PatternData struct { 17 | Metrics []string `json:"metrics" binding:"required" example:"DevOps.my_server.hdd.freespace_mbytes, DevOps.my_server.hdd.freespace_mbytes, DevOps.my_server.db.*"` 18 | Pattern string `json:"pattern" binding:"required" example:"Devops.my_server.*"` 19 | Triggers []TriggerModel `json:"triggers" binding:"required"` 20 | } 21 | -------------------------------------------------------------------------------- /pkg/filter/postinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | # Initial installation: $1 == 1 6 | # Upgrade: $1 == 2, and configured to restart on upgrade 7 | #if [ $1 -eq 1 ] ; then 8 | if ! getent group "moira" > /dev/null 2>&1 ; then 9 | groupadd -r "moira" 10 | fi 11 | if ! getent passwd "moira" > /dev/null 2>&1 ; then 12 | useradd -r -g moira -d /usr/share/moira -s /sbin/nologin \ 13 | -c "Moira user" moira 14 | fi 15 | 16 | if [ -x /bin/systemctl ] ; then 17 | /bin/systemctl daemon-reload 18 | /bin/systemctl enable moira-filter.service 19 | elif [ -x /sbin/chkconfig ] ; then 20 | /sbin/chkconfig --add moira-filter 21 | fi 22 | #fi 23 | -------------------------------------------------------------------------------- /pkg/checker/postinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | # Initial installation: $1 == 1 6 | # Upgrade: $1 == 2, and configured to restart on upgrade 7 | #if [ $1 -eq 1 ] ; then 8 | if ! getent group "moira" > /dev/null 2>&1 ; then 9 | groupadd -r "moira" 10 | fi 11 | if ! getent passwd "moira" > /dev/null 2>&1 ; then 12 | useradd -r -g moira -d /usr/share/moira -s /sbin/nologin \ 13 | -c "Moira user" moira 14 | fi 15 | 16 | if [ -x /bin/systemctl ] ; then 17 | /bin/systemctl daemon-reload 18 | /bin/systemctl enable moira-checker.service 19 | elif [ -x /sbin/chkconfig ] ; then 20 | /sbin/chkconfig --add moira-checker 21 | fi 22 | #fi 23 | -------------------------------------------------------------------------------- /pkg/notifier/postinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | # Initial installation: $1 == 1 6 | # Upgrade: $1 == 2, and configured to restart on upgrade 7 | #if [ $1 -eq 1 ] ; then 8 | if ! getent group "moira" > /dev/null 2>&1 ; then 9 | groupadd -r "moira" 10 | fi 11 | if ! getent passwd "moira" > /dev/null 2>&1 ; then 12 | useradd -r -g moira -d /usr/share/moira -s /sbin/nologin \ 13 | -c "Moira user" moira 14 | fi 15 | 16 | if [ -x /bin/systemctl ] ; then 17 | /bin/systemctl daemon-reload 18 | /bin/systemctl enable moira-notifier.service 19 | elif [ -x /sbin/chkconfig ] ; then 20 | /sbin/chkconfig --add moira-notifier 21 | fi 22 | #fi 23 | -------------------------------------------------------------------------------- /api/controller/paginate.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | // applyPagination returns entities[page*size:(page+1)*size] if possible. 4 | // If bad page and size are given or out of range than empty slice []T is returned. 5 | func applyPagination[T any](page, size, total int64, entities []T) []T { 6 | if page < 0 || (page > 0 && size < 0) { 7 | return make([]T, 0) 8 | } 9 | 10 | if page >= 0 && size >= 0 { 11 | start := page * size 12 | end := start + size 13 | 14 | if start >= total { 15 | return make([]T, 0) 16 | } else { 17 | if end > total { 18 | end = total 19 | } 20 | 21 | return entities[start:end] 22 | } 23 | } 24 | 25 | return entities 26 | } 27 | -------------------------------------------------------------------------------- /filter/connection/handler_test.go: -------------------------------------------------------------------------------- 1 | package connection 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/smartystreets/goconvey/convey" 7 | ) 8 | 9 | func TestDropCRLF(t *testing.T) { 10 | type TestCase struct { 11 | input []byte 12 | output []byte 13 | } 14 | 15 | Convey("Should drop CRLF", t, func() { 16 | testCases := []TestCase{ 17 | {[]byte{}, []byte{}}, 18 | {[]byte{'a'}, []byte{'a'}}, 19 | {[]byte{'\n'}, []byte{}}, 20 | {[]byte{'\r'}, []byte{}}, 21 | {[]byte{'\r', '\n'}, []byte{}}, 22 | } 23 | 24 | for _, testCase := range testCases { 25 | output := dropCRLF(testCase.input) 26 | So(testCase.output, ShouldResemble, output) 27 | } 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /index/bleve/trigger_index_test.go: -------------------------------------------------------------------------------- 1 | package bleve 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/moira-alert/moira/index/mapping" 7 | . "github.com/smartystreets/goconvey/convey" 8 | ) 9 | 10 | func TestTriggerIndex_CreateAndGetCount(t *testing.T) { 11 | var newIndex *TriggerIndex 12 | 13 | var err error 14 | 15 | triggerMapping := mapping.BuildIndexMapping(mapping.Trigger{}) 16 | 17 | Convey("Test create index", t, func() { 18 | newIndex, err = CreateTriggerIndex(triggerMapping) 19 | So(newIndex, ShouldHaveSameTypeAs, &TriggerIndex{}) 20 | So(err, ShouldBeNil) 21 | 22 | count, err := newIndex.GetCount() 23 | So(count, ShouldBeZeroValue) 24 | So(err, ShouldBeNil) 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /notifier/selfstate/heartbeat/heartbeat.go: -------------------------------------------------------------------------------- 1 | package heartbeat 2 | 3 | import ( 4 | "github.com/moira-alert/moira" 5 | ) 6 | 7 | // Heartbeater is the interface for simplified events verification. 8 | type Heartbeater interface { 9 | Check(int64) (int64, bool, error) 10 | NeedTurnOffNotifier() bool 11 | NeedToCheckOthers() bool 12 | GetErrorMessage() string 13 | GetCheckTags() CheckTags 14 | } 15 | 16 | // CheckTags represents a tag collection. 17 | type CheckTags []string 18 | 19 | // heartbeat basic structure for Heartbeater. 20 | type heartbeat struct { 21 | logger moira.Logger 22 | database moira.Database 23 | 24 | checkTags []string 25 | delay, lastSuccessfulCheck int64 26 | } 27 | -------------------------------------------------------------------------------- /senders/victorops/api/client_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | . "github.com/smartystreets/goconvey/convey" 8 | ) 9 | 10 | func TestNewClient(t *testing.T) { 11 | Convey("NewClient tests", t, func() { 12 | Convey("Nil http client", func() { 13 | client := NewClient("https://testurl.com", nil) 14 | So(client, ShouldResemble, &Client{httpClient: http.DefaultClient, routingURL: "https://testurl.com"}) 15 | }) 16 | 17 | Convey("Custom http client", func() { 18 | client := NewClient("https://testurl.com", http.DefaultClient) 19 | So(client, ShouldResemble, &Client{httpClient: http.DefaultClient, routingURL: "https://testurl.com"}) 20 | }) 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /api/dto/notification.go: -------------------------------------------------------------------------------- 1 | // nolint 2 | package dto 3 | 4 | import ( 5 | "net/http" 6 | 7 | "github.com/moira-alert/moira" 8 | ) 9 | 10 | type NotificationsList struct { 11 | Total int64 `json:"total" example:"0" format:"int64" binding:"required"` 12 | List []*moira.ScheduledNotification `json:"list" binding:"required"` 13 | } 14 | 15 | func (*NotificationsList) Render(w http.ResponseWriter, r *http.Request) error { 16 | return nil 17 | } 18 | 19 | type NotificationDeleteResponse struct { 20 | Result int64 `json:"result" example:"0" format:"int64" binding:"required"` 21 | } 22 | 23 | func (*NotificationDeleteResponse) Render(w http.ResponseWriter, r *http.Request) error { 24 | return nil 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | analyze: 11 | name: Analyze 12 | runs-on: ubuntu-22.04 13 | permissions: 14 | actions: read 15 | contents: read 16 | security-events: write 17 | 18 | strategy: 19 | fail-fast: false 20 | 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@v4 24 | 25 | - name: Initialize CodeQL 26 | uses: github/codeql-action/init@v3 27 | with: 28 | languages: go 29 | 30 | - run: make build 31 | 32 | - name: Perform CodeQL Analysis 33 | uses: github/codeql-action/analyze@v3 34 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Community Code of Conduct 2 | 3 | This document provides community guidelines for a safe, respectful, productive, and collaborative place for any person who is willing to contribute to the Moira community. It applies to all “collaborative space”, which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.). 4 | 5 | * Participants will be tolerant of opposing views. 6 | * Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks. 7 | * When interpreting the words and actions of others, participants should always assume good intentions. 8 | * Behaviour which can be reasonably considered harassment will not be tolerated. 9 | -------------------------------------------------------------------------------- /api/dto/list.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/go-chi/render" 7 | ) 8 | 9 | // ListDTO is a generic struct to create list types dto. 10 | type ListDTO[T render.Renderer] struct { 11 | // List of entities. 12 | List []T `json:"list" binding:"required"` 13 | // Page number. 14 | Page int64 `json:"page" example:"0" format:"int64" binding:"required"` 15 | // Size is the amount of entities per Page. 16 | Size int64 `json:"size" example:"100" format:"int64" binding:"required"` 17 | // Total amount of entities in the database. 18 | Total int64 `json:"total" example:"10" format:"int64" binding:"required"` 19 | } 20 | 21 | func (*ListDTO[T]) Render(w http.ResponseWriter, r *http.Request) error { 22 | return nil 23 | } 24 | -------------------------------------------------------------------------------- /cmd/cli/from_2.13_to_2.14.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/moira-alert/moira" 4 | 5 | func updateFrom213(logger moira.Logger, database moira.Database) error { 6 | logger.Info().Msg("Update 2.13 -> 2.14 started") 7 | 8 | err := fillTeamNamesHash(logger, database) 9 | if err != nil { 10 | return err 11 | } 12 | 13 | logger.Info().Msg("Update 2.13 -> 2.14 was finished") 14 | 15 | return nil 16 | } 17 | 18 | func downgradeTo213(logger moira.Logger, database moira.Database) error { 19 | logger.Info().Msg("Downgrade 2.14 -> 2.13 started") 20 | 21 | err := removeTeamNamesHash(logger, database) 22 | if err != nil { 23 | return err 24 | } 25 | 26 | logger.Info().Msg("Downgrade 2.14 -> 2.13 was finished") 27 | 28 | return nil 29 | } 30 | -------------------------------------------------------------------------------- /api/middleware/authorization.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/go-chi/render" 7 | "github.com/moira-alert/moira/api" 8 | ) 9 | 10 | // AdminOnlyMiddleware returns 403 if request for made by non-admin user. 11 | func AdminOnlyMiddleware() func(next http.Handler) http.Handler { 12 | return func(next http.Handler) http.Handler { 13 | fn := func(w http.ResponseWriter, r *http.Request) { 14 | auth := GetAuth(r) 15 | userLogin := GetLogin(r) 16 | 17 | if auth.IsEnabled() && !auth.IsAdmin(userLogin) { 18 | render.Render(w, r, api.ErrorForbidden("Only administrators can use this")) //nolint:errcheck 19 | return 20 | } 21 | 22 | next.ServeHTTP(w, r) 23 | } 24 | 25 | return http.HandlerFunc(fn) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/swagger-validate-openapi2.yml: -------------------------------------------------------------------------------- 1 | name: Validate OpenAPI-v2 on PR 2 | 3 | on: 4 | - pull_request 5 | 6 | jobs: 7 | mergespec: 8 | name: Validate spec file (OpenAPI-v2) 9 | runs-on: ubuntu-22.04 10 | defaults: 11 | run: 12 | working-directory: . 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - uses: actions/setup-go@v4 18 | with: 19 | go-version-file: go.mod 20 | cache-dependency-path: go.sum 21 | - run: make install-swag-v2 22 | 23 | - uses: actions/setup-node@v4 24 | with: 25 | node-version: '20.17.0' 26 | - run: npm install --location=global @openapitools/openapi-generator-cli 27 | - run: make spec-v2 28 | - run: make validate-spec-v2 29 | -------------------------------------------------------------------------------- /.github/workflows/swagger-validate-openapi3.yml: -------------------------------------------------------------------------------- 1 | name: Validate OpenAPI-v3 on PR 2 | 3 | on: 4 | - pull_request 5 | 6 | jobs: 7 | mergespec: 8 | name: Validate spec file (OpenAPI-v3) 9 | runs-on: ubuntu-22.04 10 | defaults: 11 | run: 12 | working-directory: . 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - uses: actions/setup-go@v4 18 | with: 19 | go-version-file: go.mod 20 | cache-dependency-path: go.sum 21 | - run: make install-swag-v3 22 | 23 | - uses: actions/setup-node@v4 24 | with: 25 | node-version: '20.17.0' 26 | - run: npm install --location=global @openapitools/openapi-generator-cli 27 | - run: make spec-v3 28 | - run: make validate-spec-v3 29 | -------------------------------------------------------------------------------- /cmd/cli/from_2.11_to_2.12.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/moira-alert/moira" 5 | ) 6 | 7 | func updateFrom211(logger moira.Logger, database moira.Database) error { 8 | logger.Info().Msg("Update 2.11 -> 2.12 was started") 9 | 10 | if err := updateTelegramUsersRecords(logger, database); err != nil { 11 | return err 12 | } 13 | 14 | logger.Info().Msg("Update 2.11 -> 2.12 was finished") 15 | 16 | return nil 17 | } 18 | 19 | func downgradeTo211(logger moira.Logger, database moira.Database) error { 20 | logger.Info().Msg("Downgrade 2.12 -> 2.11 started") 21 | 22 | if err := downgradeTelegramUsersRecords(logger, database); err != nil { 23 | return err 24 | } 25 | 26 | logger.Info().Msg("Downgrade 2.12 -> 2.11 was finished") 27 | 28 | return nil 29 | } 30 | -------------------------------------------------------------------------------- /api/dto/user.go: -------------------------------------------------------------------------------- 1 | // nolint 2 | package dto 3 | 4 | import ( 5 | "net/http" 6 | "github.com/moira-alert/moira/api" 7 | ) 8 | 9 | type UserSettings struct { 10 | User 11 | Contacts []ContactWithScore `json:"contacts" binding:"required"` 12 | Subscriptions []Subscription `json:"subscriptions" binding:"required"` 13 | } 14 | 15 | func (*UserSettings) Render(w http.ResponseWriter, r *http.Request) error { 16 | return nil 17 | } 18 | 19 | type User struct { 20 | Login string `json:"login" binding:"required" example:"john"` 21 | Role api.Role `json:"role,omitempty" example:"user"` 22 | AuthEnabled bool `json:"auth_enabled,omitempty" example:"true"` 23 | } 24 | 25 | func (*User) Render(w http.ResponseWriter, r *http.Request) error { 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /api/handler/config.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/go-chi/render" 7 | "github.com/moira-alert/moira/api" 8 | ) 9 | 10 | // nolint: gofmt,goimports 11 | // 12 | // @summary Get web configuration 13 | // @id get-web-config 14 | // @tags config 15 | // @produce json 16 | // @success 200 {object} api.WebConfig "Configuration fetched successfully" 17 | // @failure 422 {object} api.ErrorResponse "Render error" 18 | // @router /config [get] 19 | func getWebConfig(webConfig *api.WebConfig) http.HandlerFunc { 20 | return func(writer http.ResponseWriter, request *http.Request) { 21 | if err := render.Render(writer, request, webConfig); err != nil { 22 | render.Render(writer, request, api.ErrorRender(err)) //nolint 23 | return 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /index/mapping/mapping.go: -------------------------------------------------------------------------------- 1 | package mapping 2 | 3 | import ( 4 | "github.com/blevesearch/bleve/v2" 5 | "github.com/blevesearch/bleve/v2/mapping" 6 | ) 7 | 8 | // DocumentMapping implements mapping.DocumentMapping functionality. 9 | type DocumentMapping interface { 10 | GetDocumentMapping() *mapping.DocumentMapping 11 | Type() string 12 | } 13 | 14 | // BuildIndexMapping gets slice of documents (DocumentMapping interface) and returns index with those documents mappings. 15 | func BuildIndexMapping(documents ...DocumentMapping) mapping.IndexMapping { 16 | indexMapping := bleve.NewIndexMapping() 17 | 18 | for _, document := range documents { 19 | documentMapping := document.GetDocumentMapping() 20 | indexMapping.AddDocumentMapping(document.Type(), documentMapping) 21 | } 22 | 23 | return indexMapping 24 | } 25 | -------------------------------------------------------------------------------- /index/metrics.go: -------------------------------------------------------------------------------- 1 | package index 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | func (index *Index) checkIndexedTriggersCount() error { 8 | checkTicker := time.NewTicker(time.Millisecond * 100) //nolint 9 | 10 | for { 11 | select { 12 | case <-index.tomb.Dying(): 13 | return nil 14 | case <-checkTicker.C: 15 | if documents, err := index.triggerIndex.GetCount(); err == nil { 16 | index.metrics.IndexedTriggersCount.Update(documents) 17 | } 18 | } 19 | } 20 | } 21 | 22 | func (index *Index) checkIndexActualizationLag() error { 23 | checkTicker := time.NewTicker(time.Millisecond * 100) //nolint 24 | 25 | for { 26 | select { 27 | case <-index.tomb.Dying(): 28 | return nil 29 | case <-checkTicker.C: 30 | index.metrics.IndexActualizationLag.UpdateSince(time.Unix(index.indexActualizedTS, 0)) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /senders/webhook/delivery_check_request.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | func (sender *Sender) buildDeliveryCheckRequest(checkData deliveryCheckData) (*http.Request, error) { 9 | return buildRequest(sender.log, http.MethodGet, checkData.URL, nil, sender.deliveryCheckConfig.User, sender.deliveryCheckConfig.Password, sender.deliveryCheckConfig.Headers) 10 | } 11 | 12 | func (sender *Sender) doDeliveryCheckRequest(checkData deliveryCheckData) (int, []byte, error) { 13 | req, err := sender.buildDeliveryCheckRequest(checkData) 14 | if err != nil { 15 | return 0, nil, err 16 | } 17 | 18 | statusCode, body, err := performRequest(sender.client, req) 19 | if err != nil { 20 | return 0, nil, fmt.Errorf("check delivery request failed: %w", err) 21 | } 22 | 23 | return statusCode, body, nil 24 | } 25 | -------------------------------------------------------------------------------- /database/redis/throttling_test.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | logging "github.com/moira-alert/moira/logging/zerolog_adapter" 8 | . "github.com/smartystreets/goconvey/convey" 9 | ) 10 | 11 | func TestThrottlingErrorConnection(t *testing.T) { 12 | logger, _ := logging.GetLogger("dataBase") 13 | dataBase := NewTestDatabaseWithIncorrectConfig(logger) 14 | dataBase.Flush() 15 | 16 | defer dataBase.Flush() 17 | 18 | Convey("Should throw error when no connection", t, func() { 19 | t1, t2 := dataBase.GetTriggerThrottling("") 20 | So(t1, ShouldResemble, time.Unix(0, 0)) 21 | So(t2, ShouldResemble, time.Unix(0, 0)) 22 | 23 | err := dataBase.SetTriggerThrottling("", time.Now()) 24 | So(err, ShouldNotBeNil) 25 | 26 | err = dataBase.DeleteTriggerThrottling("") 27 | So(err, ShouldNotBeNil) 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /image_store/s3/store_test.go: -------------------------------------------------------------------------------- 1 | package s3 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/aws/aws-sdk-go/aws" 8 | . "github.com/smartystreets/goconvey/convey" 9 | ) 10 | 11 | func TestBuildUploadInput(t *testing.T) { 12 | imageStore := &ImageStore{} 13 | imageStore.Init(Config{ //nolint 14 | AccessKeyID: "123", 15 | AccessKey: "123", 16 | Region: "ap-south-1", 17 | Bucket: "testbucket", 18 | }) 19 | Convey("Build S3 upload input tests", t, func() { 20 | Convey("Build upload input with empty byte slice", func() { 21 | uploadInput, _ := imageStore.buildUploadInput([]byte{}) 22 | So(uploadInput.Body, ShouldResemble, bytes.NewReader([]byte{})) 23 | So(uploadInput.Bucket, ShouldResemble, aws.String(imageStore.bucket)) 24 | So(uploadInput.ACL, ShouldResemble, aws.String("public-read")) 25 | }) 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /cmd/cli/from_2.12_to_2.13.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/moira-alert/moira" 7 | ) 8 | 9 | func updateFrom212(logger moira.Logger, database moira.Database) error { 10 | logger.Info().Msg("Update 2.12 -> 2.13 was started") 11 | 12 | ctx := context.Background() 13 | 14 | err := splitNotificationHistoryByContactID(ctx, logger, database) 15 | if err != nil { 16 | return err 17 | } 18 | 19 | logger.Info().Msg("Update 2.12 -> 2.13 was finished") 20 | 21 | return nil 22 | } 23 | 24 | func downgradeTo212(logger moira.Logger, database moira.Database) error { 25 | logger.Info().Msg("Downgrade 2.13 -> 2.12 started") 26 | 27 | err := mergeNotificationHistory(logger, database) 28 | if err != nil { 29 | return err 30 | } 31 | 32 | logger.Info().Msg("Downgrade 2.13 -> 2.12 was finished") 33 | 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /index/bleve/write.go: -------------------------------------------------------------------------------- 1 | package bleve 2 | 3 | import ( 4 | "github.com/blevesearch/bleve/v2" 5 | "github.com/moira-alert/moira" 6 | "github.com/moira-alert/moira/index/mapping" 7 | ) 8 | 9 | // Write adds moira.TriggerChecks to TriggerIndex. 10 | func (index *TriggerIndex) Write(checks []*moira.TriggerCheck) error { 11 | batch := index.index.NewBatch() 12 | defer batch.Reset() 13 | 14 | for _, trigger := range checks { 15 | if trigger != nil { 16 | err := index.batchIndexTriggerCheck(batch, trigger) 17 | if err != nil { 18 | return err 19 | } 20 | } 21 | } 22 | 23 | return index.index.Batch(batch) 24 | } 25 | 26 | // used as abstraction. 27 | func (index *TriggerIndex) batchIndexTriggerCheck(batch *bleve.Batch, triggerCheck *moira.TriggerCheck) error { 28 | return batch.Index(triggerCheck.ID, mapping.CreateIndexedTrigger(triggerCheck)) 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-22.04 10 | services: 11 | redis: 12 | image: redis:6.2.12-alpine3.18 13 | # Set health checks to wait until redis has started 14 | options: >- 15 | --health-cmd "redis-cli ping" 16 | --health-interval 10s 17 | --health-timeout 5s 18 | --health-retries 5 19 | ports: 20 | - 6379:6379 21 | steps: 22 | - uses: actions/checkout@v4 23 | 24 | - uses: actions/setup-go@v4 25 | with: 26 | go-version-file: go.mod 27 | cache-dependency-path: go.sum 28 | 29 | - name: Run tests 30 | run: make ci-test 31 | 32 | - name: Upload coverage to Codecov 33 | run: bash <(curl -s https://codecov.io/bash) 34 | -------------------------------------------------------------------------------- /filter/pattern_index.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "github.com/moira-alert/moira" 5 | ) 6 | 7 | // PatternIndex helps to index patterns and allows to match them by metric. 8 | type PatternIndex struct { 9 | Tree *PrefixTree 10 | compatibility Compatibility 11 | } 12 | 13 | // NewPatternIndex creates new PatternIndex using patterns. 14 | func NewPatternIndex(logger moira.Logger, patterns []string, compatibility Compatibility) *PatternIndex { 15 | prefixTree := &PrefixTree{Logger: logger, Root: &PatternNode{}} 16 | for _, pattern := range patterns { 17 | prefixTree.Add(pattern) 18 | } 19 | 20 | return &PatternIndex{Tree: prefixTree, compatibility: compatibility} 21 | } 22 | 23 | // MatchPatterns allows matching pattern by metric. 24 | func (source *PatternIndex) MatchPatterns(metric string) []string { 25 | return source.Tree.Match(metric) 26 | } 27 | -------------------------------------------------------------------------------- /index/mapping/field_data.go: -------------------------------------------------------------------------------- 1 | package mapping 2 | 3 | // FieldData is container for field-related parameters. 4 | // name represents indexed object field name. 5 | // nameTag represents highlight field name for given field in search result, if value is empty then the highlight for this field is not used. 6 | // priority represents sort priority for given field. 7 | type FieldData struct { 8 | name string 9 | nameTag string 10 | priority float64 11 | } 12 | 13 | // GetName returns TriggerField name. 14 | func (field FieldData) GetName() string { 15 | return field.name 16 | } 17 | 18 | // GetTagValue returns TriggerField value used in marshalling. 19 | func (field FieldData) GetTagValue() string { 20 | return field.nameTag 21 | } 22 | 23 | // GetPriority returns field priority. 24 | func (field FieldData) GetPriority() float64 { 25 | return field.priority 26 | } 27 | -------------------------------------------------------------------------------- /Dockerfile.checker: -------------------------------------------------------------------------------- 1 | FROM golang:1.24 as builder 2 | 3 | COPY go.mod go.sum /go/src/github.com/moira-alert/moira/ 4 | WORKDIR /go/src/github.com/moira-alert/moira 5 | RUN go mod download 6 | 7 | COPY . /go/src/github.com/moira-alert/moira/ 8 | 9 | ARG GO_VERSION="GoVersion" 10 | ARG GIT_COMMIT="git_Commit" 11 | ARG MoiraVersion="MoiraVersion" 12 | 13 | RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags "-X main.MoiraVersion=${MoiraVersion} -X main.GoVersion=${GO_VERSION} -X main.GitCommit=${GIT_COMMIT}" -o build/checker github.com/moira-alert/moira/cmd/checker 14 | 15 | 16 | FROM alpine:3.18.0 17 | 18 | RUN apk add --no-cache ca-certificates && update-ca-certificates 19 | 20 | COPY pkg/checker/checker.yml /etc/moira/checker.yml 21 | 22 | COPY --from=builder /go/src/github.com/moira-alert/moira/build/checker /usr/bin/checker 23 | 24 | ENTRYPOINT ["/usr/bin/checker"] 25 | -------------------------------------------------------------------------------- /database/database.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import "fmt" 4 | 5 | // ErrNil return from database data storing methods if no object in DB. 6 | var ErrNil = fmt.Errorf("nil returned") 7 | 8 | var ( 9 | // ErrLockAlreadyHeld is returned if we attempt to double acquire. 10 | ErrLockAlreadyHeld = fmt.Errorf("lock was already held") 11 | // ErrLockAcquireInterrupted is returned if we cancel the acquire. 12 | ErrLockAcquireInterrupted = fmt.Errorf("lock's request was interrupted") 13 | ) 14 | 15 | // ErrLockNotAcquired if we cannot acquire. 16 | type ErrLockNotAcquired struct { 17 | Err error 18 | } 19 | 20 | func (e *ErrLockNotAcquired) Error() string { 21 | return fmt.Sprintf("lock was not acquired: %v", e.Err) 22 | } 23 | 24 | // ErrTeamWithNameAlreadyExists may be returned from SaveTeam method. 25 | var ErrTeamWithNameAlreadyExists = fmt.Errorf("team with such name alredy exists") 26 | -------------------------------------------------------------------------------- /pkg/notifier/notifier.yml: -------------------------------------------------------------------------------- 1 | #See https://moira.readthedocs.io/en/latest/installation/configuration.html for config explanation 2 | redis: 3 | addrs: "redis:6379" 4 | graphite: 5 | enabled: false 6 | runtime_stats: false 7 | uri: "localhost:2003" 8 | prefix: DevOps.Moira 9 | interval: 60s 10 | remote: 11 | enabled: false 12 | timeout: 60s 13 | notifier: 14 | sender_timeout: 10s 15 | resending_timeout: "1:00" 16 | senders: [] 17 | moira_selfstate: 18 | enabled: false 19 | remote_triggers_enabled: false 20 | redis_disconect_delay: 60s 21 | last_metric_received_delay: 120s 22 | last_check_delay: 120s 23 | last_remote_check_delay: 300s 24 | notice_interval: 300s 25 | front_uri: http://localhost 26 | timezone: UTC 27 | date_time_format: "15:04 02.01.2006" 28 | log: 29 | log_file: stdout 30 | log_level: info 31 | log_pretty_format: false 32 | -------------------------------------------------------------------------------- /cmd/cli/from_2.6_to_2.7.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/moira-alert/moira" 4 | 5 | func updateFrom26(logger moira.Logger, dataBase moira.Database) error { 6 | logger.Info().Msg("Update 2.6 -> 2.7 was started") 7 | 8 | logger.Info().Msg("Adding Redis Cluster support was started") 9 | 10 | if err := addRedisClusterSupport(logger, dataBase); err != nil { 11 | return err 12 | } 13 | 14 | logger.Info().Msg("Update 2.6 -> 2.7 was finished") 15 | 16 | return nil 17 | } 18 | 19 | func downgradeTo26(logger moira.Logger, dataBase moira.Database) error { 20 | logger.Info().Msg("Downgrade 2.7 -> 2.6 started") 21 | 22 | logger.Info().Msg("Removing Redis Cluster support was started") 23 | 24 | if err := removeRedisClusterSupport(logger, dataBase); err != nil { 25 | return err 26 | } 27 | 28 | logger.Info().Msg("Downgrade 2.7 -> 2.6 was finished") 29 | 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /.run/generate mocks.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /api/dto/event_history_item.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import "net/http" 4 | 5 | type ContactEventItem struct { 6 | TimeStamp int64 `json:"timestamp" format:"int64" binding:"required"` 7 | Metric string `json:"metric" binding:"required"` 8 | State string `json:"state" binding:"required"` 9 | OldState string `json:"old_state" binding:"required"` 10 | TriggerID string `json:"trigger_id" binding:"required"` 11 | } 12 | 13 | type ContactEventItemList struct { 14 | List []ContactEventItem `json:"list" binding:"required"` 15 | Page int64 `json:"page" example:"0" format:"int64" binding:"required"` 16 | Size int64 `json:"size" example:"100" format:"int64" binding:"required"` 17 | Total int64 `json:"total" example:"10" format:"int64" binding:"required"` 18 | } 19 | 20 | func (*ContactEventItemList) Render(w http.ResponseWriter, r *http.Request) error { 21 | return nil 22 | } 23 | -------------------------------------------------------------------------------- /senders/msgformat/msgformat.go: -------------------------------------------------------------------------------- 1 | // Package msgformat provides MessageFormatter interface which may be used for formatting messages. 2 | // Also, it contains some realizations such as highlightSyntaxFormatter. 3 | package msgformat 4 | 5 | import ( 6 | "github.com/moira-alert/moira" 7 | ) 8 | 9 | const ChangeTriggerRecommendation = "fix your system or tune this trigger" 10 | 11 | // MessageFormatter is used for formatting messages to send via telegram, mattermost, etc. 12 | type MessageFormatter interface { 13 | Format(params MessageFormatterParams) string 14 | } 15 | 16 | // MessageFormatterParams is the parameters for MessageFormatter. 17 | type MessageFormatterParams struct { 18 | Events moira.NotificationEvents 19 | Trigger moira.TriggerData 20 | Contact moira.ContactData 21 | // MessageMaxChars is a limit for future message. If -1 then no limit is set. 22 | MessageMaxChars int 23 | Throttled bool 24 | } 25 | -------------------------------------------------------------------------------- /checker/config.go: -------------------------------------------------------------------------------- 1 | package checker 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/moira-alert/moira" 7 | ) 8 | 9 | // Config represent checker config. 10 | type Config struct { 11 | Enabled bool 12 | LazyTriggersCheckInterval time.Duration 13 | SourceCheckConfigs map[moira.ClusterKey]SourceCheckConfig 14 | StopCheckingIntervalSeconds int64 15 | LogFile string 16 | LogLevel string 17 | LogTriggersToLevel map[string]string 18 | MetricEventPopBatchSize int64 19 | MetricEventPopDelay time.Duration 20 | CriticalTimeOfCheck time.Duration 21 | MetricEventTriggerCheckInterval time.Duration 22 | } 23 | 24 | // SourceCheckConfig represents check parameters for a single metric source. 25 | type SourceCheckConfig struct { 26 | CheckInterval time.Duration 27 | MaxParallelChecks int 28 | } 29 | -------------------------------------------------------------------------------- /Dockerfile.cli: -------------------------------------------------------------------------------- 1 | FROM golang:1.24 as builder 2 | 3 | COPY go.mod go.sum /go/src/github.com/moira-alert/moira/ 4 | WORKDIR /go/src/github.com/moira-alert/moira 5 | RUN go mod download 6 | 7 | COPY . /go/src/github.com/moira-alert/moira/ 8 | 9 | ARG GO_VERSION="GoVersion" 10 | ARG GIT_COMMIT="git_Commit" 11 | ARG MoiraVersion="MoiraVersion" 12 | 13 | RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags "-X main.MoiraVersion=${MoiraVersion} -X main.GoVersion=${GO_VERSION} -X main.GitCommit=${GIT_COMMIT}" -o build/cli github.com/moira-alert/moira/cmd/cli 14 | 15 | 16 | FROM alpine:3.18.0 17 | 18 | RUN apk add --no-cache ca-certificates && update-ca-certificates 19 | 20 | COPY pkg/cli/cli.yml /etc/moira/cli.yml 21 | 22 | COPY --from=builder /go/src/github.com/moira-alert/moira/build/cli /usr/bin/cli 23 | COPY --from=builder /usr/local/go/lib/time/zoneinfo.zip /usr/local/go/lib/time/ 24 | 25 | ENTRYPOINT ["/usr/bin/cli"] 26 | -------------------------------------------------------------------------------- /log_fields.go: -------------------------------------------------------------------------------- 1 | package moira 2 | 3 | const ( 4 | LogFieldNameCheckpoint = "moira.checkpoint" 5 | LogFieldNameContactID = "moira.contact.id" 6 | LogFieldNameContactType = "moira.contact.type" 7 | LogFieldNameContactValue = "moira.contact.value" 8 | LogFieldNameContactUser = "moira.contact.user" 9 | LogFieldNameContactTeam = "moira.contact.team" 10 | LogFieldNamePlotsBuildDuration = "moira.plots.build_duration_ms" 11 | LogFieldNameFailCount = "moira.notification.fail_count" 12 | LogFieldNameContext = "moira.context" 13 | LogFieldNameMetricName = "moira.metric.name" 14 | LogFieldNameMetricTimestamp = "moira.metric.timestamp" 15 | LogFieldNameTriggerID = "moira.trigger.id" 16 | LogFieldNameTriggerName = "moira.trigger.name" 17 | LogFieldNameSubscriptionID = "moira.subscription.id" 18 | LogFieldNameStackTrace = "stackTrace" 19 | ) 20 | -------------------------------------------------------------------------------- /metric_source/retries/retrier.go: -------------------------------------------------------------------------------- 1 | package retries 2 | 3 | import ( 4 | "github.com/cenkalti/backoff/v4" 5 | ) 6 | 7 | // Retrier retries the given operation with given backoff. 8 | type Retrier[T any] interface { 9 | // Retry the given operation until the op succeeds or op returns backoff.PermanentError or backoffPolicy returns backoff.Stop. 10 | Retry(op RetryableOperation[T], backoffPolicy backoff.BackOff) (T, error) 11 | } 12 | 13 | type standardRetrier[T any] struct{} 14 | 15 | // NewStandardRetrier returns standard retrier. 16 | func NewStandardRetrier[T any]() Retrier[T] { 17 | return standardRetrier[T]{} 18 | } 19 | 20 | // Retry the given operation until the op succeeds or op returns backoff.PermanentError or backoffPolicy returns backoff.Stop. 21 | func (r standardRetrier[T]) Retry(op RetryableOperation[T], backoffPolicy backoff.BackOff) (T, error) { 22 | return backoff.RetryWithData[T](op.DoRetryableOperation, backoffPolicy) 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/publish-packages.yml: -------------------------------------------------------------------------------- 1 | name: Publish packages 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | build: 10 | name: Create Release 11 | runs-on: ubuntu-22.04 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Set up Ruby 3.3 16 | uses: ruby/setup-ruby@v1 17 | with: 18 | ruby-version: 3.3 19 | 20 | - name: Install fpm 21 | run: gem install fpm 22 | 23 | - name: Build packages 24 | run: make packages 25 | 26 | - name: Release 27 | uses: docker://antonyurchenko/git-release:latest 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | DRAFT_RELEASE: "true" 31 | PRE_RELEASE: "true" 32 | CHANGELOG_FILE: "none" 33 | ALLOW_EMPTY_CHANGELOG: "true" 34 | with: 35 | args: | 36 | build/moira*.tar.gz build/moira*.rpm build/moira*.deb 37 | -------------------------------------------------------------------------------- /cmd/image_store.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/moira-alert/moira" 5 | 6 | "github.com/moira-alert/moira/image_store/s3" 7 | ) 8 | 9 | const ( 10 | s3ImageStore = "s3" 11 | ) 12 | 13 | // InitImageStores initializes the image storage provider with settings from the yaml config. 14 | func InitImageStores(imageStores ImageStoreConfig, logger moira.Logger) map[string]moira.ImageStore { 15 | var err error 16 | 17 | imageStoreMap := make(map[string]moira.ImageStore) 18 | 19 | imageStore := &s3.ImageStore{} 20 | if imageStores.S3 != (s3.Config{}) { 21 | if err = imageStore.Init(imageStores.S3); err != nil { 22 | logger.Warning(). 23 | Error(err). 24 | Msg("Failed to initialize image store") 25 | } else { 26 | logger.Info(). 27 | String("image_storage", s3ImageStore). 28 | Msg("Image store initialized") 29 | } 30 | } 31 | 32 | imageStoreMap[s3ImageStore] = imageStore 33 | 34 | return imageStoreMap 35 | } 36 | -------------------------------------------------------------------------------- /api/middleware/readonly_mode.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/go-chi/render" 7 | "github.com/moira-alert/moira/api" 8 | ) 9 | 10 | // ReadOnlyMiddleware returns 403 for mutating queries if readonly mode is enabled. 11 | func ReadOnlyMiddleware(config *api.Config) func(next http.Handler) http.Handler { 12 | return func(next http.Handler) http.Handler { 13 | fn := func(w http.ResponseWriter, r *http.Request) { 14 | if config.Flags.IsReadonlyEnabled && isMutatingMethod(r.Method) { 15 | render.Render(w, r, api.ErrorForbidden("Moira is currently in read-only mode")) //nolint:errcheck 16 | return 17 | } 18 | 19 | next.ServeHTTP(w, r) 20 | } 21 | 22 | return http.HandlerFunc(fn) 23 | } 24 | } 25 | 26 | func isMutatingMethod(method string) bool { 27 | return method == http.MethodPut || 28 | method == http.MethodPost || 29 | method == http.MethodPatch || 30 | method == http.MethodDelete 31 | } 32 | -------------------------------------------------------------------------------- /index/sweepper.go: -------------------------------------------------------------------------------- 1 | package index 2 | 3 | import "time" 4 | 5 | const ( 6 | sweeperTimeToKeep = time.Hour 7 | sweeperRunInterval = time.Minute 8 | ) 9 | 10 | func (index *Index) runTriggersToReindexSweepper() error { 11 | ticker := time.NewTicker(sweeperRunInterval) 12 | index.logger.Info(). 13 | String("trigger_time_to_keep", sweeperTimeToKeep.String()). 14 | String("time_between_sweeps", sweeperRunInterval.String()). 15 | Msg("Start triggers to reindex sweepper: remove outdated triggers from redis") 16 | 17 | for { 18 | select { 19 | case <-index.tomb.Dying(): 20 | index.logger.Info().Msg("Stop index sweepper") 21 | return nil 22 | case <-ticker.C: 23 | timeToDelete := time.Now().Add(-sweeperTimeToKeep).Unix() 24 | if err := index.database.RemoveTriggersToReindex(timeToDelete); err != nil { 25 | index.logger.Warning(). 26 | Error(err). 27 | Msg("Cannot sweep triggers to reindex from redis") 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /cmd/cli/extra.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/moira-alert/moira" 7 | ) 8 | 9 | func enablePlottingInAllSubscriptions(logger moira.Logger, database moira.Database) error { 10 | allTags, err := database.GetTagNames() 11 | if err != nil { 12 | return err 13 | } 14 | 15 | allSubscriptions, err := database.GetTagsSubscriptions(allTags) 16 | if err != nil { 17 | return err 18 | } 19 | 20 | for _, subscription := range allSubscriptions { 21 | if subscription == nil { 22 | continue 23 | } 24 | 25 | subscription.Plotting = moira.PlottingData{ 26 | Enabled: true, 27 | Theme: "light", 28 | } 29 | if err := database.SaveSubscription(subscription); err != nil { 30 | return err 31 | } 32 | 33 | logger.Debug(). 34 | String("subscription_id", subscription.ID). 35 | String("contacts", strings.Join(subscription.Contacts, ", ")). 36 | Msg("Successfully enabled plotting") 37 | } 38 | 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /Dockerfile.filter: -------------------------------------------------------------------------------- 1 | FROM golang:1.24 as builder 2 | 3 | COPY go.mod go.sum /go/src/github.com/moira-alert/moira/ 4 | WORKDIR /go/src/github.com/moira-alert/moira 5 | RUN go mod download 6 | 7 | COPY . /go/src/github.com/moira-alert/moira/ 8 | 9 | ARG GO_VERSION="GoVersion" 10 | ARG GIT_COMMIT="git_Commit" 11 | ARG MoiraVersion="MoiraVersion" 12 | 13 | RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags "-X main.MoiraVersion=${MoiraVersion} -X main.GoVersion=${GO_VERSION} -X main.GitCommit=${GIT_COMMIT}" -o build/filter github.com/moira-alert/moira/cmd/filter 14 | 15 | 16 | FROM alpine:3.18.0 17 | 18 | RUN apk add --no-cache ca-certificates && update-ca-certificates 19 | 20 | COPY pkg/filter/filter.yml /etc/moira/filter.yml 21 | COPY pkg/filter/storage-schemas.conf /etc/moira/storage-schemas.conf 22 | 23 | COPY --from=builder /go/src/github.com/moira-alert/moira/build/filter /usr/bin/filter 24 | 25 | EXPOSE 2003 2003 26 | 27 | ENTRYPOINT ["/usr/bin/filter"] 28 | -------------------------------------------------------------------------------- /Dockerfile.notifier: -------------------------------------------------------------------------------- 1 | FROM golang:1.24 as builder 2 | 3 | COPY go.mod go.sum /go/src/github.com/moira-alert/moira/ 4 | WORKDIR /go/src/github.com/moira-alert/moira 5 | RUN go mod download 6 | 7 | COPY . /go/src/github.com/moira-alert/moira/ 8 | 9 | ARG GO_VERSION="GoVersion" 10 | ARG GIT_COMMIT="git_Commit" 11 | ARG MoiraVersion="MoiraVersion" 12 | 13 | RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags "-X main.MoiraVersion=${MoiraVersion} -X main.GoVersion=${GO_VERSION} -X main.GitCommit=${GIT_COMMIT}" -o build/notifier github.com/moira-alert/moira/cmd/notifier 14 | 15 | 16 | FROM alpine:3.18.0 17 | 18 | RUN apk add --no-cache ca-certificates && update-ca-certificates 19 | 20 | COPY pkg/notifier/notifier.yml /etc/moira/notifier.yml 21 | 22 | COPY --from=builder /go/src/github.com/moira-alert/moira/build/notifier /usr/bin/notifier 23 | COPY --from=builder /usr/local/go/lib/time/zoneinfo.zip /usr/local/go/lib/time/ 24 | 25 | ENTRYPOINT ["/usr/bin/notifier"] 26 | -------------------------------------------------------------------------------- /pkg/filter/storage-schemas.conf: -------------------------------------------------------------------------------- 1 | # Schema definitions for Whisper files. Entries are scanned in order, 2 | # and first match wins. This file is scanned for changes every 60 seconds. 3 | # 4 | # Definition Syntax: 5 | # 6 | # [name] 7 | # pattern = regex 8 | # retentions = timePerPoint:timeToStore, timePerPoint:timeToStore, ... 9 | # 10 | # Remember: To support accurate aggregation from higher to lower resolution 11 | # archives, the precision of a longer retention archive must be 12 | # cleanly divisible by precision of next lower retention archive. 13 | # 14 | # Valid: 60s:7d,300s:30d (300/60 = 5) 15 | # Invalid: 180s:7d,300s:30d (300/180 = 3.333) 16 | # 17 | 18 | # Carbon's internal metrics. This entry should match what is specified in 19 | # CARBON_METRIC_PREFIX and CARBON_METRIC_INTERVAL settings 20 | [carbon] 21 | pattern = ^carbon\. 22 | retentions = 60:90d 23 | 24 | [default_1min_for_1day] 25 | pattern = .* 26 | retentions = 60s:1d 27 | -------------------------------------------------------------------------------- /api/handler/constants.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | const allMetricsPattern = ".*" 4 | 5 | const ( 6 | eventDefaultPage = 0 7 | eventDefaultSize = 100 8 | eventDefaultFrom = "-inf" 9 | eventDefaultTo = "+inf" 10 | eventDefaultMetric = allMetricsPattern 11 | ) 12 | 13 | const ( 14 | contactEventsDefaultFrom = "-3hour" 15 | contactEventsDefaultTo = "now" 16 | contactEventsDefaultPage = 0 17 | contactEventsDefaultSize = -1 18 | ) 19 | 20 | const ( 21 | getAllTeamsDefaultPage = 0 22 | getAllTeamsDefaultSize = -1 23 | getAllTeamsDefaultRegexTemplate = ".*" 24 | ) 25 | 26 | const ( 27 | getTriggerNoisinessDefaultPage = 0 28 | getTriggerNoisinessDefaultSize = -1 29 | getTriggerNoisinessDefaultFrom = "-3hour" 30 | getTriggerNoisinessDefaultTo = "now" 31 | ) 32 | 33 | const ( 34 | getContactNoisinessDefaultPage = 0 35 | getContactNoisinessDefaultSize = -1 36 | getContactNoisinessDefaultFrom = "-3hour" 37 | getContactNoisinessDefaultTo = "now" 38 | ) 39 | -------------------------------------------------------------------------------- /index/search.go: -------------------------------------------------------------------------------- 1 | package index 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/moira-alert/moira" 8 | ) 9 | 10 | const ( 11 | indexWaitTimeout = time.Second * 3 12 | ) 13 | 14 | // SearchTriggers search for triggers in index and returns slice of trigger IDs. 15 | func (index *Index) SearchTriggers(options moira.SearchOptions) (searchResults []*moira.SearchResult, total int64, err error) { 16 | if !index.checkIfIndexIsReady() { 17 | return make([]*moira.SearchResult, 0), 0, fmt.Errorf("index is not ready, please try later") 18 | } 19 | 20 | return index.triggerIndex.Search(options) 21 | } 22 | 23 | func (index *Index) checkIfIndexIsReady() bool { 24 | if index.IsReady() { 25 | return true 26 | } 27 | 28 | timeout := time.After(indexWaitTimeout) 29 | ticker := time.NewTicker(time.Second * 1) 30 | 31 | for { 32 | select { 33 | case <-ticker.C: 34 | if index.IsReady() { 35 | return true 36 | } 37 | case <-timeout: 38 | return index.IsReady() 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐞 Bug report 3 | about: Create a report for a bug 4 | labels: bug 5 | --- 6 | 7 | # BUG REPORT 8 | 9 | ## What version of Moira are you using (`[binary] --version`)? 10 | 11 | 12 | 13 | 14 | 15 | $ api --version 16 | 17 | $ checker --version 18 | 19 | $ cli --version 20 | 21 | $ filter --version 22 | 23 | $ notifier --version 24 | 25 | 26 | 27 | ## Configuration 28 | 29 | 30 | 31 | 32 | 33 | `api.yaml` 34 | 35 | ```yaml 36 | 37 | ``` 38 | 39 | `checker.yaml` 40 | 41 | ```yaml 42 | 43 | ``` 44 | 45 | `cli.yaml` 46 | 47 | ```yaml 48 | 49 | ``` 50 | 51 | `filter.yaml` 52 | 53 | ```yaml 54 | 55 | ``` 56 | 57 | `notifier.yaml` 58 | 59 | ```yaml 60 | 61 | ``` 62 | 63 | 64 | 65 | ## What did you expect to see? 66 | 67 | ## What did you see instead? 68 | -------------------------------------------------------------------------------- /metrics/index.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | const prefix = "searchIndex" 4 | 5 | // IndexMetrics is a collection of metrics used in full-text search index. 6 | type IndexMetrics struct { 7 | IndexedTriggersCount Histogram 8 | IndexActualizationLag Timer 9 | } 10 | 11 | // ConfigureIndexMetrics in full-text search index metrics configurator. 12 | func ConfigureIndexMetrics(registry Registry, attributedRegistry MetricRegistry) (*IndexMetrics, error) { 13 | indexedTriggersCount, err := attributedRegistry.NewHistogram("index.triggers.count") 14 | if err != nil { 15 | return nil, err 16 | } 17 | 18 | actualizationLag, err := attributedRegistry.NewTimer("index.actualization_lag") 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | return &IndexMetrics{ 24 | IndexedTriggersCount: NewCompositeHistogram(registry.NewHistogram(prefix, "indexedTriggers"), indexedTriggersCount), 25 | IndexActualizationLag: NewCompositeTimer(registry.NewTimer(prefix, "actualizationLag"), actualizationLag), 26 | }, nil 27 | } 28 | -------------------------------------------------------------------------------- /index/bleve/trigger_index.go: -------------------------------------------------------------------------------- 1 | package bleve 2 | 3 | import ( 4 | "github.com/blevesearch/bleve/v2" 5 | "github.com/blevesearch/bleve/v2/index/scorch" 6 | "github.com/blevesearch/bleve/v2/mapping" 7 | ) 8 | 9 | // TriggerIndex is implementation of index.TriggerIndex interface. 10 | type TriggerIndex struct { 11 | index bleve.Index 12 | } 13 | 14 | // CreateTriggerIndex returns TriggerIndex by provided mapping. 15 | func CreateTriggerIndex(mapping mapping.IndexMapping) (*TriggerIndex, error) { 16 | bleveIdx, err := bleve.NewUsing("", mapping, scorch.Name, scorch.Name, map[string]interface{}{}) 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | newIndex := &TriggerIndex{ 22 | index: bleveIdx, 23 | } 24 | 25 | return newIndex, nil 26 | } 27 | 28 | // GetCount returns number of documents in TriggerIndex. 29 | func (index *TriggerIndex) GetCount() (int64, error) { 30 | documents, err := index.index.DocCount() 31 | if err != nil { 32 | return 0, err 33 | } 34 | 35 | return int64(documents), nil 36 | } 37 | -------------------------------------------------------------------------------- /state_test.go: -------------------------------------------------------------------------------- 1 | package moira 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/smartystreets/goconvey/convey" 7 | ) 8 | 9 | func TestTtlState_ToMetricState(t *testing.T) { 10 | Convey("ToMetricState test", t, func() { 11 | So(TTLStateDEL.ToMetricState(), ShouldResemble, StateNODATA) 12 | So(TTLStateOK.ToMetricState(), ShouldResemble, StateOK) 13 | So(TTLStateWARN.ToMetricState(), ShouldResemble, StateWARN) 14 | So(TTLStateERROR.ToMetricState(), ShouldResemble, StateERROR) 15 | So(TTLStateNODATA.ToMetricState(), ShouldResemble, StateNODATA) 16 | }) 17 | } 18 | 19 | func TestTtlState_ToTriggerState(t *testing.T) { 20 | Convey("ToTriggerState test", t, func() { 21 | So(TTLStateDEL.ToTriggerState(), ShouldResemble, StateOK) 22 | So(TTLStateOK.ToTriggerState(), ShouldResemble, StateOK) 23 | So(TTLStateWARN.ToTriggerState(), ShouldResemble, StateWARN) 24 | So(TTLStateERROR.ToTriggerState(), ShouldResemble, StateERROR) 25 | So(TTLStateNODATA.ToTriggerState(), ShouldResemble, StateNODATA) 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /pkg/api/api.yml: -------------------------------------------------------------------------------- 1 | #See https://moira.readthedocs.io/en/latest/installation/configuration.html for config explanation 2 | redis: 3 | addrs: "redis:6379" 4 | graphite: 5 | enabled: false 6 | runtime_stats: false 7 | uri: "localhost:2003" 8 | prefix: DevOps.Moira 9 | interval: 60s 10 | remote: 11 | enabled: false 12 | timeout: 60s 13 | api: 14 | listen: ":8081" 15 | enable_cors: false 16 | web: 17 | contacts_template: 18 | - type: mail 19 | label: E-mail 20 | - type: pushover 21 | label: Pushover 22 | - type: slack 23 | label: Slack 24 | - type: telegram 25 | label: Telegram 26 | help: required to grant @MoiraBot admin privileges 27 | - type: twilio sms 28 | label: Twilio SMS 29 | - type: twilio voice 30 | label: Twilio voice 31 | feature_flags: 32 | is_plotting_available: true 33 | is_plotting_default_on: true 34 | is_subscription_to_all_tags_available: true 35 | log: 36 | log_file: stdout 37 | log_level: info 38 | log_pretty_format: false 39 | -------------------------------------------------------------------------------- /index/mapping/field_mapping.go: -------------------------------------------------------------------------------- 1 | package mapping 2 | 3 | import ( 4 | "github.com/blevesearch/bleve/v2" 5 | "github.com/blevesearch/bleve/v2/analysis/analyzer/keyword" 6 | "github.com/blevesearch/bleve/v2/analysis/analyzer/standard" 7 | "github.com/blevesearch/bleve/v2/mapping" 8 | ) 9 | 10 | func getKeywordMapping() *mapping.FieldMapping { 11 | keywordFieldMapping := bleve.NewTextFieldMapping() 12 | keywordFieldMapping.Analyzer = keyword.Name 13 | keywordFieldMapping.Store = false 14 | keywordFieldMapping.IncludeTermVectors = false 15 | keywordFieldMapping.IncludeInAll = false 16 | 17 | return keywordFieldMapping 18 | } 19 | 20 | func getStandardMapping() *mapping.FieldMapping { 21 | standardFieldMapping := bleve.NewTextFieldMapping() 22 | standardFieldMapping.Analyzer = standard.Name 23 | standardFieldMapping.Store = true 24 | standardFieldMapping.IncludeTermVectors = true 25 | 26 | return standardFieldMapping 27 | } 28 | 29 | func getNumericMapping() *mapping.FieldMapping { 30 | return bleve.NewNumericFieldMapping() 31 | } 32 | -------------------------------------------------------------------------------- /Dockerfile.api: -------------------------------------------------------------------------------- 1 | FROM golang:1.24 as builder 2 | 3 | COPY go.mod go.sum /go/src/github.com/moira-alert/moira/ 4 | WORKDIR /go/src/github.com/moira-alert/moira 5 | RUN go mod download 6 | RUN go install github.com/swaggo/swag/v2/cmd/swag@v2.0.0-rc4 7 | 8 | COPY . /go/src/github.com/moira-alert/moira/ 9 | 10 | RUN make spec-v3 11 | 12 | ARG GO_VERSION="GoVersion" 13 | ARG GIT_COMMIT="git_Commit" 14 | ARG MoiraVersion="MoiraVersion" 15 | 16 | RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags "-X main.MoiraVersion=${MoiraVersion} -X main.GoVersion=${GO_VERSION} -X main.GitCommit=${GIT_COMMIT}" -o build/api github.com/moira-alert/moira/cmd/api 17 | 18 | 19 | FROM alpine:3.18.0 20 | 21 | RUN apk add --no-cache ca-certificates && update-ca-certificates 22 | 23 | COPY pkg/api/api.yml /etc/moira/api.yml 24 | 25 | COPY --from=builder /go/src/github.com/moira-alert/moira/build/api /usr/bin/api 26 | COPY --from=builder /usr/local/go/lib/time/zoneinfo.zip /usr/local/go/lib/time/ 27 | 28 | EXPOSE 8081 8081 29 | 30 | ENTRYPOINT ["/usr/bin/api"] 31 | -------------------------------------------------------------------------------- /cmd/cli/from_2.3_to_2.4.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/moira-alert/moira" 4 | 5 | func updateFrom23(logger moira.Logger, dataBase moira.Database) error { 6 | logger.Info().Msg("Update 2.3 -> 2.4 start") 7 | 8 | logger.Info().Msg("Start marking unused triggers") 9 | 10 | if err := resaveTriggers(dataBase); err != nil { 11 | return err 12 | } 13 | 14 | logger.Info().Msg("Update 2.3 -> 2.4 finish") 15 | 16 | return nil 17 | } 18 | 19 | func downgradeTo23(logger moira.Logger, dataBase moira.Database) error { 20 | return nil 21 | } 22 | 23 | func resaveTriggers(database moira.Database) error { 24 | allTriggerIDs, err := database.GetAllTriggerIDs() 25 | if err != nil { 26 | return err 27 | } 28 | 29 | allTriggers, err := database.GetTriggers(allTriggerIDs) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | for _, trigger := range allTriggers { 35 | if trigger != nil { 36 | if err = database.SaveTrigger(trigger.ID, trigger); err != nil { 37 | return err 38 | } 39 | } 40 | } 41 | 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /database/redis/reply/subscription.go: -------------------------------------------------------------------------------- 1 | package reply 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/go-redis/redis/v8" 9 | "github.com/moira-alert/moira" 10 | "github.com/moira-alert/moira/database" 11 | ) 12 | 13 | // Subscription converts redis DB reply to moira.SubscriptionData object. 14 | func Subscription(rep *redis.StringCmd) (moira.SubscriptionData, error) { 15 | subscription := moira.SubscriptionData{ 16 | // TODO not sure if this is still necessary, maybe we should just convert database and forget about it 17 | ThrottlingEnabled: true, 18 | } 19 | 20 | bytes, err := rep.Bytes() 21 | if err != nil { 22 | if errors.Is(err, redis.Nil) { 23 | return subscription, database.ErrNil 24 | } 25 | 26 | return subscription, fmt.Errorf("failed to read subscription: %s", err.Error()) 27 | } 28 | 29 | err = json.Unmarshal(bytes, &subscription) 30 | if err != nil { 31 | return subscription, fmt.Errorf("failed to parse subscription json %s: %s", string(bytes), err.Error()) 32 | } 33 | 34 | return subscription, nil 35 | } 36 | -------------------------------------------------------------------------------- /database/stats/stats.go: -------------------------------------------------------------------------------- 1 | package stats 2 | 3 | import ( 4 | "gopkg.in/tomb.v2" 5 | ) 6 | 7 | // StatsReporter represents an interface for objects that report statistics. 8 | type StatsReporter interface { 9 | StartReport(stop <-chan struct{}) 10 | } 11 | 12 | type statsManager struct { 13 | tomb tomb.Tomb 14 | reporters []StatsReporter 15 | } 16 | 17 | // NewStatsManager creates a new statsManager instance with the given StatsReporters. 18 | func NewStatsManager(reporters ...StatsReporter) *statsManager { 19 | return &statsManager{ 20 | reporters: reporters, 21 | } 22 | } 23 | 24 | // Start starts reporting statistics for all registered StatsReporters. 25 | func (manager *statsManager) Start() { 26 | for _, reporter := range manager.reporters { 27 | manager.tomb.Go(func() error { 28 | reporter.StartReport(manager.tomb.Dying()) 29 | return nil 30 | }) 31 | } 32 | } 33 | 34 | // Stop stops all reporting activities and waits for the completion. 35 | func (manager *statsManager) Stop() error { 36 | manager.tomb.Kill(nil) 37 | return manager.tomb.Wait() 38 | } 39 | -------------------------------------------------------------------------------- /filter/metrics_parser_bench_test.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/moira-alert/moira" 7 | ) 8 | 9 | var ( 10 | metric = "test.metric;test1=test1test1;test2=test2test2test2;test3=test3test3test3test3;test4=test4test4test4test4test4;test5=test5test5test5test5;test6=test6test6test6;test7=test7test7;test8=test8" 11 | name = "test.metric" 12 | 13 | labels = map[string]string{ 14 | "test1": "test1test1", 15 | "test2": "test2test2test2", 16 | "test3": "test3test3test3test3", 17 | "test4": "test4test4test4test4test4", 18 | "test5": "test5test5test5test5", 19 | "test6": "test6test6test6", 20 | "test7": "test7test7", 21 | "test8": "test8", 22 | } 23 | ) 24 | 25 | func BenchmarkRestoreMetricStringByNameAndLabels(b *testing.B) { 26 | for i := 0; i < b.N; i++ { 27 | _ = restoreMetricStringByNameAndLabels(name, labels, len(metric)) 28 | } 29 | } 30 | 31 | func BenchmarkParseNameAndLabels(b *testing.B) { 32 | for i := 0; i < b.N; i++ { 33 | _, _, err := parseNameAndLabels(moira.UnsafeStringToBytes(metric)) 34 | if err != nil { 35 | b.Fatal(err) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/swagger-delete.yml: -------------------------------------------------------------------------------- 1 | name: Delete spec version from SwaggerHub 2 | defaults: 3 | run: 4 | working-directory: . 5 | on: 6 | push: 7 | branches: 8 | - master 9 | - release/* 10 | tags: 11 | - "v*" 12 | 13 | jobs: 14 | removespec: 15 | name: Delete api from SwaggerHub 16 | runs-on: ubuntu-22.04 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: actions/setup-node@v4 20 | with: 21 | node-version: '20.17.0' 22 | - run: npm i --location=global swaggerhub-cli 23 | - run: | 24 | VERSION=`echo ${GITHUB_REF_NAME}| sed 's#[^a-zA-Z0-9_\.\-]#_#g'` 25 | SWAGGERHUB_API_KEY=${{secrets.SWAGGERHUB_TOKEN}} swaggerhub api:unpublish "Moira/moira-alert/${VERSION}" || true 26 | SWAGGERHUB_API_KEY=${{secrets.SWAGGERHUB_TOKEN}} swaggerhub api:delete "Moira/moira-alert/${VERSION}" || true 27 | # The `|| true` at the end of the calls is necessary to keep the job from crashing 28 | # when deleting documentation that hasn't been created yet, but if you see something wrong happening, 29 | # remove `|| true` from the command 30 | -------------------------------------------------------------------------------- /metric_source/remote/fetch_result.go: -------------------------------------------------------------------------------- 1 | package remote 2 | 3 | import ( 4 | "fmt" 5 | 6 | metricSource "github.com/moira-alert/moira/metric_source" 7 | ) 8 | 9 | // FetchResult is implementation of metric_source.FetchResult interface, 10 | // which represent fetching result from remote graphite installation in moira format. 11 | type FetchResult struct { 12 | MetricsData []metricSource.MetricData 13 | } 14 | 15 | // GetMetricsData return all metrics data from fetch result. 16 | func (fetchResult *FetchResult) GetMetricsData() []metricSource.MetricData { 17 | return fetchResult.MetricsData 18 | } 19 | 20 | // GetPatterns always returns error, because we can't fetch target patterns from remote metrics source. 21 | func (*FetchResult) GetPatterns() ([]string, error) { 22 | return make([]string, 0), fmt.Errorf("remote fetch result never returns patterns") 23 | } 24 | 25 | // GetPatternMetrics always returns error, because remote fetch doesn't return base pattern metrics. 26 | func (*FetchResult) GetPatternMetrics() ([]string, error) { 27 | return make([]string, 0), fmt.Errorf("remote fetch result never returns pattern metrics") 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Moira 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /notifier/config.go: -------------------------------------------------------------------------------- 1 | package notifier 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // NotificationsLimitUnlimited There is a duplicate of this constant in database package to prevent cyclic dependencies. 8 | const NotificationsLimitUnlimited = int64(-1) 9 | 10 | // Config is sending settings including log settings. 11 | type Config struct { 12 | Enabled bool 13 | SelfStateEnabled bool 14 | SelfStateContacts []map[string]string 15 | SendingTimeout time.Duration 16 | ResendingTimeout time.Duration 17 | ReschedulingDelay time.Duration 18 | Senders []map[string]interface{} 19 | LogFile string 20 | LogLevel string 21 | FrontURL string 22 | Location *time.Location 23 | DateTimeFormat string 24 | ReadBatchSize int64 25 | MaxFailAttemptToSendAvailable int 26 | LogContactsToLevel map[string]string 27 | LogSubscriptionsToLevel map[string]string 28 | CheckNotifierStateTimeout time.Duration 29 | } 30 | -------------------------------------------------------------------------------- /database/stats/stats_test.go: -------------------------------------------------------------------------------- 1 | package stats 2 | 3 | import ( 4 | "testing" 5 | 6 | mock_moira_alert "github.com/moira-alert/moira/mock/moira-alert" 7 | . "github.com/smartystreets/goconvey/convey" 8 | "go.uber.org/mock/gomock" 9 | ) 10 | 11 | func TestNewStatsManager(t *testing.T) { 12 | mockCtrl := gomock.NewController(t) 13 | defer mockCtrl.Finish() 14 | 15 | triggerStats := mock_moira_alert.NewMockStatsReporter(mockCtrl) 16 | contactStats := mock_moira_alert.NewMockStatsReporter(mockCtrl) 17 | 18 | Convey("Test new stats manager", t, func() { 19 | Convey("Successfully create new stats manager", func() { 20 | manager := NewStatsManager(triggerStats, contactStats) 21 | 22 | So(manager.reporters, ShouldResemble, []StatsReporter{triggerStats, contactStats}) 23 | }) 24 | 25 | Convey("Successfully start stats manager", func() { 26 | manager := NewStatsManager(triggerStats, contactStats) 27 | 28 | triggerStats.EXPECT().StartReport(manager.tomb.Dying()).Times(1) 29 | contactStats.EXPECT().StartReport(manager.tomb.Dying()).Times(1) 30 | 31 | manager.Start() 32 | defer manager.Stop() //nolint 33 | }) 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /senders/msgformat/defaults_test.go: -------------------------------------------------------------------------------- 1 | package msgformat 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/smartystreets/goconvey/convey" 7 | ) 8 | 9 | func TestDefaultTagsLimiter(t *testing.T) { 10 | Convey("Test default tags limiter", t, func() { 11 | tags := []string{"tag1", "tag2"} 12 | 13 | Convey("with maxSize < 0", func() { 14 | tagsStr := DefaultTagsLimiter(tags, -1) 15 | 16 | So(tagsStr, ShouldResemble, "") 17 | }) 18 | 19 | Convey("with maxSize > total characters in tags string", func() { 20 | tagsStr := DefaultTagsLimiter(tags, 30) 21 | 22 | So(tagsStr, ShouldResemble, " [tag1][tag2]") 23 | }) 24 | 25 | Convey("with maxSize not enough for all tags", func() { 26 | tagsStr := DefaultTagsLimiter(tags, 8) 27 | 28 | So(tagsStr, ShouldResemble, " [tag1]") 29 | }) 30 | 31 | Convey("with one long tag > maxSize", func() { 32 | tagsStr := DefaultTagsLimiter([]string{"long_tag"}, 4) 33 | 34 | So(tagsStr, ShouldResemble, "") 35 | }) 36 | 37 | Convey("with no tags", func() { 38 | tagsStr := DefaultTagsLimiter([]string{}, 0) 39 | 40 | So(tagsStr, ShouldResemble, "") 41 | }) 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /senders/msgformat/defaults.go: -------------------------------------------------------------------------------- 1 | package msgformat 2 | 3 | import "unicode/utf8" 4 | 5 | // DefaultDescriptionCutter cuts description, so len(newDesc) <= maxSize. Ensure that len(desc) >= maxSize and 6 | // maxSize >= len("...\n"). 7 | func DefaultDescriptionCutter(desc string, maxSize int) string { 8 | suffix := "...\n" 9 | return desc[:maxSize-len(suffix)] + suffix 10 | } 11 | 12 | var bracketsLen = utf8.RuneCountInString("[]") 13 | 14 | // DefaultTagsLimiter cuts and formats tags to fit maxSize. There will be no tag parts, for example: 15 | // 16 | // if we have 17 | // 18 | // tags = []string{"tag1", "tag2} 19 | // maxSize = 8 20 | // 21 | // so call DefaultTagsLimiter(tags, maxSize) will return " [tag1]". 22 | func DefaultTagsLimiter(tags []string, maxSize int) string { 23 | tagsStr := " " 24 | lenTagsStr := utf8.RuneCountInString(tagsStr) 25 | 26 | for i := range tags { 27 | lenTag := utf8.RuneCountInString(tags[i]) + bracketsLen 28 | 29 | if lenTagsStr+lenTag > maxSize { 30 | break 31 | } 32 | 33 | tagsStr += "[" + tags[i] + "]" 34 | lenTagsStr += lenTag 35 | } 36 | 37 | if tagsStr == " " { 38 | return "" 39 | } 40 | 41 | return tagsStr 42 | } 43 | -------------------------------------------------------------------------------- /cmd/filter/compatibility.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/moira-alert/moira/filter" 5 | ) 6 | 7 | // Compatibility struct contains feature-flags that give user control over 8 | // features supported by different versions of graphite compatible with moira. 9 | type compatibility struct { 10 | // Controls how regices in tag matching are treated. 11 | // If false (default value), regex will match start of the string strictly. 'tag~=foo' is equivalent to 'tag~=^foo.*'. 12 | // If true, regex will match start of the string loosely. 'tag~=foo' is equivalent to 'tag~=.*foo.*'. 13 | AllowRegexLooseStartMatch bool `yaml:"allow_regex_loose_start_match"` 14 | // Controls how absent tags are treated. 15 | // If true (default value), empty tags in regices will be matched. 16 | // If false, empty tags will be discarded. 17 | AllowRegexMatchEmpty bool `yaml:"allow_regex_match_empty"` 18 | } 19 | 20 | func (compatibility *compatibility) toFilterCompatibility() filter.Compatibility { 21 | return filter.Compatibility{ 22 | AllowRegexLooseStartMatch: compatibility.AllowRegexLooseStartMatch, 23 | AllowRegexMatchEmpty: compatibility.AllowRegexMatchEmpty, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/docker-release.yml: -------------------------------------------------------------------------------- 1 | name: Publish docker release 2 | on: 3 | push: 4 | tags: 5 | - "v*" 6 | 7 | jobs: 8 | publish: 9 | name: Publish images 10 | runs-on: ubuntu-22.04 11 | strategy: 12 | matrix: 13 | services: [api, checker, cli, notifier, filter] 14 | steps: 15 | - name: Set up Docker Buildx 16 | uses: docker/setup-buildx-action@v2 17 | 18 | - uses: docker/login-action@v2 19 | name: Login to DockerHub 20 | with: 21 | username: ${{ secrets.DOCKERHUB_USERNAME }} 22 | password: ${{ secrets.DOCKERHUB_PASSWORD }} 23 | 24 | - name: Build docker tag 25 | run: echo "DOCKER_TAG=$(echo ${{github.ref_name}} | cut -c2-)" >> $GITHUB_ENV 26 | 27 | - name: Build and push 28 | uses: docker/build-push-action@v4 29 | with: 30 | file: ./Dockerfile.${{matrix.services}} 31 | build-args: | 32 | MoiraVersion=${{env.DOCKER_TAG}} 33 | GIT_COMMIT=${{github.sha}} 34 | push: true 35 | tags: moira/${{matrix.services}}:${{env.DOCKER_TAG}},moira/${{matrix.services}}:latest 36 | -------------------------------------------------------------------------------- /metric_source/remote/fetch_result_test.go: -------------------------------------------------------------------------------- 1 | package remote 2 | 3 | import ( 4 | "testing" 5 | 6 | metricSource "github.com/moira-alert/moira/metric_source" 7 | . "github.com/smartystreets/goconvey/convey" 8 | ) 9 | 10 | func TestFetchResult(t *testing.T) { 11 | Convey("Get empty metric data", t, func() { 12 | fetchResult := FetchResult{ 13 | MetricsData: make([]metricSource.MetricData, 0), 14 | } 15 | So(fetchResult.GetMetricsData(), ShouldBeEmpty) 16 | patterns, err := fetchResult.GetPatterns() 17 | So(patterns, ShouldBeEmpty) 18 | So(err, ShouldNotBeEmpty) 19 | metrics, err := fetchResult.GetPatternMetrics() 20 | So(metrics, ShouldBeEmpty) 21 | So(err, ShouldNotBeEmpty) 22 | }) 23 | 24 | Convey("Get not empty metric data", t, func() { 25 | fetchResult := &FetchResult{ 26 | MetricsData: []metricSource.MetricData{*metricSource.MakeMetricData("123", []float64{1, 2, 3}, 60, 0)}, 27 | } 28 | So(fetchResult.GetMetricsData(), ShouldHaveLength, 1) 29 | patterns, err := fetchResult.GetPatterns() 30 | So(patterns, ShouldBeEmpty) 31 | So(err, ShouldNotBeEmpty) 32 | metrics, err := fetchResult.GetPatternMetrics() 33 | So(metrics, ShouldBeEmpty) 34 | So(err, ShouldNotBeEmpty) 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /metric_source/retries/config.go: -------------------------------------------------------------------------------- 1 | package retries 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Config for exponential backoff retries. 8 | type Config struct { 9 | // InitialInterval between requests. 10 | InitialInterval time.Duration `validate:"required,gt=0s"` 11 | // RandomizationFactor is used in exponential backoff to add some randomization 12 | // when calculating next interval between requests. 13 | // It will be used in multiplication like: 14 | // RandomizedInterval = RetryInterval * (random value in range [1 - RandomizationFactor, 1 + RandomizationFactor]) 15 | RandomizationFactor float64 16 | // Each new RetryInterval will be multiplied on Multiplier. 17 | Multiplier float64 18 | // MaxInterval is the cap for RetryInterval. Note that it doesn't cap the RandomizedInterval. 19 | MaxInterval time.Duration `validate:"required,gt=0s"` 20 | // MaxElapsedTime caps the time passed from first try. If time passed is greater than MaxElapsedTime than stop retrying. 21 | MaxElapsedTime time.Duration `validate:"required_if=MaxRetriesCount 0"` 22 | // MaxRetriesCount is the amount of allowed retries. So at most MaxRetriesCount will be performed. 23 | MaxRetriesCount uint64 `validate:"required_if=MaxElapsedTime 0"` 24 | } 25 | -------------------------------------------------------------------------------- /api/authorization.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | // Authorization contains authorization configuration. 4 | type Authorization struct { 5 | AdminList map[string]struct{} 6 | Enabled bool 7 | AllowedContactTypes map[string]struct{} 8 | LimitedChangeTriggerOwners map[string]struct{} 9 | } 10 | 11 | // IsEnabled returns true if auth is enabled and false otherwise. 12 | func (auth *Authorization) IsEnabled() bool { 13 | return auth.Enabled 14 | } 15 | 16 | // IsAdmin checks whether given user is considered an administrator. 17 | func (auth *Authorization) IsAdmin(login string) bool { 18 | if !auth.IsEnabled() { 19 | return false 20 | } 21 | 22 | _, ok := auth.AdminList[login] 23 | 24 | return ok 25 | } 26 | 27 | // The Role is an enumeration that represents the scope of user's permissions. 28 | type Role string 29 | 30 | var ( 31 | RoleUndefined Role = "" 32 | RoleUser Role = "user" 33 | RoleAdmin Role = "admin" 34 | ) 35 | 36 | // GetRole Returns the role of the given user. 37 | func (auth *Authorization) GetRole(login string) Role { 38 | if !auth.IsEnabled() { 39 | return RoleUndefined 40 | } 41 | 42 | if auth.IsAdmin(login) { 43 | return RoleAdmin 44 | } 45 | 46 | return RoleUser 47 | } 48 | -------------------------------------------------------------------------------- /plotting/theme.go: -------------------------------------------------------------------------------- 1 | package plotting 2 | 3 | import ( 4 | "github.com/golang/freetype/truetype" 5 | 6 | "github.com/moira-alert/moira" 7 | "github.com/moira-alert/moira/plotting/fonts" 8 | "github.com/moira-alert/moira/plotting/themes/dark" 9 | "github.com/moira-alert/moira/plotting/themes/light" 10 | ) 11 | 12 | const ( 13 | darkPlotTheme = "dark" 14 | lightPlotTheme = "light" 15 | ) 16 | 17 | // getPlotTheme returns plot theme. 18 | func getPlotTheme(plotTheme string) (moira.PlotTheme, error) { 19 | // TODO: rewrite light theme 20 | var err error 21 | 22 | var theme moira.PlotTheme 23 | 24 | themeFont, err := getDefaultFont() 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | switch plotTheme { 30 | case darkPlotTheme: 31 | theme, err = dark.NewTheme(themeFont) 32 | if err != nil { 33 | return nil, err 34 | } 35 | case lightPlotTheme: 36 | fallthrough 37 | default: 38 | theme, err = light.NewTheme(themeFont) 39 | if err != nil { 40 | return nil, err 41 | } 42 | } 43 | 44 | return theme, nil 45 | } 46 | 47 | // getDefaultFont returns default font. 48 | func getDefaultFont() (*truetype.Font, error) { 49 | ttf, err := truetype.Parse(fonts.DejaVuSans) 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | return ttf, nil 55 | } 56 | -------------------------------------------------------------------------------- /metric_source/prometheus/prometheus_api.go: -------------------------------------------------------------------------------- 1 | package prometheus 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "fmt" 7 | 8 | "github.com/prometheus/client_golang/api" 9 | promApi "github.com/prometheus/client_golang/api/prometheus/v1" 10 | promConfig "github.com/prometheus/common/config" 11 | "github.com/prometheus/common/model" 12 | ) 13 | 14 | type PrometheusApi interface { 15 | QueryRange(ctx context.Context, query string, r promApi.Range, opts ...promApi.Option) (model.Value, promApi.Warnings, error) 16 | } 17 | 18 | func createPrometheusApi(config *Config) (promApi.API, error) { 19 | roundTripper := api.DefaultRoundTripper 20 | 21 | if config.User != "" && config.Password != "" { 22 | rawToken := fmt.Sprintf("%s:%s", config.User, config.Password) 23 | token := base64.StdEncoding.EncodeToString([]byte(rawToken)) 24 | 25 | roundTripper = promConfig.NewAuthorizationCredentialsRoundTripper( 26 | "Basic", 27 | promConfig.NewInlineSecret(token), 28 | roundTripper, 29 | ) 30 | } 31 | 32 | promClientConfig := api.Config{ 33 | Address: config.URL, 34 | RoundTripper: roundTripper, 35 | } 36 | 37 | promCl, err := api.NewClient(promClientConfig) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | return promApi.NewAPI(promCl), nil 43 | } 44 | -------------------------------------------------------------------------------- /api/middleware/middleware_test.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "testing" 7 | 8 | . "github.com/smartystreets/goconvey/convey" 9 | ) 10 | 11 | func TestGetLogin(t *testing.T) { 12 | Convey("Request does not contain login, should get anonymous", t, func() { 13 | req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "https://testurl.com", http.NoBody) 14 | So(err, ShouldBeNil) 15 | So(anonymousUser, ShouldEqual, GetLogin(req)) 16 | }) 17 | 18 | Convey("Request contains login, but empty, should get anonymous", t, func() { 19 | req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "https://testurl.com", http.NoBody) 20 | ctx := context.WithValue(req.Context(), loginKey, "") 21 | req = req.WithContext(ctx) 22 | 23 | So(err, ShouldBeNil) 24 | So(anonymousUser, ShouldEqual, GetLogin(req)) 25 | }) 26 | 27 | Convey("Request contains login header, should get that", t, func() { 28 | req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "https://testurl.com", http.NoBody) 29 | ctx := context.WithValue(req.Context(), loginKey, "awesome_user") 30 | req = req.WithContext(ctx) 31 | 32 | So(err, ShouldBeNil) 33 | So("awesome_user", ShouldEqual, GetLogin(req)) 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /metric_source/local/timer.go: -------------------------------------------------------------------------------- 1 | package local 2 | 3 | type timer struct { 4 | startTime int64 5 | stopTime int64 6 | stepTime int64 7 | } 8 | 9 | func roundTimestamps(startTime, stopTime, retention int64) (roundedStart, roundedStop int64) { 10 | until := floorToMultiplier(stopTime, retention) + retention 11 | from := ceilToMultiplier(startTime, retention) 12 | 13 | return from, until 14 | } 15 | 16 | func newTimerRoundingTimestamps(startTime int64, stopTime int64, retention int64) timer { 17 | startTime, stopTime = roundTimestamps(startTime, stopTime, retention) 18 | 19 | return timer{ 20 | startTime: startTime, 21 | stopTime: stopTime, 22 | stepTime: retention, 23 | } 24 | } 25 | 26 | func (t timer) numberOfTimeSlots() int { 27 | return t.getTimeSlot(t.stopTime) 28 | } 29 | 30 | func (t timer) getTimeSlot(timestamp int64) int { 31 | timeSlot := floorToMultiplier(timestamp-t.startTime, t.stepTime) / t.stepTime 32 | return int(timeSlot) 33 | } 34 | 35 | func ceilToMultiplier(ts, retention int64) int64 { 36 | if (ts % retention) == 0 { 37 | return ts 38 | } 39 | 40 | return (ts + retention) / retention * retention 41 | } 42 | 43 | func floorToMultiplier(ts, retention int64) int64 { 44 | if ts < 0 { 45 | ts -= retention - 1 46 | } 47 | 48 | return ts - ts%retention 49 | } 50 | -------------------------------------------------------------------------------- /index/triggers.go: -------------------------------------------------------------------------------- 1 | package index 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/moira-alert/moira" 7 | ) 8 | 9 | var fakeTriggerToIndex = &moira.TriggerCheck{ 10 | Trigger: moira.Trigger{ 11 | ID: "This.Is.Fake.Trigger.ID.It.Should.Not.Exist.In.Real.Life", 12 | Name: "Fake trigger to index", 13 | }, 14 | LastCheck: moira.CheckData{ 15 | Score: 0, 16 | }, 17 | } 18 | 19 | func (index *Index) fillIndex() error { 20 | index.logger.Debug().Msg("Start filling index with triggers") 21 | 22 | index.inProgress = true 23 | index.indexActualizedTS = time.Now().Unix() 24 | allTriggerIDs, err := index.database.GetAllTriggerIDs() 25 | 26 | index.logger.Debug(). 27 | Int("Quantity", len(allTriggerIDs)). 28 | Msg("Triggers IDs fetched from database") 29 | 30 | if err != nil { 31 | return err 32 | } 33 | 34 | // We index fake trigger to increase batch index speed. Otherwise, first batch is indexed for too long 35 | index.triggerIndex.Write([]*moira.TriggerCheck{fakeTriggerToIndex}) //nolint 36 | defer index.triggerIndex.Delete([]string{fakeTriggerToIndex.ID}) //nolint 37 | 38 | err = index.writeByBatches(allTriggerIDs, defaultIndexBatchSize) 39 | 40 | index.logger.Info(). 41 | Int("Quantity", len(allTriggerIDs)). 42 | Msg("Added triggers to index") 43 | 44 | return err 45 | } 46 | -------------------------------------------------------------------------------- /api/controller/contact_events.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/moira-alert/moira" 7 | "github.com/moira-alert/moira/api" 8 | "github.com/moira-alert/moira/api/dto" 9 | ) 10 | 11 | // GetContactEventsHistoryByID is a controller that fetches events from database by using moira.Database.GetNotificationsHistoryByContactID. 12 | func GetContactEventsHistoryByID(database moira.Database, contactID string, from, to, page, size int64, 13 | ) (*dto.ContactEventItemList, *api.ErrorResponse) { 14 | events, total, err := database.GetNotificationsHistoryByContactID(contactID, from, to, page, size) 15 | if err != nil { 16 | return nil, api.ErrorInternalServer(fmt.Errorf("GetContactEventsHistoryByID: can't get notifications for contact with id %v", contactID)) 17 | } 18 | 19 | eventsList := dto.ContactEventItemList{ 20 | List: make([]dto.ContactEventItem, 0, len(events)), 21 | Page: page, 22 | Size: size, 23 | Total: total, 24 | } 25 | 26 | for _, i := range events { 27 | contactEventItem := &dto.ContactEventItem{ 28 | TimeStamp: i.TimeStamp, 29 | Metric: i.Metric, 30 | State: string(i.State), 31 | OldState: string(i.OldState), 32 | TriggerID: i.TriggerID, 33 | } 34 | eventsList.List = append(eventsList.List, *contactEventItem) 35 | } 36 | 37 | return &eventsList, nil 38 | } 39 | -------------------------------------------------------------------------------- /metric_source/local/fetch_result.go: -------------------------------------------------------------------------------- 1 | package local 2 | 3 | import ( 4 | metricSource "github.com/moira-alert/moira/metric_source" 5 | ) 6 | 7 | // FetchResult is implementation of metric_source.FetchResult interface, 8 | // which represent fetching result from moira data source in moira format. 9 | type FetchResult struct { 10 | MetricsData []metricSource.MetricData 11 | Patterns []string 12 | Metrics []string 13 | } 14 | 15 | // CreateEmptyFetchResult just creates FetchResult with initialized empty fields. 16 | func CreateEmptyFetchResult() *FetchResult { 17 | return &FetchResult{ 18 | MetricsData: make([]metricSource.MetricData, 0), 19 | Patterns: make([]string, 0), 20 | Metrics: make([]string, 0), 21 | } 22 | } 23 | 24 | // GetMetricsData return all metrics data from fetch result. 25 | func (fetchResult *FetchResult) GetMetricsData() []metricSource.MetricData { 26 | return fetchResult.MetricsData 27 | } 28 | 29 | // GetPatterns return all patterns which contains in evaluated graphite target. 30 | func (fetchResult *FetchResult) GetPatterns() ([]string, error) { 31 | return fetchResult.Patterns, nil 32 | } 33 | 34 | // GetPatternMetrics return all metrics which match to evaluated graphite target patterns. 35 | func (fetchResult *FetchResult) GetPatternMetrics() ([]string, error) { 36 | return fetchResult.Metrics, nil 37 | } 38 | -------------------------------------------------------------------------------- /senders/discord/response.go: -------------------------------------------------------------------------------- 1 | package discord 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bwmarrin/discordgo" 7 | ) 8 | 9 | func (sender *Sender) getResponse(m *discordgo.MessageCreate, channel *discordgo.Channel) (string, error) { 10 | // Ignore all messages created by the bot itself 11 | if m.Author.ID == sender.botUserID { 12 | return "", nil 13 | } 14 | 15 | // If the message is "!start" update the channel ID for the user/channel 16 | if m.Content == "!start" { //nolint 17 | switch channel.Type { 18 | case discordgo.ChannelTypeDM: 19 | err := sender.DataBase.SetUsernameChat(messenger, "@"+m.Author.Username, channel.ID) 20 | if err != nil { 21 | return "", fmt.Errorf("error while setting the channel ID for user: %w", err) 22 | } 23 | 24 | msg := fmt.Sprintf("Okay, %s, your id is %s", m.Author.Username, channel.ID) 25 | 26 | return msg, nil 27 | case discordgo.ChannelTypeGuildText: 28 | err := sender.DataBase.SetUsernameChat(messenger, channel.Name, channel.ID) 29 | if err != nil { 30 | return "", fmt.Errorf("error while setting the channel ID for text channel: %w", err) 31 | } 32 | 33 | msg := fmt.Sprintf("Hi, all!\nI will send alerts in this group (%s).", channel.Name) 34 | 35 | return msg, nil 36 | default: 37 | return "Unsupported channel type", nil 38 | } 39 | } 40 | 41 | return "", nil 42 | } 43 | -------------------------------------------------------------------------------- /metrics/triggers.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "github.com/moira-alert/moira" 5 | ) 6 | 7 | // TriggersMetrics Collection of metrics for trigger count metrics. 8 | type TriggersMetrics struct { 9 | triggerCounts map[moira.ClusterKey]Meter 10 | } 11 | 12 | // NewTriggersMetrics Creates and configurates the instance of TriggersMetrics. 13 | func NewTriggersMetrics(registry Registry, attributedRegistry MetricRegistry, clusterKeys []moira.ClusterKey) (*TriggersMetrics, error) { 14 | meters := make(map[moira.ClusterKey]Meter, len(clusterKeys)) 15 | 16 | for _, key := range clusterKeys { 17 | attributedReg := attributedRegistry.WithAttributes(Attributes{ 18 | Attribute{Key: "trigger_source", Value: key.TriggerSource.String()}, 19 | Attribute{Key: "cluster_id", Value: key.ClusterId.String()}, 20 | }) 21 | 22 | attributedMeter, err := attributedReg.NewGauge("triggers_count") 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | meters[key] = NewCompositeMeter(registry.NewMeter("triggers", key.TriggerSource.String(), key.ClusterId.String()), attributedMeter) 28 | } 29 | 30 | return &TriggersMetrics{ 31 | triggerCounts: meters, 32 | }, nil 33 | } 34 | 35 | // Mark Marks the number of trigger for given trigger source. 36 | func (metrics *TriggersMetrics) Mark(source moira.ClusterKey, count int64) { 37 | metrics.triggerCounts[source].Mark(count) 38 | } 39 | -------------------------------------------------------------------------------- /cmd/cli/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/moira-alert/moira/cmd" 5 | ) 6 | 7 | type config struct { 8 | LogFile string `yaml:"log_file"` 9 | LogLevel string `yaml:"log_level"` 10 | LogPrettyFormat bool `yaml:"log_pretty_format"` 11 | Redis cmd.RedisConfig `yaml:"redis"` 12 | Cleanup cleanupConfig `yaml:"cleanup"` 13 | } 14 | 15 | type cleanupConfig struct { 16 | Whitelist []string `yaml:"whitelist"` 17 | Delete bool `yaml:"delete"` 18 | AddAnonymousToWhitelist bool `json:"add_anonymous_to_whitelist"` 19 | CleanupMetricsDuration string `yaml:"cleanup_metrics_duration"` 20 | CleanupFutureMetricsDuration string `yaml:"cleanup_future_metrics_duration"` 21 | CleanupNotificationHistoryDuration string `yaml:"cleanup_notification_history_duration"` 22 | } 23 | 24 | func getDefault() config { 25 | return config{ 26 | LogFile: "stdout", 27 | LogLevel: "info", 28 | LogPrettyFormat: false, 29 | Redis: cmd.DefaultRedisConfig(), 30 | Cleanup: cleanupConfig{ 31 | Whitelist: []string{}, 32 | CleanupMetricsDuration: "-168h", 33 | CleanupFutureMetricsDuration: "60m", 34 | CleanupNotificationHistoryDuration: "48h", 35 | }, 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /logging/zerolog_adapter/event_builder.go: -------------------------------------------------------------------------------- 1 | package zerolog_adapter 2 | 3 | import ( 4 | "github.com/moira-alert/moira/logging" 5 | "github.com/rs/zerolog" 6 | ) 7 | 8 | type EventBuilder struct { 9 | event *zerolog.Event 10 | } 11 | 12 | func (e EventBuilder) Msg(msg string) { 13 | if e.event != nil { 14 | e.event.Timestamp().Msg(msg) 15 | } 16 | } 17 | 18 | func (e EventBuilder) String(key, value string) logging.EventBuilder { 19 | if e.event != nil { 20 | e.event.Str(key, value) 21 | } 22 | 23 | return e 24 | } 25 | 26 | func (e EventBuilder) Error(err error) logging.EventBuilder { 27 | if e.event != nil { 28 | e.event.Err(err) 29 | } 30 | 31 | return e 32 | } 33 | 34 | func (e EventBuilder) Int(key string, value int) logging.EventBuilder { 35 | if e.event != nil { 36 | e.event.Int(key, value) 37 | } 38 | 39 | return e 40 | } 41 | 42 | func (e EventBuilder) Int64(key string, value int64) logging.EventBuilder { 43 | if e.event != nil { 44 | e.event.Int64(key, value) 45 | } 46 | 47 | return e 48 | } 49 | 50 | func (e EventBuilder) Interface(key string, value interface{}) logging.EventBuilder { 51 | if e.event != nil { 52 | e.event.Interface(key, value) 53 | } 54 | 55 | return e 56 | } 57 | 58 | func (e EventBuilder) Fields(fields map[string]interface{}) logging.EventBuilder { 59 | if e.event != nil { 60 | e.event.Fields(fields) 61 | } 62 | 63 | return e 64 | } 65 | -------------------------------------------------------------------------------- /image_store/s3/store.go: -------------------------------------------------------------------------------- 1 | package s3 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/aws/aws-sdk-go/aws" 9 | "github.com/aws/aws-sdk-go/service/s3/s3manager" 10 | "github.com/gofrs/uuid" 11 | ) 12 | 13 | // StoreImage stores an image in aws s3 and returns the link to it. 14 | func (imageStore *ImageStore) StoreImage(image []byte) (string, error) { 15 | uploadInput, err := imageStore.buildUploadInput(image) 16 | if err != nil { 17 | return "", fmt.Errorf("error while creating upload input: %w", err) 18 | } 19 | 20 | result, err := imageStore.uploader.Upload(uploadInput) 21 | if err != nil { 22 | return "", fmt.Errorf("error while uploading to s3: %w", err) 23 | } 24 | 25 | return result.Location, nil 26 | } 27 | 28 | func (imageStore *ImageStore) buildUploadInput(image []byte) (*s3manager.UploadInput, error) { 29 | uuid, err := uuid.NewV4() 30 | if err != nil { 31 | return nil, fmt.Errorf("failed to generate uuid: %w", err) 32 | } 33 | 34 | key := "moira-plots/" + uuid.String() 35 | 36 | return &s3manager.UploadInput{ 37 | Bucket: aws.String(imageStore.bucket), 38 | Key: aws.String(key), 39 | ACL: aws.String("public-read"), 40 | Body: bytes.NewReader(image), 41 | ContentType: aws.String(http.DetectContentType(image)), 42 | ContentDisposition: aws.String("attachment"), 43 | }, nil 44 | } 45 | -------------------------------------------------------------------------------- /database/redis/triggers_to_reindex.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strconv" 7 | 8 | "github.com/go-redis/redis/v8" 9 | ) 10 | 11 | // FetchTriggersToReindex returns trigger IDs updated since 'from' param. 12 | // The trigger could be changed by user, or it's score was changed during trigger check. 13 | func (connector *DbConnector) FetchTriggersToReindex(from int64) ([]string, error) { 14 | ctx := connector.context 15 | c := *connector.client 16 | 17 | rng := &redis.ZRangeBy{Min: strconv.FormatInt(from, 10), Max: "+inf"} 18 | 19 | response, err := c.ZRangeByScore(ctx, triggersToReindexKey, rng).Result() 20 | if err != nil { 21 | return nil, fmt.Errorf("failed to fetch triggers to reindex: %s", err.Error()) 22 | } 23 | 24 | if len(response) == 0 { 25 | return make([]string, 0), nil 26 | } 27 | 28 | return response, nil 29 | } 30 | 31 | // RemoveTriggersToReindex removes outdated triggerIDs from redis. 32 | func (connector *DbConnector) RemoveTriggersToReindex(to int64) error { 33 | ctx := connector.context 34 | c := *connector.client 35 | 36 | _, err := c.ZRemRangeByScore(ctx, triggersToReindexKey, "-inf", strconv.FormatInt(to, 10)).Result() 37 | 38 | if err != nil && !errors.Is(err, redis.Nil) { 39 | return fmt.Errorf("failed to remove triggers to reindex: %s", err.Error()) 40 | } 41 | 42 | return nil 43 | } 44 | 45 | var triggersToReindexKey = "moira-triggers-to-reindex" 46 | -------------------------------------------------------------------------------- /notifier/log_common.go: -------------------------------------------------------------------------------- 1 | package notifier 2 | 3 | import "github.com/moira-alert/moira" 4 | 5 | func getLogWithPackageContext(log *moira.Logger, pkg *NotificationPackage, config *Config) moira.Logger { 6 | logger := (*log).Clone(). 7 | String(moira.LogFieldNameContactID, pkg.Contact.ID). 8 | String(moira.LogFieldNameContactType, pkg.Contact.Type). 9 | String(moira.LogFieldNameContactValue, pkg.Contact.Value). 10 | Int(moira.LogFieldNameFailCount, pkg.FailCount). 11 | String(moira.LogFieldNameContext, "notification") 12 | 13 | if pkg.Trigger.ID != "" { // note: test notification without trigger info 14 | logger. 15 | String(moira.LogFieldNameTriggerID, pkg.Trigger.ID). 16 | String(moira.LogFieldNameTriggerName, pkg.Trigger.Name) 17 | } 18 | 19 | if pkg.Contact.User != "" { 20 | logger. 21 | String(moira.LogFieldNameContactUser, pkg.Contact.User) 22 | } 23 | 24 | if pkg.Contact.Team != "" { 25 | logger. 26 | String(moira.LogFieldNameContactTeam, pkg.Contact.Team) 27 | } 28 | 29 | SetLogLevelByConfig(config.LogContactsToLevel, pkg.Contact.ID, &logger) 30 | 31 | return logger 32 | } 33 | 34 | func SetLogLevelByConfig(entityToLevel map[string]string, entityID string, logger *moira.Logger) { 35 | if v, ok := entityToLevel[entityID]; ok { 36 | if _, err := (*logger).Level(v); err != nil { 37 | (*logger).Warning(). 38 | Error(err). 39 | Msg("Couldn't set log level") 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /metric_source/retries/backoff_factory.go: -------------------------------------------------------------------------------- 1 | package retries 2 | 3 | import "github.com/cenkalti/backoff/v4" 4 | 5 | // BackoffFactory is used for creating backoff. It is expected that all backoffs created with one factory instance 6 | // have the same behaviour. 7 | type BackoffFactory interface { 8 | NewBackOff() backoff.BackOff 9 | } 10 | 11 | // ExponentialBackoffFactory is a factory that generates exponential backoffs based on given config. 12 | type ExponentialBackoffFactory struct { 13 | config Config 14 | } 15 | 16 | // NewExponentialBackoffFactory creates new BackoffFactory which will generate exponential backoffs. 17 | func NewExponentialBackoffFactory(config Config) BackoffFactory { 18 | return ExponentialBackoffFactory{ 19 | config: config, 20 | } 21 | } 22 | 23 | // NewBackOff creates new backoff. 24 | func (factory ExponentialBackoffFactory) NewBackOff() backoff.BackOff { 25 | backoffPolicy := backoff.NewExponentialBackOff( 26 | backoff.WithInitialInterval(factory.config.InitialInterval), 27 | backoff.WithRandomizationFactor(factory.config.RandomizationFactor), 28 | backoff.WithMultiplier(factory.config.Multiplier), 29 | backoff.WithMaxInterval(factory.config.MaxInterval), 30 | backoff.WithMaxElapsedTime(factory.config.MaxElapsedTime)) 31 | 32 | if factory.config.MaxRetriesCount > 0 { 33 | return backoff.WithMaxRetries(backoffPolicy, factory.config.MaxRetriesCount) 34 | } 35 | 36 | return backoffPolicy 37 | } 38 | -------------------------------------------------------------------------------- /senders/pagerduty/init.go: -------------------------------------------------------------------------------- 1 | package pagerduty 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/mitchellh/mapstructure" 8 | "github.com/moira-alert/moira" 9 | "github.com/moira-alert/moira/senders" 10 | ) 11 | 12 | // Structure that represents the PagerDuty configuration in the YAML file. 13 | type config struct { 14 | FrontURI string `mapstructure:"front_uri"` 15 | } 16 | 17 | // Sender implements moira sender interface for pagerduty. 18 | type Sender struct { 19 | ImageStores map[string]moira.ImageStore 20 | imageStoreID string 21 | imageStore moira.ImageStore 22 | imageStoreConfigured bool 23 | logger moira.Logger 24 | frontURI string 25 | location *time.Location 26 | } 27 | 28 | // Init loads yaml config, configures the pagerduty client. 29 | func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, location *time.Location, dateTimeFormat string) error { 30 | var cfg config 31 | 32 | err := mapstructure.Decode(senderSettings, &cfg) 33 | if err != nil { 34 | return fmt.Errorf("failed to decode senderSettings to pagerduty config: %w", err) 35 | } 36 | 37 | sender.frontURI = cfg.FrontURI 38 | 39 | sender.imageStoreID, sender.imageStore, sender.imageStoreConfigured = senders.ReadImageStoreConfig(senderSettings, sender.ImageStores, logger) 40 | 41 | sender.logger = logger 42 | sender.location = location 43 | 44 | return nil 45 | } 46 | -------------------------------------------------------------------------------- /metric_source/prometheus/prometheus.go: -------------------------------------------------------------------------------- 1 | package prometheus 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/moira-alert/moira" 8 | metricSource "github.com/moira-alert/moira/metric_source" 9 | ) 10 | 11 | const StepTimeSeconds int64 = 60 12 | 13 | var ErrPrometheusStorageDisabled = fmt.Errorf("remote prometheus storage is not enabled") 14 | 15 | type Config struct { 16 | CheckInterval time.Duration 17 | MetricsTTL time.Duration 18 | RequestTimeout time.Duration 19 | Retries int 20 | RetryTimeout time.Duration 21 | URL string 22 | User string 23 | Password string 24 | } 25 | 26 | func Create(config *Config, logger moira.Logger) (metricSource.MetricSource, error) { 27 | promApi, err := createPrometheusApi(config) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | return &Prometheus{config: config, api: promApi, logger: logger}, nil 33 | } 34 | 35 | type Prometheus struct { 36 | config *Config 37 | logger moira.Logger 38 | api PrometheusApi 39 | } 40 | 41 | func (prometheus *Prometheus) GetMetricsTTLSeconds() int64 { 42 | return int64(prometheus.config.MetricsTTL.Seconds()) 43 | } 44 | 45 | func (prometheus *Prometheus) IsConfigured() (bool, error) { 46 | return true, nil 47 | } 48 | 49 | func (prometheus *Prometheus) IsAvailable() (bool, error) { 50 | now := time.Now().Unix() 51 | _, err := prometheus.Fetch("1", now, now, true) 52 | 53 | return err == nil, err 54 | } 55 | -------------------------------------------------------------------------------- /api/dto/tag.go: -------------------------------------------------------------------------------- 1 | // nolint 2 | package dto 3 | 4 | import ( 5 | "net/http" 6 | 7 | "github.com/moira-alert/moira" 8 | ) 9 | 10 | type TagsData struct { 11 | TagNames []string `json:"list" example:"cpu" binding:"required"` 12 | } 13 | 14 | // Render is a function that implements chi Renderer interface for TagsData. 15 | func (*TagsData) Render(w http.ResponseWriter, r *http.Request) error { 16 | return nil 17 | } 18 | 19 | // Bind is a method that implements Binder interface from chi and checks that validity of data in request. 20 | func (tags *TagsData) Bind(request *http.Request) error { 21 | tags.TagNames = normalizeTags(tags.TagNames) 22 | return nil 23 | } 24 | 25 | type MessageResponse struct { 26 | Message string `json:"message" example:"tag deleted" binding:"required"` 27 | } 28 | 29 | func (*MessageResponse) Render(w http.ResponseWriter, r *http.Request) error { 30 | return nil 31 | } 32 | 33 | type TagsStatistics struct { 34 | List []TagStatistics `json:"list" binding:"required"` 35 | } 36 | 37 | type TagStatistics struct { 38 | TagName string `json:"name" example:"cpu" binding:"required"` 39 | Triggers []string `json:"triggers" example:"bcba82f5-48cf-44c0-b7d6-e1d32c64a88c" binding:"required"` 40 | Subscriptions []moira.SubscriptionData `json:"subscriptions" binding:"required"` 41 | } 42 | 43 | func (*TagsStatistics) Render(w http.ResponseWriter, r *http.Request) error { 44 | return nil 45 | } 46 | -------------------------------------------------------------------------------- /templating/trigger.go: -------------------------------------------------------------------------------- 1 | package templating 2 | 3 | type trigger struct { 4 | Name string 5 | } 6 | 7 | // Event represents a template event with fields allowed for use in templates. 8 | type Event struct { 9 | Metric string 10 | MetricElements []string 11 | Timestamp int64 12 | Value *float64 13 | State string 14 | } 15 | 16 | // TimestampDecrease decreases the timestamp of the event by the given number of seconds. 17 | func (event Event) TimestampDecrease(second int64) int64 { 18 | return event.Timestamp - second 19 | } 20 | 21 | // TimestampIncrease increases the timestamp of the event by the given number of seconds. 22 | func (event Event) TimestampIncrease(second int64) int64 { 23 | return event.Timestamp + second 24 | } 25 | 26 | type triggerDescriptionPopulater struct { 27 | Trigger *trigger 28 | Events []Event 29 | } 30 | 31 | // NewTriggerDescriptionPopulater creates a new trigger description populater with the given trigger name and template events. 32 | func NewTriggerDescriptionPopulater(triggerName string, events []Event) *triggerDescriptionPopulater { 33 | return &triggerDescriptionPopulater{ 34 | Trigger: &trigger{ 35 | Name: triggerName, 36 | }, 37 | Events: events, 38 | } 39 | } 40 | 41 | // Populate populates the given template with trigger description data. 42 | func (templateData *triggerDescriptionPopulater) Populate(tmpl string) (string, error) { 43 | return populate(tmpl, templateData) 44 | } 45 | -------------------------------------------------------------------------------- /senders/read_image_store_config.go: -------------------------------------------------------------------------------- 1 | package senders 2 | 3 | import ( 4 | "github.com/moira-alert/moira" 5 | ) 6 | 7 | // ReadImageStoreConfig reads the image store config for a sender 8 | // from its settings and confirms whether that image store 9 | // is configured. 10 | func ReadImageStoreConfig(senderSettings interface{}, imageStores map[string]moira.ImageStore, logger moira.Logger) (string, moira.ImageStore, bool) { 11 | settings, ok := senderSettings.(map[string]interface{}) 12 | if !ok { 13 | logger.Warning().Msg("Failed conversion of senderSettings type to map[string]interface{}") 14 | return "", nil, false 15 | } 16 | 17 | IimageStoreID, ok := settings["image_store"] 18 | if !ok { 19 | logger.Warning().Msg("Cannot read image_store from the config, will not be able to attach plot images to alerts") 20 | return "", nil, false 21 | } 22 | 23 | imageStoreID, ok := IimageStoreID.(string) 24 | if !ok { 25 | logger.Warning().Msg("Failed to retrieve image_store from sender settings") 26 | return "", nil, false 27 | } 28 | 29 | imageStore, ok := imageStores[imageStoreID] 30 | imageStoreConfigured := false 31 | 32 | if ok && imageStore.IsEnabled() { 33 | imageStoreConfigured = true 34 | } else { 35 | logger.Warning(). 36 | String("image_store_id", imageStoreID). 37 | Msg("Image store specified has not been configured") 38 | 39 | return "", nil, imageStoreConfigured 40 | } 41 | 42 | return imageStoreID, imageStore, imageStoreConfigured 43 | } 44 | -------------------------------------------------------------------------------- /database/redis/reply/contact.go: -------------------------------------------------------------------------------- 1 | package reply 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/moira-alert/moira/database" 9 | 10 | "github.com/go-redis/redis/v8" 11 | "github.com/moira-alert/moira" 12 | ) 13 | 14 | func unmarshalContact(bytes []byte, err error) (moira.ContactData, error) { 15 | contact := moira.ContactData{} 16 | 17 | if err != nil { 18 | if errors.Is(err, redis.Nil) { 19 | return contact, database.ErrNil 20 | } 21 | 22 | return contact, fmt.Errorf("failed to read contact: %s", err.Error()) 23 | } 24 | 25 | err = json.Unmarshal(bytes, &contact) 26 | if err != nil { 27 | return contact, fmt.Errorf("failed to parse contact json %s: %s", string(bytes), err.Error()) 28 | } 29 | 30 | return contact, nil 31 | } 32 | 33 | // Contact converts redis DB reply to moira.ContactData object. 34 | func Contact(rep *redis.StringCmd) (moira.ContactData, error) { 35 | return unmarshalContact(rep.Bytes()) 36 | } 37 | 38 | // Contacts converts redis DB reply to moira.ContactData objects array. 39 | func Contacts(rep []*redis.StringCmd) ([]*moira.ContactData, error) { 40 | contacts := make([]*moira.ContactData, len(rep)) 41 | 42 | for i, value := range rep { 43 | contact, err := unmarshalContact(value.Bytes()) 44 | if err != nil && !errors.Is(err, database.ErrNil) { 45 | return nil, err 46 | } 47 | 48 | if errors.Is(err, database.ErrNil) { 49 | contacts[i] = nil 50 | } else { 51 | contacts[i] = &contact 52 | } 53 | } 54 | 55 | return contacts, nil 56 | } 57 | -------------------------------------------------------------------------------- /senders/webhook/request.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | 10 | "github.com/moira-alert/moira" 11 | ) 12 | 13 | func buildRequest( 14 | logger moira.Logger, 15 | method string, 16 | requestURL string, 17 | body []byte, 18 | user string, 19 | password string, 20 | headers map[string]string, 21 | ) (*http.Request, error) { 22 | request, err := http.NewRequestWithContext(context.TODO(), method, requestURL, bytes.NewBuffer(body)) 23 | if err != nil { 24 | return request, err 25 | } 26 | 27 | if user != "" && password != "" { 28 | request.SetBasicAuth(user, password) 29 | } 30 | 31 | for k, v := range headers { 32 | request.Header.Set(k, v) 33 | } 34 | 35 | logger.Debug(). 36 | String("method", request.Method). 37 | String("url", request.URL.String()). 38 | String("body", string(body)). 39 | Msg("Created request") 40 | 41 | return request, nil 42 | } 43 | 44 | func performRequest(client *http.Client, request *http.Request) (int, []byte, error) { 45 | rsp, err := client.Do(request) 46 | if err != nil { 47 | return 0, nil, fmt.Errorf("failed to perform request: %w", err) 48 | } 49 | defer rsp.Body.Close() 50 | 51 | bodyBytes, err := io.ReadAll(rsp.Body) 52 | if err != nil { 53 | return 0, nil, fmt.Errorf("failed to read response body: %w", err) 54 | } 55 | 56 | return rsp.StatusCode, bodyBytes, nil 57 | } 58 | 59 | func isAllowedResponseCode(responseCode int) bool { 60 | return (responseCode >= http.StatusOK) && (responseCode < http.StatusMultipleChoices) 61 | } 62 | -------------------------------------------------------------------------------- /database/redis/bot.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/go-redis/redis/v8" 9 | "github.com/moira-alert/moira/database" 10 | ) 11 | 12 | // GetChatByUsername read chat of user by messenger username. 13 | func (connector *DbConnector) GetChatByUsername(messenger, username string) (string, error) { 14 | if strings.HasPrefix(username, "#") { 15 | result := "@" + username[1:] 16 | return result, nil 17 | } 18 | 19 | c := *connector.client 20 | 21 | result, err := c.Get(connector.context, usernameKey(messenger, username)).Result() 22 | if errors.Is(err, redis.Nil) { 23 | return result, database.ErrNil 24 | } 25 | 26 | return result, err 27 | } 28 | 29 | // SetUsernameChat store id of username. 30 | func (connector *DbConnector) SetUsernameChat(messenger, username, chatRaw string) error { 31 | c := *connector.client 32 | err := c.Set(connector.context, usernameKey(messenger, username), chatRaw, redis.KeepTTL).Err() 33 | 34 | return err 35 | } 36 | 37 | // RemoveUser removes username from messenger data. 38 | func (connector *DbConnector) RemoveUser(messenger, username string) error { 39 | c := *connector.client 40 | 41 | err := c.Del(connector.context, usernameKey(messenger, username)).Err() 42 | if err != nil { 43 | return fmt.Errorf("failed to delete username '%s' from messenger '%s', error: %s", username, messenger, err.Error()) 44 | } 45 | 46 | return nil 47 | } 48 | 49 | func usernameKey(messenger, username string) string { 50 | return fmt.Sprintf("moira-%s-users:%s", messenger, username) 51 | } 52 | -------------------------------------------------------------------------------- /notifier/selfstate/heartbeat/database.go: -------------------------------------------------------------------------------- 1 | package heartbeat 2 | 3 | import "github.com/moira-alert/moira" 4 | 5 | type databaseHeartbeat struct{ heartbeat } 6 | 7 | func GetDatabase(delay, lastSuccessfulCheck int64, checkTags []string, logger moira.Logger, database moira.Database) Heartbeater { 8 | if delay > 0 { 9 | return &databaseHeartbeat{heartbeat{ 10 | logger: logger, 11 | database: database, 12 | delay: delay, 13 | lastSuccessfulCheck: lastSuccessfulCheck, 14 | checkTags: checkTags, 15 | }} 16 | } 17 | 18 | return nil 19 | } 20 | 21 | func (check *databaseHeartbeat) Check(nowTS int64) (int64, bool, error) { 22 | _, err := check.database.GetChecksUpdatesCount() 23 | if err == nil { 24 | check.lastSuccessfulCheck = nowTS 25 | return 0, false, nil 26 | } 27 | 28 | if check.lastSuccessfulCheck < nowTS-check.delay { 29 | check.logger.Error(). 30 | String("error", check.GetErrorMessage()). 31 | Int64("time_since_successful_check", nowTS-check.heartbeat.lastSuccessfulCheck). 32 | Msg("Send message") 33 | 34 | return nowTS - check.lastSuccessfulCheck, true, nil 35 | } 36 | 37 | return 0, false, nil 38 | } 39 | 40 | func (databaseHeartbeat) NeedTurnOffNotifier() bool { 41 | return true 42 | } 43 | 44 | func (databaseHeartbeat) NeedToCheckOthers() bool { 45 | return false 46 | } 47 | 48 | func (databaseHeartbeat) GetErrorMessage() string { 49 | return "Redis disconnected" 50 | } 51 | 52 | func (check *databaseHeartbeat) GetCheckTags() CheckTags { 53 | return check.checkTags 54 | } 55 | -------------------------------------------------------------------------------- /senders/twilio/voice.go: -------------------------------------------------------------------------------- 1 | package twilio 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | 7 | twilio_client "github.com/carlosdp/twiliogo" 8 | "github.com/moira-alert/moira" 9 | ) 10 | 11 | const twimletsEchoURL = "https://twimlets.com/echo?Twiml=" 12 | 13 | type twilioSenderVoice struct { 14 | twilioSender 15 | 16 | voiceURL string 17 | appendMessage bool 18 | twimletsEcho bool 19 | } 20 | 21 | func (sender *twilioSenderVoice) SendEvents(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plots [][]byte, throttled bool) error { 22 | voiceURL := sender.buildVoiceURL(trigger) 23 | 24 | twilioCall, err := twilio_client.NewCall(sender.client, sender.APIFromPhone, contact.Value, twilio_client.Callback(voiceURL)) 25 | if err != nil { 26 | return fmt.Errorf("failed to make call to contact %s: %s", contact.Value, err.Error()) 27 | } 28 | 29 | sender.logger.Debug(). 30 | String("status", twilioCall.Status). 31 | String("callback_url", voiceURL). 32 | Msg("Call queued to twilio") 33 | 34 | return nil 35 | } 36 | 37 | func (sender *twilioSenderVoice) buildVoiceURL(trigger moira.TriggerData) string { 38 | message := fmt.Sprintf("Hi! This is a notification for Moira trigger %s. Please, visit Moira web interface for details.", trigger.Name) 39 | voiceURL := sender.voiceURL 40 | 41 | if sender.appendMessage { 42 | voiceURL += url.QueryEscape(message) 43 | } 44 | 45 | if sender.twimletsEcho { 46 | voiceURL = twimletsEchoURL 47 | voiceURL += url.QueryEscape(fmt.Sprintf("%s", message)) 48 | } 49 | 50 | return voiceURL 51 | } 52 | -------------------------------------------------------------------------------- /.github/workflows/docker-nightly.yml: -------------------------------------------------------------------------------- 1 | name: Publish docker nightly 2 | on: 3 | push: 4 | branches: 5 | - "master" 6 | 7 | jobs: 8 | publish: 9 | name: Publish images 10 | runs-on: ubuntu-22.04 11 | strategy: 12 | matrix: 13 | services: [api, checker, cli, notifier, filter] 14 | steps: 15 | 16 | - name: Set up Docker Buildx 17 | uses: docker/setup-buildx-action@v2 18 | 19 | - uses: docker/login-action@v2 20 | name: Login to DockerHub 21 | with: 22 | username: ${{ secrets.DOCKERHUB_USERNAME }} 23 | password: ${{ secrets.DOCKERHUB_PASSWORD }} 24 | 25 | - name: Build docker tag 26 | run: echo "DOCKER_TAG=$(echo $(date '+%Y-%m-%d').${GITHUB_SHA:0:7})" >> $GITHUB_ENV 27 | 28 | - name: Build and push 29 | uses: docker/build-push-action@v4 30 | with: 31 | file: ./Dockerfile.${{matrix.services}} 32 | build-args: | 33 | MoiraVersion=${{env.DOCKER_TAG}} 34 | GIT_COMMIT=${{github.sha}} 35 | push: true 36 | tags: moira/${{matrix.services}}-nightly:${{env.DOCKER_TAG}},moira/${{matrix.services}}-nightly:latest 37 | 38 | - name: Comment PR with build tag 39 | uses: mshick/add-pr-comment@v2 40 | if: always() 41 | with: 42 | refresh-message-position: true 43 | message-success: 44 | "Build and push Docker images with tag: ${{env.DOCKER_TAG}}" 45 | message-failure: 46 | "Builds images failed. See action log for details" 47 | 48 | -------------------------------------------------------------------------------- /cmd/cli/metrics.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/moira-alert/moira" 7 | ) 8 | 9 | func handleCleanUpOutdatedMetrics(config cleanupConfig, database moira.Database) error { 10 | duration, err := time.ParseDuration(config.CleanupMetricsDuration) 11 | if err != nil { 12 | return err 13 | } 14 | 15 | if err = database.CleanUpOutdatedMetrics(duration); err != nil { 16 | return err 17 | } 18 | 19 | return nil 20 | } 21 | 22 | func handleCleanUpFutureMetrics(config cleanupConfig, database moira.Database) error { 23 | duration, err := time.ParseDuration(config.CleanupFutureMetricsDuration) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | if err = database.CleanUpFutureMetrics(duration); err != nil { 29 | return err 30 | } 31 | 32 | return nil 33 | } 34 | 35 | func handleCleanUpAbandonedRetentions(database moira.Database) error { 36 | return database.CleanUpAbandonedRetentions() 37 | } 38 | 39 | func handleCleanUpAbandonedTriggerLastCheck(database moira.Database) error { 40 | return database.CleanUpAbandonedTriggerLastCheck() 41 | } 42 | 43 | func handleCleanUpAbandonedTags(database moira.Database) (int, error) { 44 | return database.CleanUpAbandonedTags() 45 | } 46 | 47 | func handleRemoveMetricsByPrefix(database moira.Database, prefix string) error { 48 | return database.RemoveMetricsByPrefix(prefix) 49 | } 50 | 51 | func handleRemoveAllMetrics(database moira.Database) error { 52 | return database.RemoveAllMetrics() 53 | } 54 | 55 | func handleCleanUpOutdatedPatternMetrics(database moira.Database) (int64, error) { 56 | return database.CleanupOutdatedPatternMetrics() 57 | } 58 | -------------------------------------------------------------------------------- /mock/moira-alert/metrics/meter.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: github.com/moira-alert/moira/metrics (interfaces: Meter) 3 | // 4 | // Generated by this command: 5 | // 6 | // mockgen -destination=mock/moira-alert/metrics/meter.go -package=mock_moira_alert github.com/moira-alert/moira/metrics Meter 7 | // 8 | 9 | // Package mock_moira_alert is a generated GoMock package. 10 | package mock_moira_alert 11 | 12 | import ( 13 | reflect "reflect" 14 | 15 | gomock "go.uber.org/mock/gomock" 16 | ) 17 | 18 | // MockMeter is a mock of Meter interface. 19 | type MockMeter struct { 20 | ctrl *gomock.Controller 21 | recorder *MockMeterMockRecorder 22 | isgomock struct{} 23 | } 24 | 25 | // MockMeterMockRecorder is the mock recorder for MockMeter. 26 | type MockMeterMockRecorder struct { 27 | mock *MockMeter 28 | } 29 | 30 | // NewMockMeter creates a new mock instance. 31 | func NewMockMeter(ctrl *gomock.Controller) *MockMeter { 32 | mock := &MockMeter{ctrl: ctrl} 33 | mock.recorder = &MockMeterMockRecorder{mock} 34 | return mock 35 | } 36 | 37 | // EXPECT returns an object that allows the caller to indicate expected use. 38 | func (m *MockMeter) EXPECT() *MockMeterMockRecorder { 39 | return m.recorder 40 | } 41 | 42 | // Mark mocks base method. 43 | func (m *MockMeter) Mark(arg0 int64) { 44 | m.ctrl.T.Helper() 45 | m.ctrl.Call(m, "Mark", arg0) 46 | } 47 | 48 | // Mark indicates an expected call of Mark. 49 | func (mr *MockMeterMockRecorder) Mark(arg0 any) *gomock.Call { 50 | mr.mock.ctrl.T.Helper() 51 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Mark", reflect.TypeOf((*MockMeter)(nil).Mark), arg0) 52 | } 53 | -------------------------------------------------------------------------------- /database/redis/config.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import "time" 4 | 5 | // DatabaseConfig - Redis database connection config. 6 | type DatabaseConfig struct { 7 | Addrs []string 8 | 9 | Username string 10 | Password string 11 | 12 | MasterName string 13 | SentinelPassword string 14 | SentinelUsername string 15 | 16 | MetricsTTL time.Duration 17 | DialTimeout time.Duration 18 | ReadTimeout time.Duration 19 | WriteTimeout time.Duration 20 | 21 | MaxRedirects int 22 | MaxRetries int 23 | MinRetryBackoff time.Duration 24 | MaxRetryBackoff time.Duration 25 | 26 | PoolTimeout time.Duration 27 | PoolSize int 28 | 29 | ReadOnly bool 30 | RouteByLatency bool 31 | RouteRandomly bool 32 | } 33 | 34 | type NotificationHistoryConfig struct { 35 | NotificationHistoryTTL time.Duration 36 | } 37 | 38 | // NotificationConfig configuration in redis. 39 | type NotificationConfig struct { 40 | // Need to determine if notification is delayed - the difference between creation time and sending time 41 | // is greater than DelayedTime 42 | DelayedTime time.Duration 43 | // TransactionTimeout defines the timeout between fetch notifications transactions 44 | TransactionTimeout time.Duration 45 | // TransactionMaxRetries defines the maximum number of attempts to make a transaction 46 | TransactionMaxRetries int 47 | // TransactionHeuristicLimit maximum allowable limit, after this limit all notifications 48 | // without limit will be taken 49 | TransactionHeuristicLimit int64 50 | // ResaveTime is the time by which the timestamp of notifications with triggers 51 | // or metrics on Maintenance is incremented 52 | ResaveTime time.Duration 53 | } 54 | -------------------------------------------------------------------------------- /local/checker.yml: -------------------------------------------------------------------------------- 1 | #See https://moira.readthedocs.io/en/latest/installation/configuration.html for config explanation 2 | redis: 3 | addrs: "redis:6379" 4 | metrics_ttl: 3h 5 | telemetry: 6 | graphite: 7 | enabled: true 8 | runtime_stats: true 9 | uri: "relay:2003" 10 | prefix: moira 11 | interval: 60s 12 | pprof: 13 | enabled: true 14 | listen: ":8092" 15 | local: 16 | check_interval: 60s 17 | graphite_remote: 18 | - cluster_id: default 19 | cluster_name: Graphite 1 20 | url: "http://graphite:80/render" 21 | check_interval: 60s 22 | metrics_ttl: 168h 23 | timeout: 60s 24 | retries: 25 | initial_interval: 60s 26 | randomization_factor: 0.5 27 | multiplier: 1.5 28 | max_interval: 120s 29 | max_retries_count: 3 30 | health_check_timeout: 6s 31 | health_check_retries: 32 | initial_interval: 20s 33 | randomization_factor: 0.5 34 | multiplier: 1.5 35 | max_interval: 80s 36 | max_retries_count: 3 37 | prometheus_remote: 38 | - cluster_id: default 39 | cluster_name: Prometheus 1 40 | url: "http://prometheus:9090" 41 | check_interval: 60s 42 | timeout: 60s 43 | metrics_ttl: 168h 44 | - cluster_id: staging 45 | cluster_name: Prometheus 2 46 | url: "http://prometheus_2:9090" 47 | check_interval: 60s 48 | timeout: 60s 49 | metrics_ttl: 168h 50 | retries: 5 51 | retry_timeout: 15s 52 | checker: 53 | nodata_check_interval: 60s 54 | check_interval: 10s 55 | stop_checking_interval: 3600s 56 | lazy_triggers_check_interval: 60s 57 | log: 58 | log_file: stdout 59 | log_level: debug 60 | log_pretty_format: true 61 | -------------------------------------------------------------------------------- /senders/emoji_provider/provider.go: -------------------------------------------------------------------------------- 1 | package emoji_provider 2 | 3 | import ( 4 | "fmt" 5 | "maps" 6 | 7 | "github.com/moira-alert/moira" 8 | ) 9 | 10 | var defaultStateEmoji = map[moira.State]string{ 11 | moira.StateOK: ":moira-state-ok:", 12 | moira.StateWARN: ":moira-state-warn:", 13 | moira.StateERROR: ":moira-state-error:", 14 | moira.StateNODATA: ":moira-state-nodata:", 15 | moira.StateEXCEPTION: ":moira-state-exception:", 16 | moira.StateTEST: ":moira-state-test:", 17 | } 18 | 19 | // emojiProvider is struct for get emoji by trigger State. 20 | type emojiProvider struct { 21 | defaultValue string 22 | stateEmojiMap map[moira.State]string 23 | } 24 | 25 | // StateEmojiGetter is interface for emojiProvider. 26 | type StateEmojiGetter interface { 27 | GetStateEmoji(subjectState moira.State) string 28 | } 29 | 30 | // NewEmojiProvider is construct for emojiProvider. 31 | func NewEmojiProvider(defaultValue string, stateEmojiMap map[string]string) (StateEmojiGetter, error) { 32 | emojiMap := maps.Clone(defaultStateEmoji) 33 | 34 | for state, emoji := range stateEmojiMap { 35 | converted := moira.State(state) 36 | if _, ok := emojiMap[converted]; !ok { 37 | return nil, fmt.Errorf("undefined Moira's state: %s", state) 38 | } 39 | 40 | emojiMap[converted] = emoji 41 | } 42 | 43 | return &emojiProvider{ 44 | defaultValue: defaultValue, 45 | stateEmojiMap: emojiMap, 46 | }, nil 47 | } 48 | 49 | // GetStateEmoji returns corresponding state emoji. 50 | func (em *emojiProvider) GetStateEmoji(subjectState moira.State) string { 51 | if emoji, ok := em.stateEmojiMap[subjectState]; ok { 52 | return emoji 53 | } 54 | 55 | return em.defaultValue 56 | } 57 | -------------------------------------------------------------------------------- /senders/selfstate/selfstate.go: -------------------------------------------------------------------------------- 1 | package selfstate 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/moira-alert/moira" 8 | ) 9 | 10 | // Sender implements moira sender interface via selfstate. 11 | type Sender struct { 12 | Database moira.Database 13 | logger moira.Logger 14 | } 15 | 16 | // Init read yaml config. 17 | func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, location *time.Location, dateTimeFormat string) error { 18 | sender.logger = logger 19 | return nil 20 | } 21 | 22 | // SendEvents implements Sender interface Send. 23 | func (sender *Sender) SendEvents(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plots [][]byte, throttled bool) error { 24 | selfState, err := sender.Database.GetNotifierStateForSource(moira.DefaultLocalCluster) 25 | if err != nil { 26 | return fmt.Errorf("failed to get notifier state: %s", err.Error()) 27 | } 28 | 29 | state := events.GetCurrentState(throttled) 30 | 31 | switch state { 32 | case moira.StateTEST: 33 | sender.logger.Info(). 34 | String("notifier_state", selfState.State). 35 | Msg("Current notifier state") 36 | 37 | return nil 38 | case moira.StateOK, moira.StateEXCEPTION: 39 | sender.logger.Error(). 40 | String(moira.LogFieldNameTriggerID, trigger.ID). 41 | String("state", state.String()). 42 | Msg("State is ignorable") 43 | 44 | return nil 45 | default: 46 | if selfState.State != state.ToSelfState() { 47 | err := sender.Database.SetNotifierStateForSource(moira.DefaultLocalCluster, moira.SelfStateActorTrigger, moira.SelfStateERROR) 48 | if err != nil { 49 | return fmt.Errorf("failed to disable notifications: %s", err.Error()) 50 | } 51 | } 52 | } 53 | 54 | return nil 55 | } 56 | -------------------------------------------------------------------------------- /checker/metrics/conversion/set_helper.go: -------------------------------------------------------------------------------- 1 | package conversion 2 | 3 | var void struct{} = struct{}{} 4 | 5 | // set[string] is a map that represents a set of strings with corresponding methods. 6 | type set[K comparable] map[K]struct{} 7 | 8 | func (set set[K]) contains(key K) bool { 9 | _, ok := set[key] 10 | return ok 11 | } 12 | 13 | func (set set[K]) insert(key K) { 14 | set[key] = void 15 | } 16 | 17 | func newSet[K comparable](value map[K]bool) set[K] { 18 | res := make(set[K], len(value)) 19 | 20 | for k, v := range value { 21 | if v { 22 | res.insert(k) 23 | } 24 | } 25 | 26 | return res 27 | } 28 | 29 | // newSetFromTriggerTargetMetrics is a constructor function for setHelper. 30 | func newSetFromTriggerTargetMetrics(metrics TriggerTargetMetrics) set[string] { 31 | result := make(set[string], len(metrics)) 32 | for metricName := range metrics { 33 | result.insert(metricName) 34 | } 35 | 36 | return result 37 | } 38 | 39 | // diff is a set relative complement operation that returns a new set with elements 40 | // that appear only in second set. 41 | func (self set[string]) diff(other set[string]) set[string] { 42 | result := make(set[string], len(self)) 43 | 44 | for metricName := range other { 45 | if !self.contains(metricName) { 46 | result.insert(metricName) 47 | } 48 | } 49 | 50 | return result 51 | } 52 | 53 | // union is a sets union operation that return a new set with elements from both sets. 54 | func (self set[string]) union(other set[string]) set[string] { 55 | result := make(set[string], len(self)+len(other)) 56 | 57 | for metricName := range self { 58 | result.insert(metricName) 59 | } 60 | 61 | for metricName := range other { 62 | result.insert(metricName) 63 | } 64 | 65 | return result 66 | } 67 | -------------------------------------------------------------------------------- /metric_source/remote/response.go: -------------------------------------------------------------------------------- 1 | package remote 2 | 3 | import ( 4 | "encoding/json" 5 | "math" 6 | 7 | metricSource "github.com/moira-alert/moira/metric_source" 8 | ) 9 | 10 | type graphiteMetric struct { 11 | Target string 12 | DataPoints [][2]*float64 13 | } 14 | 15 | func convertResponse(metricsData []metricSource.MetricData, allowRealTimeAlerting bool) FetchResult { 16 | if allowRealTimeAlerting { 17 | return FetchResult{MetricsData: metricsData} 18 | } 19 | 20 | result := make([]metricSource.MetricData, 0, len(metricsData)) 21 | 22 | for _, metricData := range metricsData { 23 | // remove last value 24 | metricData.Values = metricData.Values[:len(metricData.Values)-1] 25 | result = append(result, metricData) 26 | } 27 | 28 | return FetchResult{MetricsData: result} 29 | } 30 | 31 | func decodeBody(body []byte) ([]metricSource.MetricData, error) { 32 | var tmp []graphiteMetric 33 | 34 | err := json.Unmarshal(body, &tmp) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | res := make([]metricSource.MetricData, 0, len(tmp)) 40 | 41 | for _, m := range tmp { 42 | var stepTime int64 = 60 43 | if len(m.DataPoints) > 1 { 44 | stepTime = int64(*m.DataPoints[1][1] - *m.DataPoints[0][1]) 45 | } 46 | 47 | metricData := metricSource.MetricData{ 48 | Name: m.Target, 49 | StartTime: int64(*m.DataPoints[0][1]), 50 | StopTime: int64(*m.DataPoints[len(m.DataPoints)-1][1]), 51 | StepTime: stepTime, 52 | Values: make([]float64, len(m.DataPoints)), 53 | } 54 | 55 | for i, v := range m.DataPoints { 56 | if v[0] == nil { 57 | metricData.Values[i] = math.NaN() 58 | } else { 59 | metricData.Values[i] = *v[0] 60 | } 61 | } 62 | 63 | res = append(res, metricData) 64 | } 65 | 66 | return res, nil 67 | } 68 | -------------------------------------------------------------------------------- /senders/discord/init_test.go: -------------------------------------------------------------------------------- 1 | package discord 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | "time" 7 | 8 | "github.com/go-playground/validator/v10" 9 | "github.com/moira-alert/moira" 10 | 11 | logging "github.com/moira-alert/moira/logging/zerolog_adapter" 12 | . "github.com/smartystreets/goconvey/convey" 13 | ) 14 | 15 | type MockDB struct { 16 | moira.Database 17 | } 18 | type MockLock struct { 19 | moira.Lock 20 | } 21 | 22 | func (lock *MockLock) Acquire(stop <-chan struct{}) (lost <-chan struct{}, err error) { 23 | return lost, nil 24 | } 25 | 26 | func (db *MockDB) NewLock(name string, ttl time.Duration) moira.Lock { 27 | return &MockLock{} 28 | } 29 | 30 | func TestInit(t *testing.T) { 31 | logger, _ := logging.ConfigureLog("stdout", "debug", "test", true) 32 | location, _ := time.LoadLocation("UTC") 33 | 34 | Convey("Init tests", t, func() { 35 | sender := Sender{DataBase: &MockDB{}} 36 | 37 | validatorErr := validator.ValidationErrors{} 38 | 39 | Convey("With empty token", func() { 40 | senderSettings := map[string]interface{}{} 41 | 42 | err := sender.Init(senderSettings, logger, nil, "") 43 | So(errors.As(err, &validatorErr), ShouldBeTrue) 44 | So(sender, ShouldResemble, Sender{DataBase: &MockDB{}}) 45 | }) 46 | 47 | Convey("Has settings", func() { 48 | senderSettings := map[string]interface{}{ 49 | "token": "123", 50 | "front_uri": "http://moira.uri", 51 | } 52 | 53 | err := sender.Init(senderSettings, logger, location, "15:04") //nolint 54 | So(err, ShouldBeNil) 55 | So(sender.frontURI, ShouldResemble, "http://moira.uri") 56 | So(sender.session.Token, ShouldResemble, "Bot 123") 57 | So(sender.logger, ShouldResemble, logger) 58 | So(sender.location, ShouldResemble, location) 59 | }) 60 | }) 61 | } 62 | -------------------------------------------------------------------------------- /metrics/contacts.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import "regexp" 4 | 5 | var nonAllowedMetricCharsRegex = regexp.MustCompile("[^a-zA-Z0-9_]") 6 | 7 | // ContactsMetrics Collection of metrics for contacts counting. 8 | type ContactsMetrics struct { 9 | contactsCount map[string]Meter 10 | registry Registry 11 | attributedRegistry MetricRegistry 12 | } 13 | 14 | // NewContactsMetrics Creates and configurates the instance of ContactsMetrics. 15 | func NewContactsMetrics(registry Registry, attributedRegistry MetricRegistry) *ContactsMetrics { 16 | meters := make(map[string]Meter) 17 | 18 | return &ContactsMetrics{ 19 | contactsCount: meters, 20 | registry: registry, 21 | attributedRegistry: attributedRegistry, 22 | } 23 | } 24 | 25 | // replaceNonAllowedMetricCharacters replaces non-allowed characters in the given metric string with underscores. 26 | func (metrics *ContactsMetrics) replaceNonAllowedMetricCharacters(metric string) string { 27 | return nonAllowedMetricCharsRegex.ReplaceAllString(metric, "_") 28 | } 29 | 30 | // Mark Marks the number of contacts of different types. 31 | func (metrics *ContactsMetrics) Mark(contact string, count int64) error { 32 | if _, ok := metrics.contactsCount[contact]; !ok { 33 | metric := metrics.replaceNonAllowedMetricCharacters(contact) 34 | attributedRegistry := metrics.attributedRegistry.WithAttributes(Attributes{ 35 | Attribute{Key: "contact_type", Value: metric}, 36 | }) 37 | 38 | attributedGauge, err := attributedRegistry.NewGauge("contacts") 39 | if err != nil { 40 | return err 41 | } 42 | 43 | metrics.contactsCount[contact] = NewCompositeMeter(metrics.registry.NewMeter("contacts", metric), attributedGauge) 44 | } 45 | 46 | metrics.contactsCount[contact].Mark(count) 47 | 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /database/redis/throttling.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/go-redis/redis/v8" 8 | ) 9 | 10 | // GetTriggerThrottling get throttling or scheduled notifications delay for given triggerID. 11 | func (connector *DbConnector) GetTriggerThrottling(triggerID string) (time.Time, time.Time) { 12 | c := *connector.client 13 | 14 | next, _ := c.Get(connector.context, notifierNextKey(triggerID)).Int64() 15 | beginning, _ := c.Get(connector.context, notifierThrottlingBeginningKey(triggerID)).Int64() 16 | 17 | return time.Unix(next, 0), time.Unix(beginning, 0) 18 | } 19 | 20 | // SetTriggerThrottling store throttling or scheduled notifications delay for given triggerID. 21 | func (connector *DbConnector) SetTriggerThrottling(triggerID string, next time.Time) error { 22 | c := *connector.client 23 | err := c.Set(connector.context, notifierNextKey(triggerID), next.Unix(), redis.KeepTTL).Err() 24 | 25 | return err 26 | } 27 | 28 | // DeleteTriggerThrottling deletes throttling and scheduled notifications delay for given triggerID. 29 | func (connector *DbConnector) DeleteTriggerThrottling(triggerID string) error { 30 | c := *connector.client 31 | 32 | pipe := c.TxPipeline() 33 | pipe.Set(connector.context, notifierThrottlingBeginningKey(triggerID), time.Now().Unix(), redis.KeepTTL) 34 | pipe.Del(connector.context, notifierNextKey(triggerID)) 35 | 36 | _, err := pipe.Exec(connector.context) 37 | if err != nil { 38 | return fmt.Errorf("failed to EXEC: %s", err.Error()) 39 | } 40 | 41 | return nil 42 | } 43 | 44 | func notifierThrottlingBeginningKey(triggerID string) string { 45 | return "moira-notifier-throttling-beginning:" + triggerID 46 | } 47 | 48 | func notifierNextKey(triggerID string) string { 49 | return "moira-notifier-next:" + triggerID 50 | } 51 | -------------------------------------------------------------------------------- /plotting/fonts/ttftogofile/ttf2gofile.go: -------------------------------------------------------------------------------- 1 | // convert file to byte array and write to variable go file. 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "io" 7 | "os" 8 | "strings" 9 | 10 | "golang.org/x/text/cases" 11 | "golang.org/x/text/language" 12 | ) 13 | 14 | func usage() { 15 | fmt.Println("USAGE:") 16 | fmt.Println("> ttf2GoFile ") 17 | } 18 | 19 | // reade file to byte array. 20 | func fileBytes(filePath string) ([]byte, error) { 21 | var err error 22 | 23 | f, err := os.Open(filePath) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | defer f.Close() 29 | 30 | return io.ReadAll(f) 31 | } 32 | 33 | // write to variable go file. 34 | func createGoFile(fileName string, dataTTF []byte) error { 35 | f, err := os.Create(strings.ToLower(fileName) + ".go") 36 | if err != nil { 37 | return err 38 | } 39 | defer f.Close() 40 | 41 | _, err = fmt.Fprintf(f, "package fonts\n\nvar %s = %#v\n", 42 | cases.Title(language.Und).String(fileName), dataTTF) 43 | 44 | return err 45 | } 46 | 47 | func main() { 48 | if len(os.Args) < 2 { //nolint 49 | usage() 50 | os.Exit(1) 51 | } 52 | 53 | file := os.Args[1] 54 | 55 | fmt.Println(file[len(file)-5:]) 56 | 57 | if strings.ToLower(file[len(file)-5:]) != ".ttf" { 58 | fmt.Printf("File name %s is not font\n", file) 59 | os.Exit(1) 60 | } 61 | 62 | fmt.Printf("Reading %s\n", file) 63 | 64 | dataTTF, err := fileBytes(file) 65 | if err != nil { 66 | fmt.Printf("Error opening file: %v\n", err) 67 | os.Exit(1) 68 | } 69 | 70 | if len(dataTTF) == 0 { 71 | fmt.Printf("File was empty.\n") 72 | os.Exit(1) 73 | } 74 | 75 | fmt.Printf("Create file: %s.go\n", file[:len(file)-4]) 76 | 77 | err = createGoFile(file[:len(file)-4], dataTTF) 78 | if err != nil { 79 | fmt.Println("Go file not created") 80 | os.Exit(1) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /database/redis/reply/metric.go: -------------------------------------------------------------------------------- 1 | package reply 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/go-redis/redis/v8" 10 | "github.com/moira-alert/moira" 11 | ) 12 | 13 | // MetricValues converts redis DB reply struct "RetentionTimestamp Value" "Timestamp" to moira.MetricValue object. 14 | func MetricValues(values *redis.ZSliceCmd) ([]*moira.MetricValue, error) { 15 | resultByMetricArr, err := values.Result() 16 | if err != nil { 17 | if errors.Is(err, redis.Nil) { 18 | return make([]*moira.MetricValue, 0), nil 19 | } 20 | 21 | return nil, fmt.Errorf("failed to read metricValues: %s", err.Error()) 22 | } 23 | 24 | metricsValues := make([]*moira.MetricValue, 0, len(resultByMetricArr)) 25 | 26 | for i := 0; i < len(resultByMetricArr); i++ { 27 | val := resultByMetricArr[i].Member.(string) 28 | valuesArr := strings.Split(val, " ") 29 | 30 | if len(valuesArr) != 2 { 31 | return nil, fmt.Errorf("value format is not valid: %s", val) 32 | } 33 | 34 | timestamp, err := strconv.ParseInt(valuesArr[0], 10, 64) 35 | if err != nil { 36 | return nil, fmt.Errorf("metric timestamp format is not valid: %s", err.Error()) 37 | } 38 | 39 | value, err := strconv.ParseFloat(valuesArr[1], 64) 40 | if err != nil { 41 | return nil, fmt.Errorf("metric value format is not valid: %s", err.Error()) 42 | } 43 | 44 | retentionTimestamp := int64(resultByMetricArr[i].Score) 45 | 46 | if err != nil { 47 | return nil, fmt.Errorf("retention timestamp format is not valid: %s", err.Error()) 48 | } 49 | 50 | metricValue := moira.MetricValue{ 51 | RetentionTimestamp: retentionTimestamp, 52 | Timestamp: timestamp, 53 | Value: value, 54 | } 55 | metricsValues = append(metricsValues, &metricValue) 56 | } 57 | 58 | return metricsValues, nil 59 | } 60 | -------------------------------------------------------------------------------- /database/redis/reply/notifier.go: -------------------------------------------------------------------------------- 1 | package reply 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/go-redis/redis/v8" 9 | "github.com/moira-alert/moira" 10 | "github.com/moira-alert/moira/database" 11 | ) 12 | 13 | // NotifierState parses moira.NotifierState from redis.StringCmd. 14 | func NotifierState(rep *redis.StringCmd) (moira.NotifierState, error) { 15 | state := moira.NotifierState{} 16 | 17 | bytes, err := rep.Bytes() 18 | if err != nil { 19 | if errors.Is(err, redis.Nil) { 20 | return state, database.ErrNil 21 | } 22 | 23 | return state, fmt.Errorf("failed to read state: %s", err.Error()) 24 | } 25 | 26 | err = json.Unmarshal(bytes, &state) 27 | if err != nil { 28 | return state, fmt.Errorf("failed to parse state json %s %s", string(bytes), err.Error()) 29 | } 30 | 31 | return state, nil 32 | } 33 | 34 | // NotifierStateForSources represents map from metric source clusters to their states. 35 | type NotifierStateForSources struct { 36 | States map[string]moira.NotifierState `json:"states"` 37 | } 38 | 39 | // ParseNotifierStateForSources parses NotifierStateBySources from redis.StringCmd. 40 | func ParseNotifierStateForSources(rep *redis.StringCmd) (NotifierStateForSources, error) { 41 | state := NotifierStateForSources{ 42 | States: map[string]moira.NotifierState{}, 43 | } 44 | 45 | bytes, err := rep.Bytes() 46 | if err != nil { 47 | if errors.Is(err, redis.Nil) { 48 | return state, database.ErrNil 49 | } 50 | 51 | return state, fmt.Errorf("failed to read state: %s", err.Error()) 52 | } 53 | 54 | if len(bytes) == 0 { 55 | return state, nil 56 | } 57 | 58 | err = json.Unmarshal(bytes, &state) 59 | if err != nil { 60 | return state, fmt.Errorf("failed to parse state json '%s' %s", string(bytes), err.Error()) 61 | } 62 | 63 | return state, nil 64 | } 65 | -------------------------------------------------------------------------------- /database/stats/trigger.go: -------------------------------------------------------------------------------- 1 | package stats 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/moira-alert/moira" 7 | "github.com/moira-alert/moira/metrics" 8 | ) 9 | 10 | type triggerStats struct { 11 | metrics *metrics.TriggersMetrics 12 | database moira.Database 13 | logger moira.Logger 14 | clusters []moira.ClusterKey 15 | } 16 | 17 | // NewTriggerStats creates and initializes a new triggerStats object. 18 | func NewTriggerStats( 19 | metricsRegistry metrics.Registry, 20 | attributedRegistry metrics.MetricRegistry, 21 | database moira.Database, 22 | logger moira.Logger, 23 | clusters []moira.ClusterKey, 24 | ) (*triggerStats, error) { 25 | metrics, err := metrics.NewTriggersMetrics(metricsRegistry, attributedRegistry, clusters) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | return &triggerStats{ 31 | logger: logger, 32 | database: database, 33 | metrics: metrics, 34 | clusters: clusters, 35 | }, nil 36 | } 37 | 38 | // StartReport starts reporting statistics about triggers. 39 | func (stats *triggerStats) StartReport(stop <-chan struct{}) { 40 | checkTicker := time.NewTicker(time.Minute) 41 | defer checkTicker.Stop() 42 | 43 | stats.logger.Info().Msg("Start trigger statistics reporter") 44 | 45 | for { 46 | select { 47 | case <-stop: 48 | stats.logger.Info().Msg("Stop trigger statistics reporter") 49 | return 50 | 51 | case <-checkTicker.C: 52 | stats.checkTriggerCount() 53 | } 54 | } 55 | } 56 | 57 | func (stats *triggerStats) checkTriggerCount() { 58 | triggersCount, err := stats.database.GetTriggerCount(stats.clusters) 59 | if err != nil { 60 | stats.logger.Warning(). 61 | Error(err). 62 | Msg("Failed to fetch triggers count") 63 | 64 | return 65 | } 66 | 67 | for source, count := range triggersCount { 68 | stats.metrics.Mark(source, count) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /image_store/s3/init.go: -------------------------------------------------------------------------------- 1 | package s3 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/aws/aws-sdk-go/aws/credentials" 7 | "github.com/aws/aws-sdk-go/service/s3/s3manager" 8 | 9 | "github.com/aws/aws-sdk-go/aws" 10 | "github.com/aws/aws-sdk-go/aws/session" 11 | ) 12 | 13 | // ImageStore implements the ImageStore interface for aws s3. 14 | type ImageStore struct { 15 | sess *session.Session 16 | uploader *s3manager.Uploader 17 | bucket string 18 | enabled bool 19 | } 20 | 21 | // Init initializes the s3 image store with config from the yaml file. 22 | func (imageStore *ImageStore) Init(config Config) error { 23 | awsconfig := &aws.Config{} 24 | 25 | if config.AccessKeyID == "" { 26 | return fmt.Errorf("access key id not found while configuring s3 image store") 27 | } 28 | 29 | if config.AccessKey == "" { 30 | return fmt.Errorf("access key not found while configuring s3 image store") 31 | } 32 | 33 | awsconfig.Credentials = credentials.NewStaticCredentials(config.AccessKeyID, config.AccessKey, "") 34 | 35 | if config.Region == "" { 36 | return fmt.Errorf("region not found while configuring s3 image store") 37 | } 38 | 39 | awsconfig.Region = aws.String(config.Region) 40 | 41 | imageStore.bucket = config.Bucket 42 | if imageStore.bucket == "" { 43 | return fmt.Errorf("bucket not found while configuring s3 image store") 44 | } 45 | 46 | var err error 47 | 48 | imageStore.sess, err = session.NewSession(awsconfig) 49 | if err != nil { 50 | return fmt.Errorf("could not configure s3 session: %w", err) 51 | } 52 | 53 | imageStore.uploader = s3manager.NewUploader(imageStore.sess) 54 | 55 | imageStore.enabled = true 56 | 57 | return nil 58 | } 59 | 60 | // IsEnabled indicates whether the image store has been configured or not. 61 | func (imageStore *ImageStore) IsEnabled() bool { 62 | return imageStore.enabled 63 | } 64 | -------------------------------------------------------------------------------- /.github/workflows/swagger-publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish spec version to SwaggerHub 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - release/* 8 | tags: 9 | - "v*" 10 | 11 | jobs: 12 | validate-spec: 13 | name: Validate spec file 14 | runs-on: ubuntu-22.04 15 | defaults: 16 | run: 17 | working-directory: . 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | 22 | - uses: actions/setup-go@v4 23 | with: 24 | go-version-file: go.mod 25 | cache-dependency-path: go.sum 26 | - run: make install-swag-v3 27 | 28 | - uses: actions/setup-node@v4 29 | with: 30 | node-version: '20.17.0' 31 | - run: npm install --location=global @openapitools/openapi-generator-cli 32 | - run: make spec-v3 33 | - run: make validate-spec-v3 34 | 35 | - name: Save build artifact 36 | uses: actions/upload-artifact@v4.6.1 37 | with: 38 | name: specfile 39 | path: docs/swagger.yaml 40 | 41 | publishspec: 42 | name: Upload generated OpenAPI description 43 | runs-on: ubuntu-22.04 44 | needs: validate-spec 45 | defaults: 46 | run: 47 | working-directory: . 48 | 49 | steps: 50 | - uses: actions/checkout@v4 51 | 52 | - name: Download spec file artifact 53 | uses: actions/download-artifact@v4.1.9 54 | with: 55 | name: specfile 56 | path: docs 57 | 58 | - uses: actions/setup-node@v4 59 | - run: npm i --location=global swaggerhub-cli 60 | - run: | 61 | VERSION=`echo ${GITHUB_REF_NAME}| sed 's#[^a-zA-Z0-9_\.\-]#_#g'` 62 | SWAGGERHUB_API_KEY=${{secrets.SWAGGERHUB_TOKEN}} swaggerhub api:create "Moira/moira-alert/${VERSION}" -f ./docs/swagger.yaml --published=publish --visibility=public 63 | -------------------------------------------------------------------------------- /senders/mattermost/sender_manual_test.go: -------------------------------------------------------------------------------- 1 | //go:build manual 2 | 3 | package mattermost_test 4 | 5 | import ( 6 | "testing" 7 | "time" 8 | 9 | "github.com/moira-alert/moira" 10 | "github.com/moira-alert/moira/senders/mattermost" 11 | 12 | logging "github.com/moira-alert/moira/logging/zerolog_adapter" 13 | . "github.com/smartystreets/goconvey/convey" 14 | ) 15 | 16 | // TestSender is integration manual test. Paste your Url, Token and Channel ID and check message in Mattermost. 17 | func TestSender(t *testing.T) { 18 | logger, _ := logging.ConfigureLog("stdout", "debug", "test", true) 19 | 20 | const ( 21 | url = "https://mattermost.com/" 22 | apiToken = "8pdo6yoiutgidgxs9qxhbo7w4h" 23 | channelID = "3y6ab8rptfdr9m1hzskghpxwsc" 24 | ) 25 | 26 | Convey("Init tests", t, func() { 27 | sender := &mattermost.Sender{} 28 | 29 | Convey("With url and apiToken", func() { 30 | senderSettings := map[string]string{ 31 | "url": url, 32 | "api_token": apiToken, 33 | "front_uri": "http://moira.url", 34 | "insecure_tls": "true", 35 | } 36 | location, _ := time.LoadLocation("UTC") 37 | err := sender.Init(senderSettings, logger, location, "") 38 | So(err, ShouldBeNil) 39 | 40 | event := moira.NotificationEvent{ 41 | TriggerID: "TriggerID", 42 | Values: map[string]float64{"t1": 123}, 43 | Timestamp: 150000000, 44 | Metric: "Metric", 45 | OldState: moira.StateOK, 46 | State: moira.StateNODATA, 47 | } 48 | events, contact, trigger, plots, throttled := moira.NotificationEvents{event}, moira.ContactData{ 49 | Value: channelID, 50 | }, moira.TriggerData{ 51 | ID: "ID", 52 | Name: "Name", 53 | }, make([][]byte, 0), false 54 | 55 | err = sender.SendEvents(events, contact, trigger, plots, throttled) 56 | So(err, ShouldBeNil) 57 | }) 58 | }) 59 | } 60 | -------------------------------------------------------------------------------- /metric_source/local/timer_test.go: -------------------------------------------------------------------------------- 1 | package local 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/smartystreets/goconvey/convey" 7 | ) 8 | 9 | func TestTimerNumberOfTimeSlots(t *testing.T) { 10 | retention := int64(60) 11 | steps := int64(10) 12 | 13 | // Specific edge-case that is required by carbonapi 14 | Convey("Given `from` is divisible by retention", t, func() { 15 | for _, from := range []int64{0, retention} { 16 | until := from + retention*steps 17 | timer := newTimerRoundingTimestamps(from, until, retention) 18 | 19 | So(timer.numberOfTimeSlots(), ShouldEqual, steps+1) 20 | } 21 | }) 22 | 23 | Convey("Given `from` is divisible by retention", t, func() { 24 | from := int64(0) 25 | until := int64(0) 26 | timer := newTimerRoundingTimestamps(from, until, retention) 27 | 28 | So(timer.numberOfTimeSlots(), ShouldEqual, 1) 29 | }) 30 | 31 | Convey("Given `from` is not divisible by retention", t, func() { 32 | for from := int64(1); from < retention; from++ { 33 | until := from + retention*steps 34 | timer := newTimerRoundingTimestamps(from, until, retention) 35 | 36 | So(timer.numberOfTimeSlots(), ShouldEqual, steps) 37 | } 38 | }) 39 | } 40 | 41 | func TestTimerGetTimeSlot(t *testing.T) { 42 | Convey("Given a set of test cases", t, func() { 43 | retention := int64(10) 44 | from := int64(10) 45 | until := int64(60) 46 | timer := newTimerRoundingTimestamps(from, until, retention) 47 | 48 | testCases := []struct { 49 | timestamp int64 50 | timeSlot int 51 | }{ 52 | {5, -1}, 53 | {10, 0}, 54 | {15, 0}, 55 | {19, 0}, 56 | {20, 1}, 57 | {21, 1}, 58 | {25, 1}, 59 | {29, 1}, 60 | {30, 2}, 61 | } 62 | 63 | for _, testCase := range testCases { 64 | actual := timer.getTimeSlot(testCase.timestamp) 65 | So(actual, ShouldEqual, testCase.timeSlot) 66 | } 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /api/controller/notification.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/moira-alert/moira" 5 | "github.com/moira-alert/moira/api" 6 | "github.com/moira-alert/moira/api/dto" 7 | ) 8 | 9 | // GetNotifications gets all notifications from current page, if end==-1 && start==0 gets all notifications. 10 | func GetNotifications(database moira.Database, start int64, end int64) (*dto.NotificationsList, *api.ErrorResponse) { 11 | notifications, total, err := database.GetNotifications(start, end) 12 | if err != nil { 13 | return nil, api.ErrorInternalServer(err) 14 | } 15 | 16 | notificationsList := dto.NotificationsList{ 17 | List: notifications, 18 | Total: total, 19 | } 20 | 21 | return ¬ificationsList, nil 22 | } 23 | 24 | // DeleteNotification removes all notifications by notification key. 25 | func DeleteNotification(database moira.Database, notificationKey string) (*dto.NotificationDeleteResponse, *api.ErrorResponse) { 26 | result, err := database.RemoveNotification(notificationKey) 27 | if err != nil { 28 | return nil, api.ErrorInternalServer(err) 29 | } 30 | 31 | return &dto.NotificationDeleteResponse{Result: result}, nil 32 | } 33 | 34 | // DeleteAllNotifications removes all notifications. 35 | func DeleteAllNotifications(database moira.Database) *api.ErrorResponse { 36 | if err := database.RemoveAllNotifications(); err != nil { 37 | return api.ErrorInternalServer(err) 38 | } 39 | 40 | return nil 41 | } 42 | 43 | // DeleteFilteredNotifications removes notifications filtered by timestamp and tags. 44 | func DeleteFilteredNotifications(database moira.Database, from, to int64, ignoredTags []string, sourceList []moira.ClusterKey) *api.ErrorResponse { 45 | _, err := database.RemoveFilteredNotifications(from, to, ignoredTags, sourceList) 46 | if err != nil { 47 | return api.ErrorInternalServer(err) 48 | } 49 | 50 | return nil 51 | } 52 | -------------------------------------------------------------------------------- /senders/telegram/handle_message.go: -------------------------------------------------------------------------------- 1 | package telegram 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | 8 | "gopkg.in/telebot.v3" 9 | ) 10 | 11 | // handleMessage handles incoming messages to start sending events to subscribers chats. 12 | func (sender *Sender) handleMessage(message *telebot.Message) error { 13 | responseMessage, err := sender.getResponseMessage(message) 14 | if err != nil { 15 | return err 16 | } 17 | 18 | if responseMessage != "" { 19 | if _, err = sender.bot.Reply(message, responseMessage); err != nil { 20 | return sender.removeTokenFromError(err) 21 | } 22 | } 23 | 24 | return nil 25 | } 26 | 27 | func (sender *Sender) getResponseMessage(message *telebot.Message) (string, error) { 28 | chatID := strconv.FormatInt(message.Chat.ID, 10) 29 | 30 | switch { 31 | case message.Chat.Type == telebot.ChatPrivate && message.Text == "/start": 32 | if message.Chat.Username == "" { 33 | return "Username is empty. Please add username in Telegram.", nil 34 | } 35 | 36 | if err := sender.setChat(message); err != nil { 37 | return "", err 38 | } 39 | 40 | return fmt.Sprintf("Okay, %s, your id is %s", strings.Trim(fmt.Sprintf("%s %s", message.Sender.FirstName, message.Sender.LastName), " "), chatID), nil 41 | case (message.Chat.Type == telebot.ChatSuperGroup || message.Chat.Type == telebot.ChatGroup): 42 | contactValue, err := sender.getContactValueByMessage(message) 43 | if err != nil { 44 | return "", fmt.Errorf("failed to get contact value from message: %w", err) 45 | } 46 | 47 | if err = sender.setChat(message); err != nil { 48 | return "", err 49 | } 50 | 51 | if strings.HasPrefix(message.Text, "/start") { 52 | return fmt.Sprintf("Hi, all!\nI will send alerts in this group (%s).", contactValue), nil 53 | } 54 | 55 | return "", nil 56 | } 57 | 58 | return "I don't understand you :(", nil 59 | } 60 | -------------------------------------------------------------------------------- /index/mapping/trigger_test.go: -------------------------------------------------------------------------------- 1 | package mapping 2 | 3 | import ( 4 | "log" 5 | "reflect" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/moira-alert/moira" 10 | . "github.com/smartystreets/goconvey/convey" 11 | ) 12 | 13 | var testTriggerFields = []FieldData{ 14 | TriggerID, 15 | TriggerName, 16 | TriggerDesc, 17 | TriggerTags, 18 | TriggerLastCheckScore, 19 | } 20 | 21 | func TestTriggerField_GetPriority(t *testing.T) { 22 | expected := []float64{5, 3, 1, 0, 0} 23 | actual := make([]float64, 0, len(testTriggerFields)) 24 | 25 | Convey("Test GetPriority returns correct field priority", t, func() { 26 | for _, triggerField := range testTriggerFields { 27 | fieldName, fieldPriority := triggerField.GetName(), triggerField.GetPriority() 28 | log.Printf("field: %s priority: %f", fieldName, fieldPriority) 29 | 30 | actual = append(actual, triggerField.GetPriority()) 31 | } 32 | 33 | So(actual, ShouldResemble, expected) 34 | }) 35 | } 36 | 37 | func TestTriggerField_GetTagValue(t *testing.T) { 38 | // This test is necessary to make sure that 39 | // SearchResult will contain highlights for actual moira.Trigger structure 40 | Convey("Test GetTagValue returns correct JSON tag", t, func() { 41 | for _, triggerField := range testTriggerFields { 42 | actual := getTagByFieldName(triggerField.GetName()) 43 | expected := triggerField.GetTagValue() 44 | So(actual, ShouldEqual, expected) 45 | } 46 | }) 47 | } 48 | 49 | // getTagByFieldName returns corresponding moira.Trigger JSON tag for given trigger field. 50 | func getTagByFieldName(fieldName string) string { 51 | var trigger moira.Trigger 52 | 53 | var fieldTag string 54 | if field, ok := reflect.TypeOf(&trigger).Elem().FieldByName(fieldName); ok { 55 | fieldTag = field.Tag.Get("json") 56 | fieldTag = strings.ReplaceAll(fieldTag, ",omitempty", "") 57 | } 58 | 59 | return fieldTag 60 | } 61 | -------------------------------------------------------------------------------- /checker/errors.go: -------------------------------------------------------------------------------- 1 | package checker 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // ErrTriggerNotExists used if trigger to check does not exists. 9 | var ErrTriggerNotExists = fmt.Errorf("trigger does not exists") 10 | 11 | // ErrTriggerHasOnlyWildcards used if trigger has only wildcard metrics. 12 | type ErrTriggerHasOnlyWildcards struct{} 13 | 14 | // Error implementation with constant error message. 15 | func (err ErrTriggerHasOnlyWildcards) Error() string { 16 | return "Trigger never received metrics" 17 | } 18 | 19 | // ErrTriggerHasSameMetricNames used if trigger has two metric data with same name. 20 | type ErrTriggerHasSameMetricNames struct { 21 | duplicates map[string][]string 22 | } 23 | 24 | // NewErrTriggerHasSameMetricNames is a constructor function for ErrTriggerHasSameMetricNames. 25 | func NewErrTriggerHasSameMetricNames(duplicates map[string][]string) ErrTriggerHasSameMetricNames { 26 | return ErrTriggerHasSameMetricNames{ 27 | duplicates: duplicates, 28 | } 29 | } 30 | 31 | // Error implementation with constant error message. 32 | func (err ErrTriggerHasSameMetricNames) Error() string { 33 | var builder strings.Builder 34 | 35 | builder.WriteString("Targets have metrics with identical name: ") 36 | 37 | for target, duplicates := range err.duplicates { 38 | builder.WriteString(target) 39 | builder.WriteRune(':') 40 | builder.WriteString(strings.Join(duplicates, ", ")) 41 | builder.WriteString("; ") 42 | } 43 | 44 | return builder.String() 45 | } 46 | 47 | // ErrTriggerHasEmptyTargets used if additional trigger target has not metrics data after fetch from source. 48 | type ErrTriggerHasEmptyTargets struct { 49 | targets []string 50 | } 51 | 52 | // Error implementation with error message. 53 | func (err ErrTriggerHasEmptyTargets) Error() string { 54 | return fmt.Sprintf("target %v has no metrics", strings.Join(err.targets, ", ")) 55 | } 56 | -------------------------------------------------------------------------------- /mock/moira-alert/database_stats.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: github.com/moira-alert/moira/database/stats (interfaces: StatsReporter) 3 | // 4 | // Generated by this command: 5 | // 6 | // mockgen -destination=mock/moira-alert/database_stats.go -package=mock_moira_alert github.com/moira-alert/moira/database/stats StatsReporter 7 | // 8 | 9 | // Package mock_moira_alert is a generated GoMock package. 10 | package mock_moira_alert 11 | 12 | import ( 13 | reflect "reflect" 14 | 15 | gomock "go.uber.org/mock/gomock" 16 | ) 17 | 18 | // MockStatsReporter is a mock of StatsReporter interface. 19 | type MockStatsReporter struct { 20 | ctrl *gomock.Controller 21 | recorder *MockStatsReporterMockRecorder 22 | isgomock struct{} 23 | } 24 | 25 | // MockStatsReporterMockRecorder is the mock recorder for MockStatsReporter. 26 | type MockStatsReporterMockRecorder struct { 27 | mock *MockStatsReporter 28 | } 29 | 30 | // NewMockStatsReporter creates a new mock instance. 31 | func NewMockStatsReporter(ctrl *gomock.Controller) *MockStatsReporter { 32 | mock := &MockStatsReporter{ctrl: ctrl} 33 | mock.recorder = &MockStatsReporterMockRecorder{mock} 34 | return mock 35 | } 36 | 37 | // EXPECT returns an object that allows the caller to indicate expected use. 38 | func (m *MockStatsReporter) EXPECT() *MockStatsReporterMockRecorder { 39 | return m.recorder 40 | } 41 | 42 | // StartReport mocks base method. 43 | func (m *MockStatsReporter) StartReport(stop <-chan struct{}) { 44 | m.ctrl.T.Helper() 45 | m.ctrl.Call(m, "StartReport", stop) 46 | } 47 | 48 | // StartReport indicates an expected call of StartReport. 49 | func (mr *MockStatsReporterMockRecorder) StartReport(stop any) *gomock.Call { 50 | mr.mock.ctrl.T.Helper() 51 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartReport", reflect.TypeOf((*MockStatsReporter)(nil).StartReport), stop) 52 | } 53 | -------------------------------------------------------------------------------- /filter/heartbeat/worker.go: -------------------------------------------------------------------------------- 1 | package heartbeat 2 | 3 | import ( 4 | "time" 5 | 6 | "gopkg.in/tomb.v2" 7 | 8 | "github.com/moira-alert/moira" 9 | "github.com/moira-alert/moira/metrics" 10 | ) 11 | 12 | // Worker is heartbeat worker realization. 13 | type Worker struct { 14 | database moira.Database 15 | metrics *metrics.FilterMetrics 16 | logger moira.Logger 17 | tomb tomb.Tomb 18 | } 19 | 20 | // NewHeartbeatWorker creates new worker. 21 | func NewHeartbeatWorker(database moira.Database, metrics *metrics.FilterMetrics, logger moira.Logger) *Worker { 22 | return &Worker{ 23 | database: database, 24 | metrics: metrics, 25 | logger: logger, 26 | } 27 | } 28 | 29 | // Start every 5 second takes TotalMetricsReceived metrics and save it to database, for self-checking. 30 | func (worker *Worker) Start() { 31 | worker.tomb.Go(func() error { 32 | count := worker.metrics.TotalMetricsReceived.Count() 33 | checkTicker := time.NewTicker(time.Second * 5) //nolint 34 | 35 | for { 36 | select { 37 | case <-worker.tomb.Dying(): 38 | worker.logger.Info().Msg("Moira Filter Heartbeat stopped") 39 | return nil 40 | case <-checkTicker.C: 41 | newCount := worker.metrics.TotalMetricsReceived.Count() 42 | if newCount != count { 43 | if err := worker.database.UpdateMetricsHeartbeat(); err != nil { 44 | worker.logger.Error(). 45 | Error(err). 46 | Msg("Update metrics heartbeat failed") 47 | } else { 48 | worker.logger.Debug(). 49 | Int64("from", count). 50 | Int64("to", newCount). 51 | Msg("Heartbeat was updated") 52 | 53 | count = newCount 54 | } 55 | } 56 | } 57 | } 58 | }) 59 | 60 | worker.logger.Info().Msg("Moira Filter Heartbeat started") 61 | } 62 | 63 | // Stop heartbeat worker. 64 | func (worker *Worker) Stop() error { 65 | worker.tomb.Kill(nil) 66 | return worker.tomb.Wait() 67 | } 68 | -------------------------------------------------------------------------------- /senders/opsgenie/init.go: -------------------------------------------------------------------------------- 1 | package opsgenie 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/mitchellh/mapstructure" 8 | "github.com/moira-alert/moira" 9 | "github.com/moira-alert/moira/senders" 10 | "github.com/opsgenie/opsgenie-go-sdk-v2/alert" 11 | "github.com/opsgenie/opsgenie-go-sdk-v2/client" 12 | ) 13 | 14 | // Structure that represents the OpsGenie configuration in the YAML file. 15 | type config struct { 16 | APIKey string `mapstructure:"api_key" validate:"required"` 17 | } 18 | 19 | // Sender implements the Sender interface for opsgenie. 20 | type Sender struct { 21 | apiKey string 22 | client *alert.Client 23 | logger moira.Logger 24 | location *time.Location 25 | ImageStores map[string]moira.ImageStore 26 | imageStoreID string 27 | imageStore moira.ImageStore 28 | imageStoreConfigured bool 29 | } 30 | 31 | // Init initializes the opsgenie sender. 32 | func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, location *time.Location, dateTimeFormat string) error { 33 | var cfg config 34 | 35 | err := mapstructure.Decode(senderSettings, &cfg) 36 | if err != nil { 37 | return fmt.Errorf("failed to decode senderSettings to opsgenie config: %w", err) 38 | } 39 | 40 | if err = moira.ValidateStruct(cfg); err != nil { 41 | return fmt.Errorf("opsgenie config validation error: %w", err) 42 | } 43 | 44 | sender.apiKey = cfg.APIKey 45 | sender.imageStoreID, sender.imageStore, sender.imageStoreConfigured = senders.ReadImageStoreConfig(senderSettings, sender.ImageStores, logger) 46 | 47 | sender.client, err = alert.NewClient(&client.Config{ 48 | ApiKey: sender.apiKey, 49 | }) 50 | if err != nil { 51 | return fmt.Errorf("error while creating opsgenie client: %w", err) 52 | } 53 | 54 | sender.logger = logger 55 | sender.location = location 56 | 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /database/redis/delivery.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/go-redis/redis/v8" 7 | "github.com/moira-alert/moira/database" 8 | ) 9 | 10 | // AddDeliveryChecksData adds given data to sorted by timestamp set relative to contact type. 11 | func (connector *DbConnector) AddDeliveryChecksData(contactType string, timestamp int64, data string) error { 12 | client := connector.Client() 13 | ctx := connector.Context() 14 | 15 | return client.ZAdd( 16 | ctx, 17 | deliveryCheckKeyWithContactType(contactType), 18 | &redis.Z{ 19 | Score: float64(timestamp), 20 | Member: data, 21 | }).Err() 22 | } 23 | 24 | // GetDeliveryChecksData reads data from for given tim range relative to contact type. 25 | func (connector *DbConnector) GetDeliveryChecksData(contactType string, from string, to string) ([]string, error) { 26 | client := connector.Client() 27 | ctx := connector.Context() 28 | 29 | res, err := client.ZRangeByScore( 30 | ctx, 31 | deliveryCheckKeyWithContactType(contactType), 32 | &redis.ZRangeBy{ 33 | Min: from, 34 | Max: to, 35 | Offset: 0, 36 | Count: -1, 37 | }).Result() 38 | if err != nil { 39 | if errors.Is(err, redis.Nil) { 40 | return nil, database.ErrNil 41 | } 42 | 43 | return nil, err 44 | } 45 | 46 | return res, nil 47 | } 48 | 49 | // RemoveDeliveryChecksData removes data from for given time range relative to contact type. 50 | func (connector *DbConnector) RemoveDeliveryChecksData(contactType string, from string, to string) (int64, error) { 51 | client := connector.Client() 52 | ctx := connector.Context() 53 | 54 | return client.ZRemRangeByScore(ctx, deliveryCheckKeyWithContactType(contactType), from, to).Result() 55 | } 56 | 57 | const deliveryCheckKey = "moira-delivery-check" 58 | 59 | func deliveryCheckKeyWithContactType(contactType string) string { 60 | return deliveryCheckKey + ":" + contactType 61 | } 62 | -------------------------------------------------------------------------------- /notifier/selfstate/heartbeat/local_checker.go: -------------------------------------------------------------------------------- 1 | package heartbeat 2 | 3 | import "github.com/moira-alert/moira" 4 | 5 | type localChecker struct { 6 | heartbeat 7 | 8 | count int64 9 | } 10 | 11 | func GetLocalChecker(delay, lastSuccessfulCheck int64, checkTags []string, logger moira.Logger, database moira.Database) Heartbeater { 12 | if delay > 0 { 13 | return &localChecker{heartbeat: heartbeat{ 14 | logger: logger, 15 | database: database, 16 | delay: delay, 17 | lastSuccessfulCheck: lastSuccessfulCheck, 18 | checkTags: checkTags, 19 | }} 20 | } 21 | 22 | return nil 23 | } 24 | 25 | func (check *localChecker) Check(nowTS int64) (int64, bool, error) { 26 | defaultLocalCluster := moira.DefaultLocalCluster 27 | 28 | triggersCount, err := check.database.GetTriggersToCheckCount(defaultLocalCluster) 29 | if err != nil { 30 | return 0, false, err 31 | } 32 | 33 | checksCount, _ := check.database.GetChecksUpdatesCount() 34 | if check.count != checksCount || triggersCount == 0 { 35 | check.count = checksCount 36 | check.lastSuccessfulCheck = nowTS 37 | 38 | return 0, false, nil 39 | } 40 | 41 | if check.lastSuccessfulCheck < nowTS-check.delay { 42 | check.logger.Error(). 43 | String("error", check.GetErrorMessage()). 44 | Int64("time_since_successful_check", nowTS-check.heartbeat.lastSuccessfulCheck). 45 | Msg("Send message") 46 | 47 | return nowTS - check.lastSuccessfulCheck, true, nil 48 | } 49 | 50 | return 0, false, nil 51 | } 52 | 53 | func (localChecker) NeedToCheckOthers() bool { 54 | return true 55 | } 56 | 57 | func (check localChecker) NeedTurnOffNotifier() bool { 58 | return false 59 | } 60 | 61 | func (localChecker) GetErrorMessage() string { 62 | return "Moira-Checker does not check triggers" 63 | } 64 | 65 | func (check *localChecker) GetCheckTags() CheckTags { 66 | return check.checkTags 67 | } 68 | -------------------------------------------------------------------------------- /perfomance_tests/filter/filter_plain_metrics_test.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "math/rand" 5 | "strings" 6 | "testing" 7 | "time" 8 | 9 | "github.com/moira-alert/moira/filter" 10 | ) 11 | 12 | func BenchmarkProcessIncomingPlainMetric(b *testing.B) { 13 | plainPatterns, err := loadPatterns("plain_patterns.txt") 14 | if err != nil { 15 | b.Error(err.Error()) 16 | } 17 | 18 | patternsStorage, err := createPatternsStorage(plainPatterns, b) 19 | if err != nil { 20 | b.Errorf("Can not create new patterns storage %s", err) 21 | } 22 | 23 | testMetricsLines := generateMetrics(patternsStorage, b.N) 24 | 25 | runBenchmark(b, patternsStorage, testMetricsLines) 26 | } 27 | 28 | func generateMetrics(patternStorage *filter.PatternStorage, count int) *[]string { 29 | result := make([]string, 0, count) 30 | timestamp := time.Now() 31 | patternTree := patternStorage.PatternIndex.Load().(*filter.PatternIndex).Tree.Root 32 | 33 | for i := 0; i < count; i++ { 34 | parts := make([]string, 0, 16) 35 | 36 | node := patternTree.Children[rand.Intn(len(patternTree.Children))] 37 | matched := rand.Float64() < 0.02 38 | level := float64(0) 39 | 40 | for { 41 | part := node.Part 42 | if len(node.InnerParts) > 0 { 43 | part = node.InnerParts[rand.Intn(len(node.InnerParts))] 44 | } 45 | 46 | if !matched && rand.Float64() < 0.2+level { 47 | part = RandStringBytes(len(part)) 48 | } 49 | 50 | parts = append(parts, strings.ReplaceAll(part, "*", "XXXXXXXXX")) 51 | 52 | if len(node.Children) == 0 { 53 | break 54 | } 55 | 56 | level += 0.7 57 | node = node.Children[rand.Intn(len(node.Children))] 58 | } 59 | 60 | matchedMetricPath := strings.Join(parts, ".") 61 | metric := generateMetricLineByPath(matchedMetricPath, timestamp) 62 | result = append(result, metric) 63 | timestamp = timestamp.Add(time.Microsecond) 64 | } 65 | 66 | return &result 67 | } 68 | -------------------------------------------------------------------------------- /database/redis/database_test.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/go-redis/redis/v8" 8 | logging "github.com/moira-alert/moira/logging/zerolog_adapter" 9 | 10 | . "github.com/smartystreets/goconvey/convey" 11 | ) 12 | 13 | func TestNewDatabase(t *testing.T) { 14 | Convey("NewDatabase should return correct DBConnector", t, func() { 15 | logger, _ := logging.ConfigureLog("stdout", "info", "test", true) // nolint: govet 16 | database := NewTestDatabase(logger) 17 | So(database, ShouldNotBeEmpty) 18 | So(database.source, ShouldEqual, "test") 19 | So(database.logger, ShouldEqual, logger) 20 | So(database.context, ShouldResemble, context.Background()) 21 | 22 | database.Flush() 23 | defer database.Flush() 24 | 25 | Convey("Redis client must be workable", func() { 26 | ctx := context.Background() 27 | 28 | Convey("Can get the value of key that does not exists", func() { 29 | err := (*database.client).Get(ctx, "key").Err() 30 | So(err, ShouldEqual, redis.Nil) 31 | }) 32 | 33 | Convey("Can set key to hold the string value", func() { 34 | err := (*database.client).Set(ctx, "key", "value", 0).Err() 35 | So(err, ShouldBeNil) 36 | }) 37 | 38 | Convey("Can get the value of key that exists", func() { 39 | (*database.client).Set(ctx, "key", "value", 0) 40 | 41 | val, err := (*database.client).Get(ctx, "key").Result() 42 | So(err, ShouldBeNil) 43 | So(val, ShouldEqual, "value") 44 | }) 45 | 46 | Convey("Can remove key", func() { 47 | (*database.client).Set(ctx, "key", "value", 0) 48 | val := (*database.client).Get(ctx, "key").Val() 49 | So(val, ShouldEqual, "value") 50 | 51 | err := (*database.client).Del(ctx, "key").Err() 52 | So(err, ShouldBeNil) 53 | 54 | err = (*database.client).Get(ctx, "key").Err() 55 | So(err, ShouldEqual, redis.Nil) 56 | }) 57 | }) 58 | }) 59 | } 60 | -------------------------------------------------------------------------------- /api/handler/validate.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/go-graphite/carbonapi/date" 9 | ) 10 | 11 | // DateRangeValidator for query parameters from and to. 12 | type DateRangeValidator struct { 13 | AllowInf bool 14 | } 15 | 16 | // ValidateDateRangeStrings validates and converts both from and to query params. 17 | // Returns converted from and to, or error if there was any. 18 | // If AllowInf is true "-inf" is allowed for from, "+inf" for to. 19 | func (d DateRangeValidator) ValidateDateRangeStrings(fromStr, toStr string) (string, string, error) { 20 | fromStr, err := d.validateFromStr(fromStr) 21 | if err != nil { 22 | return "", "", err 23 | } 24 | 25 | toStr, err = d.validateToStr(toStr) 26 | if err != nil { 27 | return "", "", err 28 | } 29 | 30 | return fromStr, toStr, nil 31 | } 32 | 33 | // validateFromStr by trying to parse date with carbonapi/date package. Also converts to proper format. 34 | // If AllowInf is true, then "-inf" is also allowed. 35 | func (d DateRangeValidator) validateFromStr(fromStr string) (string, error) { 36 | if d.AllowInf && fromStr == "-inf" { 37 | return fromStr, nil 38 | } 39 | 40 | from := date.DateParamToEpoch(fromStr, "UTC", 0, time.UTC) 41 | if from == 0 { 42 | return "", fmt.Errorf("can not parse from: %s", fromStr) 43 | } 44 | 45 | return strconv.FormatInt(from, 10), nil 46 | } 47 | 48 | // validateToStr by trying to parse date with carbonapi/date package. Also converts to proper format. 49 | // If AllowInf is true, then "+inf" is also allowed. 50 | func (d DateRangeValidator) validateToStr(toStr string) (string, error) { 51 | if d.AllowInf && toStr == "+inf" { 52 | return toStr, nil 53 | } 54 | 55 | to := date.DateParamToEpoch(toStr, "UTC", 0, time.UTC) 56 | if to == 0 { 57 | return "", fmt.Errorf("can not parse to: %v", to) 58 | } 59 | 60 | return strconv.FormatInt(to, 10), nil 61 | } 62 | -------------------------------------------------------------------------------- /database/stats/contact.go: -------------------------------------------------------------------------------- 1 | package stats 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/moira-alert/moira" 7 | "github.com/moira-alert/moira/metrics" 8 | ) 9 | 10 | type contactStats struct { 11 | metrics *metrics.ContactsMetrics 12 | database moira.Database 13 | logger moira.Logger 14 | } 15 | 16 | // NewContactStats creates and initializes a new contactStats object. 17 | func NewContactStats( 18 | metricsRegistry metrics.Registry, 19 | attributedRegistry metrics.MetricRegistry, 20 | database moira.Database, 21 | logger moira.Logger, 22 | ) *contactStats { 23 | return &contactStats{ 24 | metrics: metrics.NewContactsMetrics(metricsRegistry, attributedRegistry), 25 | database: database, 26 | logger: logger, 27 | } 28 | } 29 | 30 | // StartReport starts reporting statistics about contacts. 31 | func (stats *contactStats) StartReport(stop <-chan struct{}) { 32 | checkTicker := time.NewTicker(time.Minute) 33 | defer checkTicker.Stop() 34 | 35 | stats.logger.Info().Msg("Start contact statistics reporter") 36 | 37 | for { 38 | select { 39 | case <-stop: 40 | stats.logger.Info().Msg("Stop contact statistics reporter") 41 | return 42 | 43 | case <-checkTicker.C: 44 | stats.checkContactsCount() 45 | } 46 | } 47 | } 48 | 49 | func (stats *contactStats) checkContactsCount() { 50 | contacts, err := stats.database.GetAllContacts() 51 | if err != nil { 52 | stats.logger.Warning(). 53 | Error(err). 54 | Msg("Failed to get all contacts") 55 | 56 | return 57 | } 58 | 59 | contactsCounter := make(map[string]int64) 60 | 61 | for _, contact := range contacts { 62 | if contact != nil { 63 | contactsCounter[contact.Type]++ 64 | } 65 | } 66 | 67 | for contact, count := range contactsCounter { 68 | err := stats.metrics.Mark(contact, count) 69 | if err != nil { 70 | stats.logger.Warning(). 71 | Error(err). 72 | Msg("Failed to mark contacts count") 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /api/dto/team_test.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/moira-alert/moira/api" 11 | "github.com/moira-alert/moira/api/middleware" 12 | 13 | . "github.com/smartystreets/goconvey/convey" 14 | ) 15 | 16 | func TestTeamValidation(t *testing.T) { 17 | Convey("Test team validation", t, func() { 18 | teamModel := TeamModel{} 19 | 20 | limits := api.GetTestLimitsConfig() 21 | 22 | request, _ := http.NewRequestWithContext(context.Background(), http.MethodPost, "/api/teams", nil) 23 | request.Header.Set("Content-Type", "application/json") 24 | request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "limits", limits)) 25 | 26 | Convey("with empty team.Name", func() { 27 | err := teamModel.Bind(request) 28 | 29 | So(err, ShouldResemble, errEmptyTeamName) 30 | }) 31 | 32 | Convey("with team.Name has characters more than in limit", func() { 33 | teamModel.Name = strings.Repeat("ё", limits.Team.MaxNameSize+1) 34 | 35 | err := teamModel.Bind(request) 36 | 37 | So(err, ShouldResemble, fmt.Errorf("team name cannot be longer than %d characters", limits.Team.MaxNameSize)) 38 | }) 39 | 40 | Convey("with team.Description has characters more than in limit", func() { 41 | teamModel.Name = strings.Repeat("ё", limits.Team.MaxNameSize) 42 | teamModel.Description = strings.Repeat("ё", limits.Team.MaxDescriptionSize+1) 43 | 44 | err := teamModel.Bind(request) 45 | 46 | So(err, ShouldResemble, fmt.Errorf("team description cannot be longer than %d characters", limits.Team.MaxDescriptionSize)) 47 | }) 48 | 49 | Convey("with valid team", func() { 50 | teamModel.Name = strings.Repeat("ё", limits.Team.MaxNameSize) 51 | teamModel.Description = strings.Repeat("ё", limits.Team.MaxDescriptionSize) 52 | 53 | err := teamModel.Bind(request) 54 | 55 | So(err, ShouldBeNil) 56 | }) 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /local/notifier.yml: -------------------------------------------------------------------------------- 1 | #See https://moira.readthedocs.io/en/latest/installation/configuration.html for config explanation 2 | redis: 3 | addrs: "redis:6379" 4 | metrics_ttl: 3h 5 | telemetry: 6 | graphite: 7 | enabled: true 8 | runtime_stats: true 9 | uri: "relay:2003" 10 | prefix: moira 11 | interval: 60s 12 | pprof: 13 | enabled: true 14 | listen: ":8093" 15 | graphite_remote: 16 | - cluster_id: default 17 | cluster_name: Graphite 1 18 | url: "http://graphite:80/render" 19 | check_interval: 60s 20 | metrics_ttl: 168h 21 | timeout: 60s 22 | retries: 23 | initial_interval: 60s 24 | randomization_factor: 0.5 25 | multiplier: 1.5 26 | max_interval: 120s 27 | max_retries_count: 3 28 | health_check_timeout: 6s 29 | health_check_retries: 30 | initial_interval: 20s 31 | randomization_factor: 0.5 32 | multiplier: 1.5 33 | max_interval: 80s 34 | max_retries_count: 3 35 | prometheus_remote: 36 | - cluster_id: default 37 | cluster_name: Prometheus 1 38 | url: "http://prometheus:9090" 39 | check_interval: 60s 40 | timeout: 60s 41 | metrics_ttl: 168h 42 | notifier: 43 | sender_timeout: 10s 44 | resending_timeout: "1:00" 45 | rescheduling_delay: 60s 46 | senders: [] 47 | moira_selfstate: 48 | enabled: false 49 | remote_triggers_enabled: false 50 | redis_disconect_delay: 60s 51 | last_metric_received_delay: 120s 52 | last_check_delay: 120s 53 | last_remote_check_delay: 300s 54 | notice_interval: 300s 55 | front_uri: http://localhost 56 | timezone: UTC 57 | date_time_format: "15:04 02.01.2006" 58 | notification_history: 59 | ttl: 48h 60 | notification: 61 | delayed_time: 50s 62 | transaction_timeout: 100ms 63 | transaction_max_retries: 10 64 | transaction_heuristic_limit: 10000 65 | resave_time: 30s 66 | log: 67 | log_file: stdout 68 | log_level: info 69 | log_pretty_format: true 70 | -------------------------------------------------------------------------------- /metric_source/metric_data.go: -------------------------------------------------------------------------------- 1 | package metricsource 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | ) 7 | 8 | // MetricData is moira implementation of target evaluation result. 9 | type MetricData struct { 10 | Name string 11 | StartTime int64 12 | StopTime int64 13 | StepTime int64 14 | Values []float64 15 | Wildcard bool 16 | } 17 | 18 | // MakeMetricData creates new metrics data with given metric timeseries. 19 | func MakeMetricData(name string, values []float64, step, start int64) *MetricData { 20 | stop := start + int64(len(values))*step 21 | 22 | return &MetricData{ 23 | Name: name, 24 | Values: values, 25 | StartTime: start, 26 | StepTime: step, 27 | StopTime: stop, 28 | } 29 | } 30 | 31 | // MakeEmptyMetricData create MetricData with given interval and retention step with all empty metric points. 32 | func MakeEmptyMetricData(name string, step, start, stop int64) *MetricData { 33 | values := make([]float64, 0) 34 | for i := start; i < stop; i += step { 35 | values = append(values, math.NaN()) 36 | } 37 | 38 | return &MetricData{ 39 | Name: name, 40 | Values: values, 41 | StartTime: start, 42 | StepTime: step, 43 | StopTime: stop, 44 | } 45 | } 46 | 47 | // GetTimestampValue gets value of given timestamp index, if value is Nil, then return NaN. 48 | func (metricData *MetricData) GetTimestampValue(valueTimestamp int64) float64 { 49 | if valueTimestamp < metricData.StartTime { 50 | return math.NaN() 51 | } 52 | 53 | valueIndex := int((valueTimestamp - metricData.StartTime) / metricData.StepTime) 54 | 55 | if len(metricData.Values) <= valueIndex { 56 | return math.NaN() 57 | } 58 | 59 | return metricData.Values[valueIndex] 60 | } 61 | 62 | func (metricData *MetricData) String() string { 63 | return fmt.Sprintf("Metric: %s, StartTime: %v, StopTime: %v, StepTime: %v, Points: %v", metricData.Name, metricData.StartTime, metricData.StopTime, metricData.StepTime, metricData.Values) 64 | } 65 | -------------------------------------------------------------------------------- /notifier/selfstate/heartbeat/remote_checker.go: -------------------------------------------------------------------------------- 1 | package heartbeat 2 | 3 | import "github.com/moira-alert/moira" 4 | 5 | type remoteChecker struct { 6 | heartbeat 7 | 8 | count int64 9 | } 10 | 11 | func GetRemoteChecker(delay, lastSuccessfulCheck int64, checkTags []string, logger moira.Logger, database moira.Database) Heartbeater { 12 | if delay > 0 { 13 | return &remoteChecker{heartbeat: heartbeat{ 14 | logger: logger, 15 | database: database, 16 | delay: delay, 17 | lastSuccessfulCheck: lastSuccessfulCheck, 18 | checkTags: checkTags, 19 | }} 20 | } 21 | 22 | return nil 23 | } 24 | 25 | func (check *remoteChecker) Check(nowTS int64) (int64, bool, error) { 26 | defaultRemoteCluster := moira.DefaultGraphiteRemoteCluster 27 | 28 | triggerCount, err := check.database.GetTriggersToCheckCount(defaultRemoteCluster) 29 | if err != nil { 30 | return 0, false, err 31 | } 32 | 33 | remoteTriggersCount, _ := check.database.GetRemoteChecksUpdatesCount() 34 | if check.count != remoteTriggersCount || triggerCount == 0 { 35 | check.count = remoteTriggersCount 36 | check.lastSuccessfulCheck = nowTS 37 | 38 | return 0, false, nil 39 | } 40 | 41 | if check.lastSuccessfulCheck < nowTS-check.delay { 42 | check.logger.Error(). 43 | String("error", check.GetErrorMessage()). 44 | Int64("time_since_successful_check", nowTS-check.heartbeat.lastSuccessfulCheck). 45 | Msg("Send message") 46 | 47 | return nowTS - check.lastSuccessfulCheck, true, nil 48 | } 49 | 50 | return 0, false, nil 51 | } 52 | 53 | func (check remoteChecker) NeedTurnOffNotifier() bool { 54 | return false 55 | } 56 | 57 | func (remoteChecker) NeedToCheckOthers() bool { 58 | return true 59 | } 60 | 61 | func (remoteChecker) GetErrorMessage() string { 62 | return "Moira-Remote-Checker does not check remote triggers" 63 | } 64 | 65 | func (check *remoteChecker) GetCheckTags() CheckTags { 66 | return check.checkTags 67 | } 68 | -------------------------------------------------------------------------------- /cmd/cli/metrics_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/moira-alert/moira/database/redis" 8 | mocks "github.com/moira-alert/moira/mock/moira-alert" 9 | 10 | . "github.com/smartystreets/goconvey/convey" 11 | "go.uber.org/mock/gomock" 12 | ) 13 | 14 | func TestCleanUpOutdatedMetrics(t *testing.T) { 15 | conf := getDefault() 16 | 17 | mockCtrl := gomock.NewController(t) 18 | defer mockCtrl.Finish() 19 | 20 | db := mocks.NewMockDatabase(mockCtrl) 21 | 22 | Convey("Test cleanup outdated metrics", t, func() { 23 | Convey("With valid duration", func() { 24 | db.EXPECT().CleanUpOutdatedMetrics(-168 * time.Hour).Return(nil) 25 | err := handleCleanUpOutdatedMetrics(conf.Cleanup, db) 26 | So(err, ShouldBeNil) 27 | }) 28 | 29 | Convey("With invalid duration", func() { 30 | conf.Cleanup.CleanupMetricsDuration = "168h" 31 | 32 | db.EXPECT().CleanUpOutdatedMetrics(168 * time.Hour).Return(redis.ErrCleanUpDurationGreaterThanZero) 33 | err := handleCleanUpOutdatedMetrics(conf.Cleanup, db) 34 | So(err, ShouldEqual, redis.ErrCleanUpDurationGreaterThanZero) 35 | }) 36 | }) 37 | } 38 | 39 | func TestCleanUpFutureMetrics(t *testing.T) { 40 | conf := getDefault() 41 | 42 | mockCtrl := gomock.NewController(t) 43 | defer mockCtrl.Finish() 44 | 45 | db := mocks.NewMockDatabase(mockCtrl) 46 | 47 | Convey("Test cleanup future metrics", t, func() { 48 | Convey("With valid duration", func() { 49 | db.EXPECT().CleanUpFutureMetrics(60 * time.Minute).Return(nil) 50 | err := handleCleanUpFutureMetrics(conf.Cleanup, db) 51 | So(err, ShouldBeNil) 52 | }) 53 | 54 | Convey("With invalid duration", func() { 55 | conf.Cleanup.CleanupFutureMetricsDuration = "-60m" 56 | 57 | db.EXPECT().CleanUpFutureMetrics(-60 * time.Minute).Return(redis.ErrCleanUpDurationLessThanZero) 58 | err := handleCleanUpFutureMetrics(conf.Cleanup, db) 59 | So(err, ShouldEqual, redis.ErrCleanUpDurationLessThanZero) 60 | }) 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /mock/scheduler/scheduler.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: github.com/moira-alert/moira/notifier (interfaces: Scheduler) 3 | // 4 | // Generated by this command: 5 | // 6 | // mockgen -destination=mock/scheduler/scheduler.go -package=mock_scheduler github.com/moira-alert/moira/notifier Scheduler 7 | // 8 | 9 | // Package mock_scheduler is a generated GoMock package. 10 | package mock_scheduler 11 | 12 | import ( 13 | reflect "reflect" 14 | 15 | moira "github.com/moira-alert/moira" 16 | gomock "go.uber.org/mock/gomock" 17 | ) 18 | 19 | // MockScheduler is a mock of Scheduler interface. 20 | type MockScheduler struct { 21 | ctrl *gomock.Controller 22 | recorder *MockSchedulerMockRecorder 23 | isgomock struct{} 24 | } 25 | 26 | // MockSchedulerMockRecorder is the mock recorder for MockScheduler. 27 | type MockSchedulerMockRecorder struct { 28 | mock *MockScheduler 29 | } 30 | 31 | // NewMockScheduler creates a new mock instance. 32 | func NewMockScheduler(ctrl *gomock.Controller) *MockScheduler { 33 | mock := &MockScheduler{ctrl: ctrl} 34 | mock.recorder = &MockSchedulerMockRecorder{mock} 35 | return mock 36 | } 37 | 38 | // EXPECT returns an object that allows the caller to indicate expected use. 39 | func (m *MockScheduler) EXPECT() *MockSchedulerMockRecorder { 40 | return m.recorder 41 | } 42 | 43 | // ScheduleNotification mocks base method. 44 | func (m *MockScheduler) ScheduleNotification(params moira.SchedulerParams, logger moira.Logger) *moira.ScheduledNotification { 45 | m.ctrl.T.Helper() 46 | ret := m.ctrl.Call(m, "ScheduleNotification", params, logger) 47 | ret0, _ := ret[0].(*moira.ScheduledNotification) 48 | return ret0 49 | } 50 | 51 | // ScheduleNotification indicates an expected call of ScheduleNotification. 52 | func (mr *MockSchedulerMockRecorder) ScheduleNotification(params, logger any) *gomock.Call { 53 | mr.mock.ctrl.T.Helper() 54 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ScheduleNotification", reflect.TypeOf((*MockScheduler)(nil).ScheduleNotification), params, logger) 55 | } 56 | --------------------------------------------------------------------------------
14 | 15 | $ api --version 16 | 17 | $ checker --version 18 | 19 | $ cli --version 20 | 21 | $ filter --version 22 | 23 | $ notifier --version 24 | 25 |