2021-07-12 06:56:30 +02:00
|
|
|
package database
|
|
|
|
|
|
|
|
import (
|
|
|
|
"database/sql"
|
|
|
|
"errors"
|
2021-07-16 04:07:30 +02:00
|
|
|
"fmt"
|
2021-07-12 06:56:30 +02:00
|
|
|
"log"
|
2021-07-12 10:25:25 +02:00
|
|
|
"strings"
|
|
|
|
"time"
|
2021-07-12 06:56:30 +02:00
|
|
|
|
|
|
|
"github.com/TwinProduction/gatus/core"
|
2021-07-15 04:26:51 +02:00
|
|
|
"github.com/TwinProduction/gatus/storage/store/paging"
|
2021-07-12 06:56:30 +02:00
|
|
|
"github.com/TwinProduction/gatus/util"
|
|
|
|
_ "modernc.org/sqlite"
|
|
|
|
)
|
|
|
|
|
2021-07-14 04:59:43 +02:00
|
|
|
//////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Note that only exported functions in this file may create, commit, or rollback a transaction //
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
2021-07-12 10:25:25 +02:00
|
|
|
const (
|
|
|
|
arraySeparator = "|~|"
|
2021-07-14 07:40:27 +02:00
|
|
|
|
2021-07-15 07:56:49 +02:00
|
|
|
uptimeCleanUpThreshold = 10 * 24 * time.Hour // Maximum uptime age before triggering a clean up
|
|
|
|
eventsCleanUpThreshold = core.MaximumNumberOfEvents + 10 // Maximum number of events before triggering a clean up
|
|
|
|
resultsCleanUpThreshold = core.MaximumNumberOfResults + 10 // Maximum number of results before triggering a clean up
|
2021-07-14 07:40:27 +02:00
|
|
|
|
|
|
|
uptimeRetention = 7 * 24 * time.Hour
|
2021-07-12 10:25:25 +02:00
|
|
|
)
|
|
|
|
|
2021-07-12 06:56:30 +02:00
|
|
|
var (
|
|
|
|
// ErrFilePathNotSpecified is the error returned when path parameter passed in NewStore is blank
|
|
|
|
ErrFilePathNotSpecified = errors.New("file path cannot be empty")
|
|
|
|
|
|
|
|
// ErrDatabaseDriverNotSpecified is the error returned when the driver parameter passed in NewStore is blank
|
|
|
|
ErrDatabaseDriverNotSpecified = errors.New("database driver cannot be empty")
|
|
|
|
|
|
|
|
errServiceNotFoundInDatabase = errors.New("service does not exist in database")
|
|
|
|
errNoRowsReturned = errors.New("expected a row to be returned, but none was")
|
|
|
|
)
|
|
|
|
|
|
|
|
// Store that leverages a database
|
|
|
|
type Store struct {
|
|
|
|
driver, file string
|
|
|
|
|
|
|
|
db *sql.DB
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewStore initializes the database and creates the schema if it doesn't already exist in the file specified
|
|
|
|
func NewStore(driver, path string) (*Store, error) {
|
|
|
|
if len(driver) == 0 {
|
|
|
|
return nil, ErrDatabaseDriverNotSpecified
|
|
|
|
}
|
|
|
|
if len(path) == 0 {
|
|
|
|
return nil, ErrFilePathNotSpecified
|
|
|
|
}
|
|
|
|
store := &Store{driver: driver, file: path}
|
|
|
|
var err error
|
|
|
|
if store.db, err = sql.Open(driver, path); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2021-07-13 04:53:14 +02:00
|
|
|
if driver == "sqlite" {
|
|
|
|
_, _ = store.db.Exec("PRAGMA foreign_keys=ON")
|
|
|
|
_, _ = store.db.Exec("PRAGMA journal_mode=WAL")
|
|
|
|
// Prevents driver from running into "database is locked" errors
|
|
|
|
// This is because we're using WAL to improve performance
|
|
|
|
store.db.SetMaxOpenConns(1)
|
|
|
|
}
|
2021-07-12 06:56:30 +02:00
|
|
|
if err = store.createSchema(); err != nil {
|
|
|
|
_ = store.db.Close()
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return store, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// createSchema creates the schema required to perform all database operations.
|
|
|
|
func (s *Store) createSchema() error {
|
|
|
|
_, err := s.db.Exec(`
|
|
|
|
CREATE TABLE IF NOT EXISTS service (
|
|
|
|
service_id INTEGER PRIMARY KEY,
|
|
|
|
service_key TEXT UNIQUE,
|
|
|
|
service_name TEXT,
|
|
|
|
service_group TEXT,
|
|
|
|
UNIQUE(service_name, service_group)
|
|
|
|
)
|
|
|
|
`)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
_, err = s.db.Exec(`
|
|
|
|
CREATE TABLE IF NOT EXISTS service_event (
|
|
|
|
service_event_id INTEGER PRIMARY KEY,
|
2021-07-13 04:53:14 +02:00
|
|
|
service_id INTEGER REFERENCES service(service_id) ON DELETE CASCADE,
|
2021-07-12 06:56:30 +02:00
|
|
|
event_type TEXT,
|
|
|
|
event_timestamp TIMESTAMP
|
|
|
|
)
|
|
|
|
`)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
_, err = s.db.Exec(`
|
|
|
|
CREATE TABLE IF NOT EXISTS service_result (
|
|
|
|
service_result_id INTEGER PRIMARY KEY,
|
2021-07-13 04:53:14 +02:00
|
|
|
service_id INTEGER REFERENCES service(service_id) ON DELETE CASCADE,
|
2021-07-12 06:56:30 +02:00
|
|
|
success INTEGER,
|
2021-07-12 08:54:16 +02:00
|
|
|
errors TEXT,
|
2021-07-12 06:56:30 +02:00
|
|
|
connected INTEGER,
|
|
|
|
status INTEGER,
|
|
|
|
dns_rcode TEXT,
|
|
|
|
certificate_expiration INTEGER,
|
|
|
|
hostname TEXT,
|
|
|
|
ip TEXT,
|
|
|
|
duration INTEGER,
|
|
|
|
timestamp TIMESTAMP
|
|
|
|
)
|
|
|
|
`)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
_, err = s.db.Exec(`
|
|
|
|
CREATE TABLE IF NOT EXISTS service_result_condition (
|
|
|
|
service_result_condition_id INTEGER PRIMARY KEY,
|
2021-07-13 04:53:14 +02:00
|
|
|
service_result_id INTEGER REFERENCES service_result(service_result_id) ON DELETE CASCADE,
|
2021-07-12 06:56:30 +02:00
|
|
|
condition TEXT,
|
|
|
|
success INTEGER
|
|
|
|
)
|
|
|
|
`)
|
2021-07-14 05:21:12 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
_, err = s.db.Exec(`
|
|
|
|
CREATE TABLE IF NOT EXISTS service_uptime (
|
|
|
|
service_uptime_id INTEGER PRIMARY KEY,
|
|
|
|
service_id INTEGER REFERENCES service(service_id) ON DELETE CASCADE,
|
|
|
|
hour_unix_timestamp INTEGER,
|
|
|
|
total_executions INTEGER,
|
|
|
|
successful_executions INTEGER,
|
|
|
|
total_response_time INTEGER,
|
|
|
|
UNIQUE(service_id, hour_unix_timestamp)
|
|
|
|
)
|
|
|
|
`)
|
2021-07-12 06:56:30 +02:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2021-07-15 04:26:51 +02:00
|
|
|
// GetAllServiceStatuses returns all monitored core.ServiceStatus
|
2021-07-12 10:25:25 +02:00
|
|
|
// with a subset of core.Result defined by the page and pageSize parameters
|
2021-07-15 04:26:51 +02:00
|
|
|
func (s *Store) GetAllServiceStatuses(params *paging.ServiceStatusParams) map[string]*core.ServiceStatus {
|
2021-07-14 04:59:43 +02:00
|
|
|
tx, err := s.db.Begin()
|
|
|
|
if err != nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
keys, err := s.getAllServiceKeys(tx)
|
|
|
|
if err != nil {
|
|
|
|
_ = tx.Rollback()
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
serviceStatuses := make(map[string]*core.ServiceStatus, len(keys))
|
|
|
|
for _, key := range keys {
|
2021-07-15 04:26:51 +02:00
|
|
|
serviceStatus, err := s.getServiceStatusByKey(tx, key, params)
|
2021-07-14 04:59:43 +02:00
|
|
|
if err != nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
serviceStatuses[key] = serviceStatus
|
2021-07-14 04:17:27 +02:00
|
|
|
}
|
2021-07-14 04:59:43 +02:00
|
|
|
if err = tx.Commit(); err != nil {
|
|
|
|
_ = tx.Rollback()
|
|
|
|
}
|
|
|
|
return serviceStatuses
|
2021-07-12 06:56:30 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// GetServiceStatus returns the service status for a given service name in the given group
|
2021-07-15 04:26:51 +02:00
|
|
|
func (s *Store) GetServiceStatus(groupName, serviceName string, parameters *paging.ServiceStatusParams) *core.ServiceStatus {
|
|
|
|
return s.GetServiceStatusByKey(util.ConvertGroupAndServiceToKey(groupName, serviceName), parameters)
|
2021-07-12 06:56:30 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// GetServiceStatusByKey returns the service status for a given key
|
2021-07-15 04:26:51 +02:00
|
|
|
func (s *Store) GetServiceStatusByKey(key string, params *paging.ServiceStatusParams) *core.ServiceStatus {
|
2021-07-14 04:59:43 +02:00
|
|
|
tx, err := s.db.Begin()
|
|
|
|
if err != nil {
|
|
|
|
return nil
|
|
|
|
}
|
2021-07-15 04:26:51 +02:00
|
|
|
serviceStatus, err := s.getServiceStatusByKey(tx, key, params)
|
2021-07-14 04:59:43 +02:00
|
|
|
if err != nil {
|
|
|
|
_ = tx.Rollback()
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
if err = tx.Commit(); err != nil {
|
|
|
|
_ = tx.Rollback()
|
|
|
|
}
|
2021-07-12 06:56:30 +02:00
|
|
|
return serviceStatus
|
|
|
|
}
|
|
|
|
|
2021-07-13 04:53:14 +02:00
|
|
|
// Insert adds the observed result for the specified service into the store
|
|
|
|
func (s *Store) Insert(service *core.Service, result *core.Result) {
|
|
|
|
tx, err := s.db.Begin()
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
//start := time.Now()
|
|
|
|
serviceID, err := s.getServiceID(tx, service)
|
|
|
|
if err != nil {
|
|
|
|
if err == errServiceNotFoundInDatabase {
|
|
|
|
// Service doesn't exist in the database, insert it
|
|
|
|
if serviceID, err = s.insertService(tx, service); err != nil {
|
2021-07-14 04:30:30 +02:00
|
|
|
_ = tx.Rollback()
|
2021-07-13 04:53:14 +02:00
|
|
|
return // failed to insert service
|
|
|
|
}
|
|
|
|
} else {
|
2021-07-14 04:30:30 +02:00
|
|
|
_ = tx.Rollback()
|
2021-07-13 04:53:14 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// First, we need to check if we need to insert a new event.
|
|
|
|
//
|
|
|
|
// A new event must be added if either of the following cases happen:
|
|
|
|
// 1. There is only 1 event. The total number of events for a service can only be 1 if the only existing event is
|
|
|
|
// of type EventStart, in which case we will have to create a new event of type EventHealthy or EventUnhealthy
|
|
|
|
// based on result.Success.
|
|
|
|
// 2. The lastResult.Success != result.Success. This implies that the service went from healthy to unhealthy or
|
|
|
|
// vice-versa, in which case we will have to create a new event of type EventHealthy or EventUnhealthy
|
|
|
|
// based on result.Success.
|
|
|
|
numberOfEvents, err := s.getNumberOfEventsByServiceID(tx, serviceID)
|
|
|
|
if err != nil {
|
2021-07-14 05:07:02 +02:00
|
|
|
log.Printf("[database][Insert] Failed to retrieve total number of events for group=%s; service=%s: %s", service.Group, service.Name, err.Error())
|
2021-07-13 04:53:14 +02:00
|
|
|
}
|
|
|
|
if numberOfEvents == 0 {
|
|
|
|
// There's no events yet, which means we need to add the EventStart and the first healthy/unhealthy event
|
|
|
|
err = s.insertEvent(tx, serviceID, &core.Event{
|
|
|
|
Type: core.EventStart,
|
|
|
|
Timestamp: result.Timestamp.Add(-50 * time.Millisecond),
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
// Silently fail
|
|
|
|
log.Printf("[database][Insert] Failed to insert event=%s for group=%s; service=%s: %s", core.EventStart, service.Group, service.Name, err.Error())
|
|
|
|
}
|
|
|
|
event := generateEventBasedOnResult(result)
|
2021-07-14 07:40:27 +02:00
|
|
|
if err = s.insertEvent(tx, serviceID, event); err != nil {
|
2021-07-13 04:53:14 +02:00
|
|
|
// Silently fail
|
|
|
|
log.Printf("[database][Insert] Failed to insert event=%s for group=%s; service=%s: %s", event.Type, service.Group, service.Name, err.Error())
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Get the success value of the previous result
|
|
|
|
var lastResultSuccess bool
|
2021-07-14 07:40:27 +02:00
|
|
|
if lastResultSuccess, err = s.getLastServiceResultSuccessValue(tx, serviceID); err != nil {
|
2021-07-13 04:53:14 +02:00
|
|
|
log.Printf("[database][Insert] Failed to retrieve outcome of previous result for group=%s; service=%s: %s", service.Group, service.Name, err.Error())
|
|
|
|
} else {
|
|
|
|
// If we managed to retrieve the outcome of the previous result, we'll compare it with the new result.
|
|
|
|
// If the final outcome (success or failure) of the previous and the new result aren't the same, it means
|
|
|
|
// that the service either went from Healthy to Unhealthy or Unhealthy -> Healthy, therefore, we'll add
|
|
|
|
// an event to mark the change in state
|
|
|
|
if lastResultSuccess != result.Success {
|
|
|
|
event := generateEventBasedOnResult(result)
|
2021-07-14 07:40:27 +02:00
|
|
|
if err = s.insertEvent(tx, serviceID, event); err != nil {
|
2021-07-13 04:53:14 +02:00
|
|
|
// Silently fail
|
|
|
|
log.Printf("[database][Insert] Failed to insert event=%s for group=%s; service=%s: %s", event.Type, service.Group, service.Name, err.Error())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Clean up old events if there's more than twice the maximum number of events
|
|
|
|
// This lets us both keep the table clean without impacting performance too much
|
|
|
|
// (since we're only deleting MaximumNumberOfEvents at a time instead of 1)
|
2021-07-15 07:56:49 +02:00
|
|
|
if numberOfEvents > eventsCleanUpThreshold {
|
2021-07-14 07:40:27 +02:00
|
|
|
if err = s.deleteOldServiceEvents(tx, serviceID); err != nil {
|
2021-07-13 04:53:14 +02:00
|
|
|
log.Printf("[database][Insert] Failed to delete old events for group=%s; service=%s: %s", service.Group, service.Name, err.Error())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Second, we need to insert the result.
|
2021-07-14 07:40:27 +02:00
|
|
|
if err = s.insertResult(tx, serviceID, result); err != nil {
|
2021-07-13 04:53:14 +02:00
|
|
|
log.Printf("[database][Insert] Failed to insert result for group=%s; service=%s: %s", service.Group, service.Name, err.Error())
|
2021-07-14 07:40:27 +02:00
|
|
|
_ = tx.Rollback() // If we can't insert the result, we'll rollback now since there's no point continuing
|
2021-07-14 05:07:02 +02:00
|
|
|
return
|
2021-07-13 04:53:14 +02:00
|
|
|
}
|
|
|
|
// Clean up old results
|
|
|
|
numberOfResults, err := s.getNumberOfResultsByServiceID(tx, serviceID)
|
|
|
|
if err != nil {
|
2021-07-14 05:07:02 +02:00
|
|
|
log.Printf("[database][Insert] Failed to retrieve total number of results for group=%s; service=%s: %s", service.Group, service.Name, err.Error())
|
2021-07-14 07:40:27 +02:00
|
|
|
} else {
|
2021-07-15 07:56:49 +02:00
|
|
|
if numberOfResults > resultsCleanUpThreshold {
|
2021-07-14 07:40:27 +02:00
|
|
|
if err = s.deleteOldServiceResults(tx, serviceID); err != nil {
|
|
|
|
log.Printf("[database][Insert] Failed to delete old results for group=%s; service=%s: %s", service.Group, service.Name, err.Error())
|
|
|
|
}
|
|
|
|
}
|
2021-07-13 04:53:14 +02:00
|
|
|
}
|
2021-07-14 07:40:27 +02:00
|
|
|
// Finally, we need to insert the uptime data.
|
|
|
|
// Because the uptime data significantly outlives the results, we can't rely on the results for determining the uptime
|
|
|
|
if err = s.updateServiceUptime(tx, serviceID, result); err != nil {
|
|
|
|
log.Printf("[database][Insert] Failed to update uptime for group=%s; service=%s: %s", service.Group, service.Name, err.Error())
|
|
|
|
}
|
|
|
|
// Clean up old uptime entries
|
|
|
|
ageOfOldestUptimeEntry, err := s.getAgeOfOldestServiceUptimeEntry(tx, serviceID)
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("[database][Insert] Failed to retrieve oldest service uptime entry for group=%s; service=%s: %s", service.Group, service.Name, err.Error())
|
|
|
|
} else {
|
|
|
|
if ageOfOldestUptimeEntry > uptimeCleanUpThreshold {
|
|
|
|
if err = s.deleteOldUptimeEntries(tx, serviceID, time.Now().Add(-(uptimeRetention + time.Hour))); err != nil {
|
|
|
|
log.Printf("[database][Insert] Failed to delete old uptime entries for group=%s; service=%s: %s", service.Group, service.Name, err.Error())
|
|
|
|
}
|
2021-07-13 04:53:14 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
//log.Printf("[database][Insert] Successfully inserted result in duration=%dns", time.Since(start).Nanoseconds())
|
|
|
|
if err = tx.Commit(); err != nil {
|
|
|
|
_ = tx.Rollback()
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// DeleteAllServiceStatusesNotInKeys removes all rows owned by a service whose key is not within the keys provided
|
|
|
|
func (s *Store) DeleteAllServiceStatusesNotInKeys(keys []string) int {
|
2021-07-16 04:07:30 +02:00
|
|
|
if len(keys) == 0 {
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
args := make([]interface{}, 0, len(keys))
|
|
|
|
for i := range keys {
|
|
|
|
args = append(args, keys[i])
|
|
|
|
}
|
|
|
|
_, err := s.db.Exec(fmt.Sprintf("DELETE FROM service WHERE service_key NOT IN (%s)", strings.Trim(strings.Repeat("?,", len(keys)), ",")), args...)
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("err: %v", err)
|
|
|
|
}
|
|
|
|
return 0
|
2021-07-13 04:53:14 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Clear deletes everything from the store
|
|
|
|
func (s *Store) Clear() {
|
|
|
|
_, _ = s.db.Exec("DELETE FROM service")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Save does nothing, because this store is immediately persistent.
|
|
|
|
func (s *Store) Save() error {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Close the database handle
|
|
|
|
func (s *Store) Close() {
|
|
|
|
_ = s.db.Close()
|
|
|
|
}
|
|
|
|
|
2021-07-14 05:23:14 +02:00
|
|
|
// insertService inserts a service in the store and returns the generated id of said service
|
|
|
|
func (s *Store) insertService(tx *sql.Tx, service *core.Service) (int64, error) {
|
|
|
|
//log.Printf("[database][insertService] Inserting service with group=%s and name=%s", service.Group, service.Name)
|
|
|
|
result, err := tx.Exec(
|
|
|
|
"INSERT INTO service (service_key, service_name, service_group) VALUES ($1, $2, $3)",
|
|
|
|
service.Key(),
|
|
|
|
service.Name,
|
|
|
|
service.Group,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
return result.LastInsertId()
|
|
|
|
}
|
|
|
|
|
|
|
|
// insertEvent inserts a service event in the store
|
|
|
|
func (s *Store) insertEvent(tx *sql.Tx, serviceID int64, event *core.Event) error {
|
|
|
|
_, err := tx.Exec(
|
|
|
|
"INSERT INTO service_event (service_id, event_type, event_timestamp) VALUES ($1, $2, $3)",
|
|
|
|
serviceID,
|
|
|
|
event.Type,
|
|
|
|
event.Timestamp,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// insertResult inserts a result in the store
|
|
|
|
func (s *Store) insertResult(tx *sql.Tx, serviceID int64, result *core.Result) error {
|
|
|
|
res, err := tx.Exec(
|
|
|
|
`
|
|
|
|
INSERT INTO service_result (service_id, success, errors, connected, status, dns_rcode, certificate_expiration, hostname, ip, duration, timestamp)
|
|
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
|
|
|
`,
|
|
|
|
serviceID,
|
|
|
|
result.Success,
|
|
|
|
strings.Join(result.Errors, arraySeparator),
|
|
|
|
result.Connected,
|
|
|
|
result.HTTPStatus,
|
|
|
|
result.DNSRCode,
|
|
|
|
result.CertificateExpiration,
|
|
|
|
result.Hostname,
|
|
|
|
result.IP,
|
|
|
|
result.Duration,
|
|
|
|
result.Timestamp,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
serviceResultID, err := res.LastInsertId()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return s.insertConditionResults(tx, serviceResultID, result.ConditionResults)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Store) insertConditionResults(tx *sql.Tx, serviceResultID int64, conditionResults []*core.ConditionResult) error {
|
|
|
|
var err error
|
|
|
|
for _, cr := range conditionResults {
|
|
|
|
_, err = tx.Exec("INSERT INTO service_result_condition (service_result_id, condition, success) VALUES ($1, $2, $3)",
|
|
|
|
serviceResultID,
|
|
|
|
cr.Condition,
|
|
|
|
cr.Success,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-07-14 07:40:27 +02:00
|
|
|
func (s *Store) updateServiceUptime(tx *sql.Tx, serviceID int64, result *core.Result) error {
|
|
|
|
unixTimestampFlooredAtHour := result.Timestamp.Truncate(time.Hour).Unix()
|
|
|
|
var successfulExecutions int
|
|
|
|
if result.Success {
|
|
|
|
successfulExecutions = 1
|
|
|
|
}
|
|
|
|
_, err := tx.Exec(
|
|
|
|
`
|
|
|
|
INSERT INTO service_uptime (service_id, hour_unix_timestamp, total_executions, successful_executions, total_response_time)
|
|
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
|
|
ON CONFLICT(service_id, hour_unix_timestamp) DO UPDATE SET
|
|
|
|
total_executions = excluded.total_executions + total_executions,
|
|
|
|
successful_executions = excluded.successful_executions + successful_executions,
|
|
|
|
total_response_time = excluded.total_response_time + total_response_time
|
|
|
|
`,
|
|
|
|
serviceID,
|
|
|
|
unixTimestampFlooredAtHour,
|
|
|
|
1,
|
|
|
|
successfulExecutions,
|
|
|
|
result.Duration.Milliseconds(),
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-07-14 04:59:43 +02:00
|
|
|
func (s *Store) getAllServiceKeys(tx *sql.Tx) (keys []string, err error) {
|
|
|
|
rows, err := tx.Query("SELECT service_key FROM service")
|
2021-07-14 04:17:27 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
for rows.Next() {
|
|
|
|
var key string
|
|
|
|
_ = rows.Scan(&key)
|
|
|
|
keys = append(keys, key)
|
|
|
|
}
|
|
|
|
_ = rows.Close()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-07-15 04:26:51 +02:00
|
|
|
func (s *Store) getServiceStatusByKey(tx *sql.Tx, key string, parameters *paging.ServiceStatusParams) (*core.ServiceStatus, error) {
|
2021-07-16 04:07:30 +02:00
|
|
|
serviceID, serviceGroup, serviceName, err := s.getServiceIDGroupAndNameByKey(tx, key)
|
2021-07-14 04:17:27 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2021-07-14 07:53:14 +02:00
|
|
|
serviceStatus := core.NewServiceStatus(key, serviceGroup, serviceName)
|
2021-07-15 04:26:51 +02:00
|
|
|
if parameters.EventsPageSize > 0 {
|
|
|
|
if serviceStatus.Events, err = s.getEventsByServiceID(tx, serviceID, parameters.EventsPage, parameters.EventsPageSize); err != nil {
|
2021-07-14 04:17:27 +02:00
|
|
|
log.Printf("[database][getServiceStatusByKey] Failed to retrieve events for key=%s: %s", key, err.Error())
|
|
|
|
}
|
|
|
|
}
|
2021-07-15 04:26:51 +02:00
|
|
|
if parameters.ResultsPageSize > 0 {
|
|
|
|
if serviceStatus.Results, err = s.getResultsByServiceID(tx, serviceID, parameters.ResultsPage, parameters.ResultsPageSize); err != nil {
|
2021-07-14 04:17:27 +02:00
|
|
|
log.Printf("[database][getServiceStatusByKey] Failed to retrieve results for key=%s: %s", key, err.Error())
|
|
|
|
}
|
|
|
|
}
|
2021-07-15 04:26:51 +02:00
|
|
|
if parameters.IncludeUptime {
|
2021-07-14 07:40:27 +02:00
|
|
|
now := time.Now()
|
|
|
|
serviceStatus.Uptime.LastHour, _, _ = s.getServiceUptime(tx, serviceID, now.Add(-time.Hour), now)
|
|
|
|
serviceStatus.Uptime.LastTwentyFourHours, _, _ = s.getServiceUptime(tx, serviceID, now.Add(-24*time.Hour), now)
|
|
|
|
serviceStatus.Uptime.LastSevenDays, _, _ = s.getServiceUptime(tx, serviceID, now.Add(-7*24*time.Hour), now)
|
2021-07-14 05:21:12 +02:00
|
|
|
}
|
2021-07-14 04:17:27 +02:00
|
|
|
return serviceStatus, nil
|
|
|
|
}
|
|
|
|
|
2021-07-14 04:59:43 +02:00
|
|
|
func (s *Store) getServiceIDGroupAndNameByKey(tx *sql.Tx, key string) (id int64, group, name string, err error) {
|
|
|
|
rows, err := tx.Query("SELECT service_id, service_group, service_name FROM service WHERE service_key = $1 LIMIT 1", key)
|
2021-07-12 06:56:30 +02:00
|
|
|
if err != nil {
|
|
|
|
return 0, "", "", err
|
|
|
|
}
|
|
|
|
for rows.Next() {
|
2021-07-14 04:17:27 +02:00
|
|
|
_ = rows.Scan(&id, &group, &name)
|
2021-07-12 06:56:30 +02:00
|
|
|
}
|
2021-07-13 04:53:14 +02:00
|
|
|
_ = rows.Close()
|
2021-07-12 06:56:30 +02:00
|
|
|
if id == 0 {
|
|
|
|
return 0, "", "", errServiceNotFoundInDatabase
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-07-14 04:59:43 +02:00
|
|
|
func (s *Store) getEventsByServiceID(tx *sql.Tx, serviceID int64, page, pageSize int) (events []*core.Event, err error) {
|
|
|
|
rows, err := tx.Query(
|
2021-07-14 04:17:27 +02:00
|
|
|
`
|
|
|
|
SELECT event_type, event_timestamp
|
|
|
|
FROM service_event
|
|
|
|
WHERE service_id = $1
|
2021-07-16 04:07:30 +02:00
|
|
|
ORDER BY service_event_id ASC
|
2021-07-14 04:17:27 +02:00
|
|
|
LIMIT $2 OFFSET $3
|
|
|
|
`,
|
|
|
|
serviceID,
|
|
|
|
pageSize,
|
|
|
|
(page-1)*pageSize,
|
|
|
|
)
|
2021-07-12 06:56:30 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
for rows.Next() {
|
|
|
|
event := &core.Event{}
|
|
|
|
_ = rows.Scan(&event.Type, &event.Timestamp)
|
|
|
|
events = append(events, event)
|
|
|
|
}
|
|
|
|
_ = rows.Close()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-07-14 04:59:43 +02:00
|
|
|
func (s *Store) getResultsByServiceID(tx *sql.Tx, serviceID int64, page, pageSize int) (results []*core.Result, err error) {
|
2021-07-14 04:17:27 +02:00
|
|
|
rows, err := tx.Query(
|
2021-07-12 10:25:25 +02:00
|
|
|
`
|
|
|
|
SELECT service_result_id, success, errors, connected, status, dns_rcode, certificate_expiration, hostname, ip, duration, timestamp
|
2021-07-12 06:56:30 +02:00
|
|
|
FROM service_result
|
|
|
|
WHERE service_id = $1
|
2021-07-16 23:48:38 +02:00
|
|
|
ORDER BY service_result_id DESC -- Normally, we'd sort by timestamp, but sorting by service_result_id is faster
|
2021-07-14 04:17:27 +02:00
|
|
|
LIMIT $2 OFFSET $3
|
2021-07-13 04:53:14 +02:00
|
|
|
`,
|
2021-07-16 23:48:38 +02:00
|
|
|
//`
|
|
|
|
// SELECT * FROM (
|
|
|
|
// SELECT service_result_id, success, errors, connected, status, dns_rcode, certificate_expiration, hostname, ip, duration, timestamp
|
|
|
|
// FROM service_result
|
|
|
|
// WHERE service_id = $1
|
|
|
|
// ORDER BY service_result_id DESC -- Normally, we'd sort by timestamp, but sorting by service_result_id is faster
|
|
|
|
// LIMIT $2 OFFSET $3
|
|
|
|
// )
|
|
|
|
// ORDER BY service_result_id ASC -- Normally, we'd sort by timestamp, but sorting by service_result_id is faster
|
|
|
|
//`,
|
2021-07-12 06:56:30 +02:00
|
|
|
serviceID,
|
2021-07-14 04:17:27 +02:00
|
|
|
pageSize,
|
|
|
|
(page-1)*pageSize,
|
2021-07-12 06:56:30 +02:00
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2021-07-12 07:06:44 +02:00
|
|
|
idResultMap := make(map[int64]*core.Result)
|
2021-07-12 06:56:30 +02:00
|
|
|
for rows.Next() {
|
|
|
|
result := &core.Result{}
|
|
|
|
var id int64
|
2021-07-12 10:25:25 +02:00
|
|
|
var joinedErrors string
|
|
|
|
_ = rows.Scan(&id, &result.Success, &joinedErrors, &result.Connected, &result.HTTPStatus, &result.DNSRCode, &result.CertificateExpiration, &result.Hostname, &result.IP, &result.Duration, &result.Timestamp)
|
2021-07-16 04:07:30 +02:00
|
|
|
if len(joinedErrors) != 0 {
|
|
|
|
result.Errors = strings.Split(joinedErrors, arraySeparator)
|
|
|
|
}
|
2021-07-16 23:48:38 +02:00
|
|
|
//results = append(results, result)
|
|
|
|
// This is faster than using a subselect
|
|
|
|
results = append([]*core.Result{result}, results...)
|
2021-07-12 07:06:44 +02:00
|
|
|
idResultMap[id] = result
|
2021-07-12 06:56:30 +02:00
|
|
|
}
|
|
|
|
_ = rows.Close()
|
|
|
|
// Get the conditionResults
|
2021-07-12 07:06:44 +02:00
|
|
|
for serviceResultID, result := range idResultMap {
|
2021-07-14 04:17:27 +02:00
|
|
|
rows, err = tx.Query(
|
2021-07-12 10:25:25 +02:00
|
|
|
`
|
2021-07-16 04:07:30 +02:00
|
|
|
SELECT condition, success
|
2021-07-12 10:25:25 +02:00
|
|
|
FROM service_result_condition
|
|
|
|
WHERE service_result_id = $1
|
|
|
|
`,
|
2021-07-12 06:56:30 +02:00
|
|
|
serviceResultID,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
for rows.Next() {
|
|
|
|
conditionResult := &core.ConditionResult{}
|
2021-07-16 04:07:30 +02:00
|
|
|
if err = rows.Scan(&conditionResult.Condition, &conditionResult.Success); err != nil {
|
|
|
|
return
|
|
|
|
}
|
2021-07-12 07:06:44 +02:00
|
|
|
result.ConditionResults = append(result.ConditionResults, conditionResult)
|
2021-07-12 06:56:30 +02:00
|
|
|
}
|
|
|
|
_ = rows.Close()
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-07-14 07:40:27 +02:00
|
|
|
func (s *Store) getServiceUptime(tx *sql.Tx, serviceID int64, from, to time.Time) (uptime float64, avgResponseTime time.Duration, err error) {
|
|
|
|
rows, err := tx.Query(
|
|
|
|
`
|
|
|
|
SELECT SUM(total_executions), SUM(successful_executions), SUM(total_response_time)
|
|
|
|
FROM service_uptime
|
|
|
|
WHERE service_id = $1
|
|
|
|
AND hour_unix_timestamp >= $2
|
|
|
|
AND hour_unix_timestamp <= $3
|
|
|
|
`,
|
|
|
|
serviceID,
|
|
|
|
from.Unix(),
|
|
|
|
to.Unix(),
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return 0, 0, err
|
|
|
|
}
|
|
|
|
var totalExecutions, totalSuccessfulExecutions, totalResponseTime int
|
|
|
|
for rows.Next() {
|
|
|
|
_ = rows.Scan(&totalExecutions, &totalSuccessfulExecutions, &totalResponseTime)
|
|
|
|
break
|
|
|
|
}
|
|
|
|
_ = rows.Close()
|
|
|
|
if totalExecutions > 0 {
|
|
|
|
uptime = float64(totalSuccessfulExecutions) / float64(totalExecutions)
|
|
|
|
avgResponseTime = time.Duration(float64(totalResponseTime)/float64(totalExecutions)) * time.Millisecond
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-07-12 08:54:16 +02:00
|
|
|
func (s *Store) getServiceID(tx *sql.Tx, service *core.Service) (int64, error) {
|
2021-07-14 04:30:30 +02:00
|
|
|
rows, err := tx.Query("SELECT service_id FROM service WHERE service_key = $1", service.Key())
|
2021-07-12 06:56:30 +02:00
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
var id int64
|
|
|
|
var found bool
|
|
|
|
for rows.Next() {
|
|
|
|
_ = rows.Scan(&id)
|
|
|
|
found = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
_ = rows.Close()
|
|
|
|
if !found {
|
|
|
|
return 0, errServiceNotFoundInDatabase
|
|
|
|
}
|
|
|
|
return id, nil
|
|
|
|
}
|
|
|
|
|
2021-07-12 08:54:16 +02:00
|
|
|
func (s *Store) getNumberOfEventsByServiceID(tx *sql.Tx, serviceID int64) (int64, error) {
|
|
|
|
rows, err := tx.Query("SELECT COUNT(1) FROM service_event WHERE service_id = $1", serviceID)
|
2021-07-12 06:56:30 +02:00
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
var numberOfEvents int64
|
|
|
|
var found bool
|
|
|
|
for rows.Next() {
|
|
|
|
_ = rows.Scan(&numberOfEvents)
|
|
|
|
found = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
_ = rows.Close()
|
|
|
|
if !found {
|
|
|
|
return 0, errNoRowsReturned
|
|
|
|
}
|
|
|
|
return numberOfEvents, nil
|
|
|
|
}
|
|
|
|
|
2021-07-13 04:53:14 +02:00
|
|
|
func (s *Store) getNumberOfResultsByServiceID(tx *sql.Tx, serviceID int64) (int64, error) {
|
|
|
|
rows, err := tx.Query("SELECT COUNT(1) FROM service_result WHERE service_id = $1", serviceID)
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
var numberOfResults int64
|
|
|
|
var found bool
|
|
|
|
for rows.Next() {
|
|
|
|
_ = rows.Scan(&numberOfResults)
|
|
|
|
found = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
_ = rows.Close()
|
|
|
|
if !found {
|
|
|
|
return 0, errNoRowsReturned
|
|
|
|
}
|
|
|
|
return numberOfResults, nil
|
|
|
|
}
|
|
|
|
|
2021-07-14 07:40:27 +02:00
|
|
|
func (s *Store) getAgeOfOldestServiceUptimeEntry(tx *sql.Tx, serviceID int64) (time.Duration, error) {
|
|
|
|
rows, err := tx.Query(
|
|
|
|
`
|
|
|
|
SELECT hour_unix_timestamp
|
|
|
|
FROM service_uptime
|
|
|
|
WHERE service_id = $1
|
|
|
|
ORDER BY hour_unix_timestamp
|
|
|
|
LIMIT 1
|
|
|
|
`,
|
|
|
|
serviceID,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
var oldestServiceUptimeUnixTimestamp int64
|
|
|
|
var found bool
|
|
|
|
for rows.Next() {
|
|
|
|
_ = rows.Scan(&oldestServiceUptimeUnixTimestamp)
|
|
|
|
found = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
_ = rows.Close()
|
|
|
|
if !found {
|
|
|
|
return 0, errNoRowsReturned
|
|
|
|
}
|
|
|
|
return time.Since(time.Unix(oldestServiceUptimeUnixTimestamp, 0)), nil
|
|
|
|
}
|
|
|
|
|
2021-07-12 08:54:16 +02:00
|
|
|
func (s *Store) getLastServiceResultSuccessValue(tx *sql.Tx, serviceID int64) (bool, error) {
|
2021-07-12 10:25:25 +02:00
|
|
|
rows, err := tx.Query("SELECT success FROM service_result WHERE service_id = $1 ORDER BY service_result_id DESC LIMIT 1", serviceID)
|
2021-07-12 06:56:30 +02:00
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
var success bool
|
|
|
|
var found bool
|
|
|
|
for rows.Next() {
|
|
|
|
_ = rows.Scan(&success)
|
|
|
|
found = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
_ = rows.Close()
|
|
|
|
if !found {
|
|
|
|
return false, errNoRowsReturned
|
|
|
|
}
|
|
|
|
return success, nil
|
|
|
|
}
|
|
|
|
|
2021-07-13 05:11:33 +02:00
|
|
|
// deleteOldServiceEvents deletes old service events that are no longer needed
|
|
|
|
func (s *Store) deleteOldServiceEvents(tx *sql.Tx, serviceID int64) error {
|
2021-07-13 04:53:14 +02:00
|
|
|
_, err := tx.Exec(
|
2021-07-12 10:25:25 +02:00
|
|
|
`
|
|
|
|
DELETE FROM service_event
|
|
|
|
WHERE service_id = $1
|
|
|
|
AND service_event_id NOT IN (
|
|
|
|
SELECT service_event_id
|
|
|
|
FROM service_event
|
|
|
|
WHERE service_id = $1
|
|
|
|
ORDER BY service_event_id DESC
|
|
|
|
LIMIT $2
|
|
|
|
)
|
|
|
|
`,
|
|
|
|
serviceID,
|
|
|
|
core.MaximumNumberOfEvents,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2021-07-13 04:53:14 +02:00
|
|
|
//rowsAffected, _ := result.RowsAffected()
|
|
|
|
//log.Printf("deleted %d rows from service_event", rowsAffected)
|
2021-07-12 10:25:25 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-07-13 05:11:33 +02:00
|
|
|
// deleteOldServiceResults deletes old service results that are no longer needed
|
|
|
|
func (s *Store) deleteOldServiceResults(tx *sql.Tx, serviceID int64) error {
|
2021-07-13 04:53:14 +02:00
|
|
|
_, err := tx.Exec(
|
|
|
|
`
|
|
|
|
DELETE FROM service_result
|
|
|
|
WHERE service_id = $1
|
|
|
|
AND service_result_id NOT IN (
|
|
|
|
SELECT service_result_id
|
|
|
|
FROM service_result
|
|
|
|
WHERE service_id = $1
|
|
|
|
ORDER BY service_result_id DESC
|
|
|
|
LIMIT $2
|
|
|
|
)
|
|
|
|
`,
|
|
|
|
serviceID,
|
|
|
|
core.MaximumNumberOfResults,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
//rowsAffected, _ := result.RowsAffected()
|
|
|
|
//log.Printf("deleted %d rows from service_result", rowsAffected)
|
2021-07-12 06:56:30 +02:00
|
|
|
return nil
|
|
|
|
}
|
2021-07-14 07:40:27 +02:00
|
|
|
|
|
|
|
func (s *Store) deleteOldUptimeEntries(tx *sql.Tx, serviceID int64, maxAge time.Time) error {
|
|
|
|
_, err := tx.Exec("DELETE FROM service_uptime WHERE service_id = $1 AND hour_unix_timestamp < $2", serviceID, maxAge.Unix())
|
|
|
|
//if err != nil {
|
|
|
|
// return err
|
|
|
|
//}
|
|
|
|
//rowsAffected, _ := result.RowsAffected()
|
|
|
|
//log.Printf("deleted %d rows from service_uptime", rowsAffected)
|
|
|
|
return err
|
|
|
|
}
|