mirror of
https://github.com/Icinga/icingadb.git
synced 2026-02-18 18:18:00 -05:00
SLA reporting: tests for the SQL stored function
This commit is contained in:
parent
b81392857a
commit
5ea82188dc
7 changed files with 530 additions and 21 deletions
58
.github/workflows/sql.yml
vendored
Normal file
58
.github/workflows/sql.yml
vendored
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
name: SQL
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request: {}
|
||||
|
||||
jobs:
|
||||
sql:
|
||||
name: ${{ matrix.database.name }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
database:
|
||||
- {type: MYSQL, name: MySQL 5.5, image: "icinga/icingadb-mysql:5.5"}
|
||||
- {type: MYSQL, name: MySQL 5.6, image: "icinga/icingadb-mysql:5.6"}
|
||||
- {type: MYSQL, name: MySQL 5.7, image: "mysql:5.7"}
|
||||
- {type: MYSQL, name: MySQL latest, image: "mysql:latest"}
|
||||
- {type: MYSQL, name: MariaDB 10.1, image: "mariadb:10.1"}
|
||||
- {type: MYSQL, name: MariaDB 10.2, image: "mariadb:10.2"}
|
||||
- {type: MYSQL, name: MariaDB 10.3, image: "mariadb:10.3"}
|
||||
- {type: MYSQL, name: MariaDB 10.4, image: "mariadb:10.4"}
|
||||
- {type: MYSQL, name: MariaDB 10.5, image: "mariadb:10.5"}
|
||||
- {type: MYSQL, name: MariaDB 10.6, image: "mariadb:10.6"}
|
||||
- {type: MYSQL, name: MariaDB 10.7, image: "mariadb:10.7"}
|
||||
- {type: MYSQL, name: MariaDB latest, image: "mariadb:latest"}
|
||||
- {type: PGSQL, name: PostgreSQL 9.6, image: "postgres:9.6"}
|
||||
- {type: PGSQL, name: PostgreSQL 10, image: "postgres:10"}
|
||||
- {type: PGSQL, name: PostgreSQL 11, image: "postgres:11"}
|
||||
- {type: PGSQL, name: PostgreSQL 12, image: "postgres:12"}
|
||||
- {type: PGSQL, name: PostgreSQL 13, image: "postgres:13"}
|
||||
- {type: PGSQL, name: PostgreSQL latest, image: "postgres:latest"}
|
||||
|
||||
steps:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: '^1.16'
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Download dependencies
|
||||
run: go get -v -t -d ./...
|
||||
working-directory: tests/
|
||||
|
||||
- name: Run tests
|
||||
env:
|
||||
ICINGADB_TESTS_DATABASE_TYPE: ${{ matrix.database.type }}
|
||||
ICINGA_TESTING_${{ matrix.database.type }}_IMAGE: ${{ matrix.database.image }}
|
||||
ICINGA_TESTING_ICINGADB_SCHEMA_MYSQL: ${{ github.workspace }}/schema/mysql/schema.sql
|
||||
ICINGA_TESTING_ICINGADB_SCHEMA_PGSQL: ${{ github.workspace }}/schema/pgsql/schema.sql
|
||||
timeout-minutes: 10
|
||||
run: go test -v -timeout 5m ./sql
|
||||
working-directory: tests/
|
||||
|
|
@ -5,10 +5,12 @@ go 1.16
|
|||
require (
|
||||
github.com/containerd/containerd v1.5.6 // indirect
|
||||
github.com/go-redis/redis/v8 v8.11.4
|
||||
github.com/go-sql-driver/mysql v1.6.0
|
||||
github.com/goccy/go-yaml v1.9.5
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/icinga/icinga-testing v0.0.0-20220503150619-1c215361234c
|
||||
github.com/icinga/icinga-testing v0.0.0-20220513095329-9c98d3145b01
|
||||
github.com/jmoiron/sqlx v1.3.4
|
||||
github.com/lib/pq v1.10.5
|
||||
github.com/stretchr/testify v1.7.0
|
||||
go.uber.org/zap v1.21.0
|
||||
golang.org/x/net v0.0.0-20211020060615-d418f374d309 // indirect
|
||||
|
|
|
|||
13
tests/go.sum
13
tests/go.sum
|
|
@ -37,6 +37,8 @@ github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZ
|
|||
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/Icinga/go-libs v0.0.0-20220420130327-ef58ad52edd8 h1:hG4Y/LPERK9i+P8/jnYlq9PeDd9deIkwEWOIimDU3uk=
|
||||
github.com/Icinga/go-libs v0.0.0-20220420130327-ef58ad52edd8/go.mod h1:xlgU55MKs/vIg1fMlAEBSrslahYayZNwjXvf3w1dvyA=
|
||||
github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
|
||||
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
|
||||
github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
|
||||
|
|
@ -60,6 +62,7 @@ github.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5
|
|||
github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:mw7qgWloBUl75W/gVH3cQszUg1+gUITj7D6NY7ywVnY=
|
||||
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
|
||||
|
|
@ -68,6 +71,7 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy
|
|||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0=
|
||||
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
|
||||
|
|
@ -402,8 +406,8 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
|
|||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/icinga/icinga-testing v0.0.0-20220503150619-1c215361234c h1:jJVPK9dvsZ99Xl/xDOBM+DEj+7i7En51/04ckc/lTZc=
|
||||
github.com/icinga/icinga-testing v0.0.0-20220503150619-1c215361234c/go.mod h1:W9pLmq2dsgLSag568N/LDHNu4oah6qWvjT05Drz2RYw=
|
||||
github.com/icinga/icinga-testing v0.0.0-20220513095329-9c98d3145b01 h1:0dwlZFGWPnmmhvHr2P7chxMwzbW7+R3iX6SyeFBd+WM=
|
||||
github.com/icinga/icinga-testing v0.0.0-20220513095329-9c98d3145b01/go.mod h1:ZP0pyqhmrRwwQ6FpAfz7UZMgmH7i3vOjEOm9JcFwOw0=
|
||||
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||
|
|
@ -444,8 +448,9 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
|||
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk=
|
||||
github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lib/pq v1.10.5 h1:J+gdV2cUmX7ZqL2B0lFcW0m+egaHC2V3lpO8nWxyYiQ=
|
||||
github.com/lib/pq v1.10.5/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
|
|
@ -459,6 +464,7 @@ github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHX
|
|||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
|
||||
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
|
||||
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
|
||||
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
|
|
@ -725,6 +731,7 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB
|
|||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
|
|
|
|||
28
tests/internal/utils/database.go
Normal file
28
tests/internal/utils/database.go
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/icinga/icinga-testing"
|
||||
"github.com/icinga/icinga-testing/services"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func GetDatabase(it *icingatesting.IT, t testing.TB) services.RelationalDatabase {
|
||||
k := "ICINGADB_TESTS_DATABASE_TYPE"
|
||||
v := strings.ToLower(os.Getenv(k))
|
||||
|
||||
var rdb services.RelationalDatabase
|
||||
|
||||
switch v {
|
||||
case "mysql":
|
||||
rdb = it.MysqlDatabaseT(t)
|
||||
case "pgsql":
|
||||
rdb = it.PostgresqlDatabaseT(t)
|
||||
default:
|
||||
panic(fmt.Sprintf(`unknown database in %s environment variable: %q (must be "mysql" or "pgsql")`, k, v))
|
||||
}
|
||||
|
||||
return rdb
|
||||
}
|
||||
|
|
@ -1,10 +1,9 @@
|
|||
package icingadb_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/icinga/icinga-testing"
|
||||
"github.com/icinga/icinga-testing/services"
|
||||
"os"
|
||||
"github.com/icinga/icingadb/tests/internal/utils"
|
||||
"testing"
|
||||
)
|
||||
|
||||
|
|
@ -26,19 +25,5 @@ func getDatabase(t testing.TB) services.RelationalDatabase {
|
|||
}
|
||||
|
||||
func getEmptyDatabase(t testing.TB) services.RelationalDatabase {
|
||||
k := "ICINGADB_TESTS_DATABASE_TYPE"
|
||||
v := os.Getenv(k)
|
||||
|
||||
var rdb services.RelationalDatabase
|
||||
|
||||
switch v {
|
||||
case "mysql":
|
||||
rdb = it.MysqlDatabaseT(t)
|
||||
case "pgsql":
|
||||
rdb = it.PostgresqlDatabaseT(t)
|
||||
default:
|
||||
panic(fmt.Sprintf(`unknown database in %s environment variable: %q (must be "mysql" or "pgsql")`, k, v))
|
||||
}
|
||||
|
||||
return rdb
|
||||
return utils.GetDatabase(it, t)
|
||||
}
|
||||
|
|
|
|||
23
tests/sql/main_test.go
Normal file
23
tests/sql/main_test.go
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
package sql_test
|
||||
|
||||
import (
|
||||
"github.com/icinga/icinga-testing"
|
||||
"github.com/icinga/icinga-testing/services"
|
||||
"github.com/icinga/icingadb/tests/internal/utils"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var it *icingatesting.IT
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
it = icingatesting.NewIT()
|
||||
defer it.Cleanup()
|
||||
|
||||
m.Run()
|
||||
}
|
||||
|
||||
func getDatabase(t testing.TB) services.RelationalDatabase {
|
||||
rdb := utils.GetDatabase(it, t)
|
||||
rdb.ImportIcingaDbSchema()
|
||||
return rdb
|
||||
}
|
||||
406
tests/sql/sla_test.go
Normal file
406
tests/sql/sla_test.go
Normal file
|
|
@ -0,0 +1,406 @@
|
|||
package sql_test
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
"github.com/go-sql-driver/mysql"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/lib/pq"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSla(t *testing.T) {
|
||||
rdb := getDatabase(t)
|
||||
db, err := sqlx.Open(rdb.Driver(), rdb.DSN())
|
||||
require.NoError(t, err, "connect to database")
|
||||
|
||||
type TestData struct {
|
||||
Name string
|
||||
Events []SlaHistoryEvent
|
||||
Start uint64
|
||||
End uint64
|
||||
Expected float64
|
||||
}
|
||||
|
||||
tests := []TestData{{
|
||||
Name: "EmptyHistory",
|
||||
// Empty history implies no previous problem state, therefore SLA should be 100%
|
||||
Events: nil,
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Expected: 100.0,
|
||||
}, {
|
||||
Name: "MultipleStateChanges",
|
||||
// Some flapping, test that all changes are considered.
|
||||
Events: []SlaHistoryEvent{
|
||||
&State{Time: 1000, State: 2, PreviousState: 99}, // -10%
|
||||
&State{Time: 1100, State: 0, PreviousState: 2},
|
||||
&State{Time: 1300, State: 2, PreviousState: 0}, // -10%
|
||||
&State{Time: 1400, State: 0, PreviousState: 2},
|
||||
&State{Time: 1600, State: 2, PreviousState: 0}, // -10%
|
||||
&State{Time: 1700, State: 0, PreviousState: 2},
|
||||
&State{Time: 1900, State: 2, PreviousState: 0}, // -10%
|
||||
},
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Expected: 60.0,
|
||||
}, {
|
||||
Name: "OverlappingDowntimesAndProblems",
|
||||
// SLA should be 90%:
|
||||
// 1000..1100: OK, no downtime
|
||||
// 1100..1200: OK, in downtime
|
||||
// 1200..1300: CRITICAL, in downtime
|
||||
// 1300..1400: CRITICAL, no downtime (only period counting for SLA, -10%)
|
||||
// 1400..1500: CRITICAL, in downtime
|
||||
// 1500..1600: OK, in downtime
|
||||
// 1600..2000: OK, no downtime
|
||||
Events: []SlaHistoryEvent{
|
||||
&Downtime{Start: 1100, End: 1300},
|
||||
&Downtime{Start: 1400, End: 1600},
|
||||
&State{Time: 1200, State: 2, PreviousState: 0},
|
||||
&State{Time: 1500, State: 0, PreviousState: 2},
|
||||
},
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Expected: 90.0,
|
||||
}, {
|
||||
Name: "CriticalBeforeInterval",
|
||||
// If there is no event within the SLA interval, the last state from before the interval should be used.
|
||||
Events: []SlaHistoryEvent{
|
||||
&State{Time: 0, State: 2, PreviousState: 99},
|
||||
},
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Expected: 0.0,
|
||||
}, {
|
||||
Name: "CriticalBeforeIntervalWithDowntime",
|
||||
// State change and downtime start from before the SLA interval should be considered if still relevant.
|
||||
Events: []SlaHistoryEvent{
|
||||
&State{Time: 800, State: 2, PreviousState: 99},
|
||||
&Downtime{Start: 600, End: 1800},
|
||||
},
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Expected: 80.0,
|
||||
}, {
|
||||
Name: "CriticalBeforeIntervalWithOverlappingDowntimes",
|
||||
// Test that overlapping downtimes are properly accounted for.
|
||||
Events: []SlaHistoryEvent{
|
||||
&State{Time: 800, State: 2, PreviousState: 99},
|
||||
&Downtime{Start: 600, End: 1000},
|
||||
&Downtime{Start: 800, End: 1200},
|
||||
&Downtime{Start: 1000, End: 1400},
|
||||
// Everything except 1400-1600 is covered by downtimes, -20%
|
||||
&Downtime{Start: 1600, End: 2000},
|
||||
&Downtime{Start: 1800, End: 2200},
|
||||
},
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Expected: 80.0,
|
||||
}, {
|
||||
Name: "FallbackToPreviousState",
|
||||
// If there is no state event from before the SLA interval, the previous hard state from the first event
|
||||
// after the beginning of the SLA interval should be used as the initial state.
|
||||
Events: []SlaHistoryEvent{
|
||||
&State{Time: 1200, State: 0, PreviousState: 2},
|
||||
},
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Expected: 80.0,
|
||||
}, {
|
||||
Name: "FallbackToCurrentState",
|
||||
// If there are no state history events, the current state of the checkable should be used.
|
||||
Events: []SlaHistoryEvent{
|
||||
&CurrentState{State: 2},
|
||||
},
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Expected: 0.0,
|
||||
}, {
|
||||
Name: "PreferInitialStateFromBeforeOverLaterState",
|
||||
// The previous_hard_state should only be used as a fallback when there is no event from before the
|
||||
// SLA interval. Therefore, the latter should be preferred if there is conflicting information.
|
||||
Events: []SlaHistoryEvent{
|
||||
&State{Time: 800, State: 2, PreviousState: 99},
|
||||
&State{Time: 1200, State: 0, PreviousState: 0},
|
||||
},
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Expected: 80.0,
|
||||
}, {
|
||||
Name: "PreferInitialStateFromBeforeOverCurrentState",
|
||||
// The current state should only be used as a fallback when there is no state history event.
|
||||
// Therefore, the latter should be preferred if there is conflicting information.
|
||||
Events: []SlaHistoryEvent{
|
||||
&State{Time: 800, State: 2, PreviousState: 99},
|
||||
&CurrentState{State: 0},
|
||||
},
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Expected: 0.0,
|
||||
}, {
|
||||
Name: "PreferLaterStateOverCurrentState",
|
||||
// The current state should only be used as a fallback when there is no state history event.
|
||||
// Therefore, the latter should be preferred if there is conflicting information.
|
||||
Events: []SlaHistoryEvent{
|
||||
&State{Time: 1200, State: 0, PreviousState: 2},
|
||||
&CurrentState{State: 2},
|
||||
},
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Expected: 80.0,
|
||||
}, {
|
||||
Name: "InitialUnknownReducesTotalTime",
|
||||
Events: []SlaHistoryEvent{
|
||||
&State{Time: 1500, State: 2, PreviousState: 99},
|
||||
&State{Time: 1700, State: 0, PreviousState: 2},
|
||||
&CurrentState{State: 0},
|
||||
},
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Expected: 60,
|
||||
}, {
|
||||
Name: "IntermediateUnknownReducesTotalTime",
|
||||
Events: []SlaHistoryEvent{
|
||||
&State{Time: 1000, State: 0, PreviousState: 2},
|
||||
&State{Time: 1100, State: 2, PreviousState: 0},
|
||||
&State{Time: 1600, State: 0, PreviousState: 99},
|
||||
&State{Time: 1800, State: 2, PreviousState: 0},
|
||||
&CurrentState{State: 0},
|
||||
},
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Expected: 60,
|
||||
}}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.Name, func(t *testing.T) {
|
||||
testSla(t, db, test.Events, test.Start, test.End, test.Expected, "unexpected SLA value")
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("Invalid", func(t *testing.T) {
|
||||
m := SlaHistoryMeta{
|
||||
EnvironmentId: make([]byte, 20),
|
||||
EndpointId: make([]byte, 20),
|
||||
ObjectType: "host",
|
||||
HostId: make([]byte, 20),
|
||||
}
|
||||
|
||||
checkErr := func(t *testing.T, err error) {
|
||||
require.Error(t, err, "SLA function should return an error")
|
||||
|
||||
switch d := db.DriverName(); d {
|
||||
case "mysql":
|
||||
var mysqlErr *mysql.MySQLError
|
||||
require.ErrorAs(t, err, &mysqlErr, "SLA function should return a MySQL error")
|
||||
// https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html#error_er_signal_exception
|
||||
assert.Equal(t, uint16(1644), mysqlErr.Number, "MySQL error should be ER_SIGNAL_EXCEPTION")
|
||||
assert.Equal(t, "end time must be greater than start time", mysqlErr.Message,
|
||||
"MySQL error should contain custom message")
|
||||
|
||||
case "postgres":
|
||||
var pqErr *pq.Error
|
||||
require.ErrorAs(t, err, &pqErr, "SLA function should return a PostgreSQL error")
|
||||
assert.Equal(t, pq.ErrorCode("P0001"), pqErr.Code, "MySQL error should be ER_SIGNAL_EXCEPTION")
|
||||
assert.Equal(t, "end time must be greater than start time", pqErr.Message,
|
||||
"PostgreSQL error should contain custom message")
|
||||
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown database driver %q", d))
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("ZeroDuration", func(t *testing.T) {
|
||||
_, err := execSqlSlaFunc(db, &m, 1000, 1000)
|
||||
checkErr(t, err)
|
||||
})
|
||||
|
||||
t.Run("NegativeDuration", func(t *testing.T) {
|
||||
_, err := execSqlSlaFunc(db, &m, 2000, 1000)
|
||||
checkErr(t, err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func execSqlSlaFunc(db *sqlx.DB, m *SlaHistoryMeta, start uint64, end uint64) (float64, error) {
|
||||
var result float64
|
||||
err := db.Get(&result, db.Rebind("SELECT get_sla_ok_percent(?, ?, ?, ?)"),
|
||||
m.HostId, m.ServiceId, start, end)
|
||||
return result, err
|
||||
}
|
||||
|
||||
func testSla(t *testing.T, db *sqlx.DB, events []SlaHistoryEvent, start uint64, end uint64, expected float64, msg string) {
|
||||
t.Run("Host", func(t *testing.T) {
|
||||
testSlaWithObjectType(t, db, events, false, start, end, expected, msg)
|
||||
})
|
||||
t.Run("Service", func(t *testing.T) {
|
||||
testSlaWithObjectType(t, db, events, true, start, end, expected, msg)
|
||||
})
|
||||
}
|
||||
|
||||
func testSlaWithObjectType(t *testing.T, db *sqlx.DB,
|
||||
events []SlaHistoryEvent, service bool, start uint64, end uint64, expected float64, msg string,
|
||||
) {
|
||||
makeId := func() []byte {
|
||||
id := make([]byte, 20)
|
||||
_, err := rand.Read(id)
|
||||
require.NoError(t, err, "generating random id failed")
|
||||
return id
|
||||
}
|
||||
|
||||
meta := SlaHistoryMeta{
|
||||
EnvironmentId: makeId(),
|
||||
EndpointId: makeId(),
|
||||
HostId: makeId(),
|
||||
}
|
||||
if service {
|
||||
meta.ObjectType = "service"
|
||||
meta.ServiceId = makeId()
|
||||
} else {
|
||||
meta.ObjectType = "host"
|
||||
}
|
||||
|
||||
for _, event := range events {
|
||||
err := event.WriteSlaEventToDatabase(db, &meta)
|
||||
require.NoErrorf(t, err, "Inserting SLA history for %#v failed", event)
|
||||
}
|
||||
|
||||
r, err := execSqlSlaFunc(db, &meta, start, end)
|
||||
require.NoError(t, err, "SLA query should not fail")
|
||||
assert.Equal(t, expected, r, msg)
|
||||
}
|
||||
|
||||
type SlaHistoryMeta struct {
|
||||
EnvironmentId NullableBytes `db:"environment_id"`
|
||||
EndpointId NullableBytes `db:"endpoint_id"`
|
||||
ObjectType string `db:"object_type"`
|
||||
HostId NullableBytes `db:"host_id"`
|
||||
ServiceId NullableBytes `db:"service_id"`
|
||||
}
|
||||
|
||||
type SlaHistoryEvent interface {
|
||||
WriteSlaEventToDatabase(db *sqlx.DB, m *SlaHistoryMeta) error
|
||||
}
|
||||
|
||||
type State struct {
|
||||
Time uint64
|
||||
State uint8
|
||||
PreviousState uint8
|
||||
}
|
||||
|
||||
var _ SlaHistoryEvent = (*State)(nil)
|
||||
|
||||
func (s *State) WriteSlaEventToDatabase(db *sqlx.DB, m *SlaHistoryMeta) error {
|
||||
type values struct {
|
||||
*SlaHistoryMeta
|
||||
Id []byte `db:"id"`
|
||||
EventTime uint64 `db:"event_time"`
|
||||
HardState uint8 `db:"hard_state"`
|
||||
PreviousHardState uint8 `db:"previous_hard_state"`
|
||||
}
|
||||
|
||||
id := make([]byte, 20)
|
||||
_, err := rand.Read(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = db.NamedExec("INSERT INTO sla_history_state"+
|
||||
" (id, environment_id, endpoint_id, object_type, host_id, service_id, event_time, hard_state, previous_hard_state)"+
|
||||
" VALUES (:id, :environment_id, :endpoint_id, :object_type, :host_id, :service_id, :event_time, :hard_state, :previous_hard_state)",
|
||||
&values{
|
||||
SlaHistoryMeta: m,
|
||||
Id: id[:],
|
||||
EventTime: s.Time,
|
||||
HardState: s.State,
|
||||
PreviousHardState: s.PreviousState,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
type CurrentState struct {
|
||||
State uint8
|
||||
}
|
||||
|
||||
func (c *CurrentState) WriteSlaEventToDatabase(db *sqlx.DB, m *SlaHistoryMeta) error {
|
||||
type values struct {
|
||||
*SlaHistoryMeta
|
||||
State uint8 `db:"state"`
|
||||
PropertiesChecksum NullableBytes `db:"properties_checksum"`
|
||||
}
|
||||
|
||||
v := values{
|
||||
SlaHistoryMeta: m,
|
||||
State: c.State,
|
||||
PropertiesChecksum: make([]byte, 20),
|
||||
}
|
||||
|
||||
if len(m.ServiceId) == 0 {
|
||||
_, err := db.NamedExec("INSERT INTO host_state"+
|
||||
" (id, host_id, environment_id, properties_checksum, soft_state, previous_soft_state,"+
|
||||
" hard_state, previous_hard_state, attempt, severity, last_state_change, next_check, next_update)"+
|
||||
" VALUES (:host_id, :host_id, :environment_id, :properties_checksum, :state, :state, :state, :state, 0, 0, 0, 0, 0)",
|
||||
&v)
|
||||
return err
|
||||
} else {
|
||||
_, err := db.NamedExec("INSERT INTO service_state"+
|
||||
" (id, host_id, service_id, environment_id, properties_checksum, soft_state, previous_soft_state,"+
|
||||
" hard_state, previous_hard_state, attempt, severity, last_state_change, next_check, next_update)"+
|
||||
" VALUES (:service_id, :host_id, :service_id, :environment_id, :properties_checksum, :state, :state, :state, :state, 0, 0, 0, 0, 0)",
|
||||
&v)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var _ SlaHistoryEvent = (*CurrentState)(nil)
|
||||
|
||||
type Downtime struct {
|
||||
Start uint64
|
||||
End uint64
|
||||
}
|
||||
|
||||
var _ SlaHistoryEvent = (*Downtime)(nil)
|
||||
|
||||
type slaHistoryDowntime struct {
|
||||
*SlaHistoryMeta
|
||||
DowntimeId []byte `db:"downtime_id"`
|
||||
DowntimeStart uint64 `db:"downtime_start"`
|
||||
DowntimeEnd uint64 `db:"downtime_end"`
|
||||
}
|
||||
|
||||
func (d *Downtime) WriteSlaEventToDatabase(db *sqlx.DB, m *SlaHistoryMeta) error {
|
||||
downtimeId := make([]byte, 20)
|
||||
_, err := rand.Read(downtimeId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = db.NamedExec("INSERT INTO sla_history_downtime"+
|
||||
" (environment_id, endpoint_id, object_type, host_id, service_id, downtime_id, downtime_start, downtime_end)"+
|
||||
" VALUES (:environment_id, :endpoint_id, :object_type, :host_id,"+
|
||||
" :service_id, :downtime_id, :downtime_start, :downtime_end)",
|
||||
&slaHistoryDowntime{
|
||||
SlaHistoryMeta: m,
|
||||
DowntimeId: downtimeId[:],
|
||||
DowntimeStart: d.Start,
|
||||
DowntimeEnd: d.End,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// NullableBytes allows writing to binary columns in a database with support for NULL.
|
||||
type NullableBytes []byte
|
||||
|
||||
// Value implements the database/sql/driver.Valuer interface.
|
||||
func (b NullableBytes) Value() (driver.Value, error) {
|
||||
if b != nil {
|
||||
return []byte(b), nil
|
||||
}
|
||||
|
||||
// any(nil) is treated as NULL in contrast to []byte(nil) which is a non-NULL byte sequence of length 0.
|
||||
return nil, nil
|
||||
}
|
||||
Loading…
Reference in a new issue