Compare commits

..
13 Commits
Author SHA1 Message Date
TwinProduction d65cebb1fb Remove Uptime.Last* parameters 2021-08-13 01:25:50 -04:00
TwinProduction 0b6fc6b520 Add GetUptimeByKey to store interface 2021-08-13 01:25:50 -04:00
TwinProduction 968b960283 Add memory.db to .gitignore 2021-08-13 01:25:50 -04:00
TwinProduction 77ba2169cf Close #125: Add more uptime badge colors 2021-08-11 21:05:51 -04:00
TwinProduction f6c32a90ac Add small comment 2021-08-10 19:58:19 -04:00
TwinProduction 932a67d9e7 Update list of sponsors 2021-08-09 19:03:07 -04:00
TwinProduction ee414df03f Add missing comment 2021-08-08 20:16:12 -04:00
TwinProduction 718f8260bb Register missing struct 2021-08-07 12:22:39 -04:00
TwinProduction 3cbe068fc1 Rename storage type inmemory to memory
This is technically a breaking change, but given how long ago this field was implemented as well as the fact that this is the default value if the type is not specified, I doubt anybody's explicitly setting it as inmemory
2021-08-07 12:11:35 -04:00
TwinProduction 4ada6ee7c9 Remove unneeded constants 2021-08-07 11:54:22 -04:00
TwinProduction 1e28905c8d Add clarification on hack 2021-08-07 11:46:58 -04:00
TwinProduction 4dbde07b85 Fix typo 2021-07-30 18:57:43 -04:00
TwinProduction 8f35679299 Improve documentation formatting 2021-07-30 18:56:05 -04:00
32 changed files with 331 additions and 301 deletions

No files matched your search

+2 -1
View File
@@ -4,4 +4,5 @@ gatus
db.db
config/config.yml
db.db-shm
db.db-wal
db.db-wal
memory.db
+46 -43
View File
@@ -68,7 +68,7 @@ For more details, see [Usage](#usage)
- [Service groups](#service-groups)
- [Exposing Gatus on a custom port](#exposing-gatus-on-a-custom-port)
- [Uptime Badges (ALPHA)](#uptime-badges)
- [API](#API)
- [API](#api)
- [High level design overview](#high-level-design-overview)
- [Sponsors](#sponsors)
@@ -141,18 +141,18 @@ If you want to test it locally, see [Docker](#docker).
|:---------------------------------------- |:----------------------------------------------------------------------------- |:-------------- |
| `debug` | Whether to enable debug logs. | `false` |
| `metrics` | Whether to expose metrics at /metrics. | `false` |
| `storage` | Storage configuration. See [Storage](#storage). | `{}` |
| `storage` | Storage configuration. <br />See [Storage](#storage). | `{}` |
| `services` | List of services to monitor. | Required `[]` |
| `services[].name` | Name of the service. Can be anything. | Required `""` |
| `services[].group` | Group name. Used to group multiple services together on the dashboard. See [Service groups](#service-groups). | `""` |
| `services[].group` | Group name. Used to group multiple services together on the dashboard. <br />See [Service groups](#service-groups). | `""` |
| `services[].url` | URL to send the request to. | Required `""` |
| `services[].method` | Request method. | `GET` |
| `services[].conditions` | Conditions used to determine the health of the service. See [Conditions](#conditions). | `[]` |
| `services[].conditions` | Conditions used to determine the health of the service. <br />See [Conditions](#conditions). | `[]` |
| `services[].interval` | Duration to wait between every status check. | `60s` |
| `services[].graphql` | Whether to wrap the body in a query param (`{"query":"$body"}`). | `false` |
| `services[].body` | Request body. | `""` |
| `services[].headers` | Request headers. | `{}` |
| `services[].dns` | Configuration for a service of type DNS. See [Monitoring a service using DNS queries](#monitoring-a-service-using-dns-queries). | `""` |
| `services[].dns` | Configuration for a service of type DNS. <br />See [Monitoring a service using DNS queries](#monitoring-a-service-using-dns-queries). | `""` |
| `services[].dns.query-type` | Query type for DNS service. | `""` |
| `services[].dns.query-name` | Query name for DNS service. | `""` |
| `services[].alerts[].type` | Type of alert. Valid types: `slack`, `discord`, `pagerduty`, `twilio`, `mattermost`, `messagebird`, `teams` `custom`. | Required `""` |
@@ -161,14 +161,14 @@ If you want to test it locally, see [Docker](#docker).
| `services[].alerts[].success-threshold` | Number of successes in a row before an ongoing incident is marked as resolved. | `2` |
| `services[].alerts[].send-on-resolved` | Whether to send a notification once a triggered alert is marked as resolved. | `false` |
| `services[].alerts[].description` | Description of the alert. Will be included in the alert sent. | `""` |
| `services[].client` | Client configuration. See [Client configuration](#client-configuration). | `{}` |
| `alerting` | Configuration for alerting. See [Alerting](#alerting). | `{}` |
| `services[].client` | Client configuration. <br />See [Client configuration](#client-configuration). | `{}` |
| `alerting` | Configuration for alerting. <br />See [Alerting](#alerting). | `{}` |
| `security` | Security configuration. | `{}` |
| `security.basic` | Basic authentication security configuration. | `{}` |
| `security.basic.username` | Username for Basic authentication. | Required `""` |
| `security.basic.password-sha512` | Password's SHA512 hash for Basic authentication. | Required `""` |
| `disable-monitoring-lock` | Whether to [disable the monitoring lock](#disable-monitoring-lock). | `false` |
| `skip-invalid-config-update` | Whether to ignore invalid configuration update. See [Reloading configuration on the fly](#reloading-configuration-on-the-fly). | `false` |
| `skip-invalid-config-update` | Whether to ignore invalid configuration update. <br />See [Reloading configuration on the fly](#reloading-configuration-on-the-fly). | `false` |
| `web` | Web configuration. | `{}` |
| `web.address` | Address to listen on. | `0.0.0.0` |
| `web.port` | Port to listen on. | `8080` |
@@ -229,10 +229,10 @@ Here are some examples of conditions you can use:
| Parameter | Description | Default |
|:------------------ |:-------------------------------------------------------------------------------------- |:-------------- |
| `storage` | Storage configuration | `{}` |
| `storage.file` | File to persist the data in. If the type is `inmemory`, data is persisted on interval. | `""` |
| `storage.type` | Type of storage. Valid types: `inmemory`, `sqlite`. | `"inmemory"` |
| `storage.file` | File to persist the data in. If the type is `memory`, data is persisted on interval. | `""` |
| `storage.type` | Type of storage. Valid types: `memory`, `sqlite`. | `"memory"` |
- If `storage.type` is `inmemory` (default) and `storage.file` is set to a non-blank value.
- If `storage.type` is `memory` (default) and `storage.file` is set to a non-blank value.
Furthermore, the data is periodically persisted, but everything remains in memory.
- If `storage.type` is `sqlite`, `storage.file` must not be blank.
```yaml
@@ -288,15 +288,15 @@ ignored.
| Parameter | Description | Default |
|:-----------------------|:---------------------------------------------------------------------------------------------------------------------- |:-------|
| `alerting.discord` | Configuration for alerts of type `discord`. See [Configuring Discord alerts](#configuring-discord-alerts). | `{}` |
| `alerting.mattermost` | Configuration for alerts of type `mattermost`. See [Configuring Mattermost alerts](#configuring-mattermost-alerts). | `{}` |
| `alerting.messagebird` | Configuration for alerts of type `messagebird`. See [Configuring Messagebird alerts](#configuring-messagebird-alerts). | `{}` |
| `alerting.pagerduty` | Configuration for alerts of type `pagerduty`. See [Configuring PagerDuty alerts](#configuring-pagerduty-alerts). | `{}` |
| `alerting.slack` | Configuration for alerts of type `slack`. See [Configuring Slack alerts](#configuring-slack-alerts). | `{}` |
| `alerting.teams` | Configuration for alerts of type `teams`. See [Configuring Teams alerts](#configuring-teams-alerts). | `{}` |
| `alerting.telegram` | Configuration for alerts of type `telegram`. See [Configuring Telegram alerts](#configuring-telegram-alerts). | `{}` |
| `alerting.twilio` | Settings for alerts of type `twilio`. See [Configuring Twilio alerts](#configuring-twilio-alerts). | `{}` |
| `alerting.custom` | Configuration for custom actions on failure or alerts. See [Configuring Custom alerts](#configuring-custom-alerts). | `{}` |
| `alerting.discord` | Configuration for alerts of type `discord`. <br />See [Configuring Discord alerts](#configuring-discord-alerts). | `{}` |
| `alerting.mattermost` | Configuration for alerts of type `mattermost`. <br />See [Configuring Mattermost alerts](#configuring-mattermost-alerts). | `{}` |
| `alerting.messagebird` | Configuration for alerts of type `messagebird`. <br />See [Configuring Messagebird alerts](#configuring-messagebird-alerts). | `{}` |
| `alerting.pagerduty` | Configuration for alerts of type `pagerduty`. <br />See [Configuring PagerDuty alerts](#configuring-pagerduty-alerts). | `{}` |
| `alerting.slack` | Configuration for alerts of type `slack`. <br />See [Configuring Slack alerts](#configuring-slack-alerts). | `{}` |
| `alerting.teams` | Configuration for alerts of type `teams`. <br />See [Configuring Teams alerts](#configuring-teams-alerts). | `{}` |
| `alerting.telegram` | Configuration for alerts of type `telegram`. <br />See [Configuring Telegram alerts](#configuring-telegram-alerts). | `{}` |
| `alerting.twilio` | Settings for alerts of type `twilio`. <br />See [Configuring Twilio alerts](#configuring-twilio-alerts). | `{}` |
| `alerting.custom` | Configuration for custom actions on failure or alerts. <br />See [Configuring Custom alerts](#configuring-custom-alerts). | `{}` |
#### Configuring Discord alerts
@@ -304,7 +304,7 @@ ignored.
|:---------------------------------------- |:-------------------------------------------- |:-------------- |
| `alerting.discord` | Configuration for alerts of type `discord` | `{}` |
| `alerting.discord.webhook-url` | Discord Webhook URL | Required `""` |
| `alerting.discord.default-alert` | Default alert configuration. See [Setting a default alert](#setting-a-default-alert) | N/A |
| `alerting.discord.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
```yaml
alerting:
@@ -328,12 +328,12 @@ services:
#### Configuring Mattermost alerts
| Parameter | Description | Default |
|:---------------------------------------- |:----------------------------------------------------------------------------- |:-------------- |
| `alerting.mattermost` | Configuration for alerts of type `mattermost` | `{}` |
| `alerting.mattermost.webhook-url` | Mattermost Webhook URL | Required `""` |
| `alerting.mattermost.client` | Client configuration. See [Client configuration](#client-configuration). | `{}` |
| `alerting.mattermost.default-alert` | Default alert configuration. See [Setting a default alert](#setting-a-default-alert) | N/A |
| Parameter | Description | Default |
|:----------------------------------- |:------------------------------------------------------------------------------------------- |:-------------- |
| `alerting.mattermost` | Configuration for alerts of type `mattermost` | `{}` |
| `alerting.mattermost.webhook-url` | Mattermost Webhook URL | Required `""` |
| `alerting.mattermost.client` | Client configuration. <br />See [Client configuration](#client-configuration). | `{}` |
| `alerting.mattermost.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert). | N/A |
```yaml
alerting:
@@ -363,13 +363,13 @@ Here's an example of what the notifications look like:
#### Configuring Messagebird alerts
| Parameter | Description | Default |
|:---------------------------------------- |:----------------------------------------------------------------------------- |:-------------- |
| `alerting.messagebird` | Settings for alerts of type `messagebird` | `{}` |
| `alerting.messagebird.access-key` | Messagebird access key | Required `""` |
| `alerting.messagebird.originator` | The sender of the message | Required `""` |
| `alerting.messagebird.recipients` | The recipients of the message | Required `""` |
| `alerting.messagebird.default-alert` | Default alert configuration. See [Setting a default alert](#setting-a-default-alert) | N/A |
| Parameter | Description | Default |
|:-------------------------------------|:----------------------------------------------------------------------------- |:-------------- |
| `alerting.messagebird` | Settings for alerts of type `messagebird` | `{}` |
| `alerting.messagebird.access-key` | Messagebird access key | Required `""` |
| `alerting.messagebird.originator` | The sender of the message | Required `""` |
| `alerting.messagebird.recipients` | The recipients of the message | Required `""` |
| `alerting.messagebird.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
Example of sending **SMS** text message alert using Messagebird:
```yaml
@@ -400,7 +400,7 @@ services:
|:---------------------------------------- |:----------------------------------------------------------------------------- |:-------------- |
| `alerting.pagerduty` | Configuration for alerts of type `pagerduty` | `{}` |
| `alerting.pagerduty.integration-key` | PagerDuty Events API v2 integration key. | Required `""` |
| `alerting.pagerduty.default-alert` | Default alert configuration. See [Setting a default alert](#setting-a-default-alert) | N/A |
| `alerting.pagerduty.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
It is highly recommended to set `services[].alerts[].send-on-resolved` to `true` for alerts
of type `pagerduty`, because unlike other alerts, the operation resulting from setting said
@@ -435,7 +435,7 @@ services:
|:-------------------------------- |:----------------------------------------------------------------------------- |:-------------- |
| `alerting.slack` | Configuration for alerts of type `slack` | `{}` |
| `alerting.slack.webhook-url` | Slack Webhook URL | Required `""` |
| `alerting.slack.default-alert` | Default alert configuration. See [Setting a default alert](#setting-a-default-alert) | N/A |
| `alerting.slack.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
```yaml
alerting:
@@ -472,7 +472,7 @@ Here's an example of what the notifications look like:
|:-------------------------------- |:----------------------------------------------------------------------------- |:-------------- |
| `alerting.teams` | Configuration for alerts of type `teams` | `{}` |
| `alerting.teams.webhook-url` | Teams Webhook URL | Required `""` |
| `alerting.teams.default-alert` | Default alert configuration. See [Setting a default alert](#setting-a-default-alert) | N/A |
| `alerting.teams.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
```yaml
alerting:
@@ -504,7 +504,7 @@ Here's an example of what the notifications look like:
| `alerting.telegram` | Configuration for alerts of type `telegram` | `{}` |
| `alerting.telegram.token` | Telegram Bot Token | Required `""` |
| `alerting.telegram.id` | Telegram User ID | Required `""` |
| `alerting.telegram.default-alert` | Default alert configuration. See [Setting a default alert](#setting-a-default-alert) | N/A |
| `alerting.telegram.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
```yaml
alerting:
@@ -538,7 +538,7 @@ Here's an example of what the notifications look like:
| `alerting.twilio.token` | Twilio auth token | Required `""` |
| `alerting.twilio.from` | Number to send Twilio alerts from | Required `""` |
| `alerting.twilio.to` | Number to send twilio alerts to | Required `""` |
| `alerting.twilio.default-alert` | Default alert configuration. See [Setting a default alert](#setting-a-default-alert) | N/A |
| `alerting.twilio.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
```yaml
alerting:
@@ -573,8 +573,8 @@ services:
| `alerting.custom.method` | Request method | `GET` |
| `alerting.custom.body` | Custom alerting request body. | `""` |
| `alerting.custom.headers` | Custom alerting request headers | `{}` |
| `alerting.custom.client` | Client configuration. See [Client configuration](#client-configuration). | `{}` |
| `alerting.custom.default-alert` | Default alert configuration. See [Setting a default alert](#setting-a-default-alert) | N/A |
| `alerting.custom.client` | Client configuration. <br />See [Client configuration](#client-configuration). | `{}` |
| `alerting.custom.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
While they're called alerts, you can use this feature to call anything.
@@ -734,7 +734,7 @@ services:
| `kubernetes` | Kubernetes configuration | `{}` |
| `kubernetes.auto-discover` | Whether to enable auto discovery | `false` |
| `kubernetes.cluster-mode` | Cluster mode to use for authenticating. Supported values: `in`, `out` | Required `""` |
| `kubernetes.service-template` | Service template. See `services[]` in [Configuration](#configuration) | Required `nil` |
| `kubernetes.service-template` | Service template. <br />See `services[]` in [Configuration](#configuration) | Required `nil` |
| `kubernetes.excluded-service-suffixes` | List of service suffixes to not monitor (e.g. `canary`) | `[]` |
| `kubernetes.namespaces` | List of configurations for the namespaces from which services will be discovered | `[]` |
| `kubernetes.namespaces[].name` | Namespace name | Required `""` |
@@ -1092,6 +1092,7 @@ web:
port: ${PORT}
```
### Uptime badges
![Uptime 1h](https://status.twinnation.org/api/v1/badges/uptime/1h/core_twinnation-external.svg)
![Uptime 24h](https://status.twinnation.org/api/v1/badges/uptime/24h/core_twinnation-external.svg)
@@ -1127,6 +1128,7 @@ Example: ![Uptime 24h](https://status.twinnation.org/api/v1/badges/uptime/24h/co
If you'd like to see a visual example of each badges available, you can simply navigate to the service's detail page.
### API
Gatus provides a simple read-only API which can be queried in order to programmatically determine service status and history.
@@ -1157,4 +1159,5 @@ No such header is required to query the API.
## Sponsors
You can find the full list of sponsors [here](https://github.com/sponsors/TwinProduction).
[<img src="https://github.com/math280h.png" width="35" />](https://github.com/math280h)
[<img src="https://github.com/math280h.png" width="50" />](https://github.com/math280h)
[<img src="https://github.com/pyroscope-io.png" width="50" />](https://github.com/pyroscope-io)
+1 -1
View File
@@ -177,7 +177,7 @@ func parseAndValidateConfigBytes(yamlBytes []byte) (config *Config, err error) {
func validateStorageConfig(config *Config) error {
if config.Storage == nil {
config.Storage = &storage.Config{
Type: storage.TypeInMemory,
Type: storage.TypeMemory,
}
}
err := storage.Initialize(config.Storage)
+39 -25
View File
@@ -6,9 +6,8 @@ import (
"strings"
"time"
"github.com/TwinProduction/gatus/core"
"github.com/TwinProduction/gatus/storage"
"github.com/TwinProduction/gatus/storage/store/paging"
"github.com/TwinProduction/gatus/storage/store/common"
"github.com/gorilla/mux"
)
@@ -19,22 +18,31 @@ import (
func badgeHandler(writer http.ResponseWriter, request *http.Request) {
variables := mux.Vars(request)
duration := variables["duration"]
if duration != "7d" && duration != "24h" && duration != "1h" {
var from time.Time
switch duration {
case "7d":
from = time.Now().Add(-time.Hour * 24 * 7)
case "24h":
from = time.Now().Add(-time.Hour * 24)
case "1h":
from = time.Now().Add(-time.Hour)
default:
writer.WriteHeader(http.StatusBadRequest)
_, _ = writer.Write([]byte("Durations supported: 7d, 24h, 1h"))
return
}
identifier := variables["identifier"]
key := strings.TrimSuffix(identifier, ".svg")
serviceStatus := storage.Get().GetServiceStatusByKey(key, paging.NewServiceStatusParams().WithUptime())
if serviceStatus == nil {
writer.WriteHeader(http.StatusNotFound)
_, _ = writer.Write([]byte("Requested service not found"))
return
}
if serviceStatus.Uptime == nil {
writer.WriteHeader(http.StatusInternalServerError)
_, _ = writer.Write([]byte("Failed to compute uptime"))
uptime, err := storage.Get().GetUptimeByKey(key, from, time.Now())
if err != nil {
if err == common.ErrServiceNotFound {
writer.WriteHeader(http.StatusNotFound)
} else if err == common.ErrInvalidTimeRange {
writer.WriteHeader(http.StatusBadRequest)
} else {
writer.WriteHeader(http.StatusInternalServerError)
}
_, _ = writer.Write([]byte(err.Error()))
return
}
formattedDate := time.Now().Format(http.TimeFormat)
@@ -42,31 +50,22 @@ func badgeHandler(writer http.ResponseWriter, request *http.Request) {
writer.Header().Set("Date", formattedDate)
writer.Header().Set("Expires", formattedDate)
writer.Header().Set("Content-Type", "image/svg+xml")
_, _ = writer.Write(generateSVG(duration, serviceStatus.Uptime))
_, _ = writer.Write(generateSVG(duration, uptime))
}
func generateSVG(duration string, uptime *core.Uptime) []byte {
func generateSVG(duration string, uptime float64) []byte {
var labelWidth, valueWidth, valueWidthAdjustment int
var color string
var value float64
switch duration {
case "7d":
labelWidth = 65
value = uptime.LastSevenDays
case "24h":
labelWidth = 70
value = uptime.LastTwentyFourHours
case "1h":
labelWidth = 65
value = uptime.LastHour
default:
}
if value >= 0.8 {
color = "#40cc11"
} else {
color = "#c7130a"
}
sanitizedValue := strings.TrimRight(strings.TrimRight(fmt.Sprintf("%.2f", value*100), "0"), ".") + "%"
color := getBadgeColorFromUptime(uptime)
sanitizedValue := strings.TrimRight(strings.TrimRight(fmt.Sprintf("%.2f", uptime*100), "0"), ".") + "%"
if strings.Contains(sanitizedValue, ".") {
valueWidthAdjustment = -10
}
@@ -104,3 +103,18 @@ func generateSVG(duration string, uptime *core.Uptime) []byte {
</svg>`, width, width, labelWidth, color, labelWidth, valueWidth, labelWidth, width, labelX, duration, labelX, duration, valueX, sanitizedValue, valueX, sanitizedValue))
return svg
}
func getBadgeColorFromUptime(uptime float64) string {
if uptime >= 0.975 {
return "#40cc11"
} else if uptime >= 0.95 {
return "#94cc11"
} else if uptime >= 0.9 {
return "#ccc311"
} else if uptime >= 0.8 {
return "#ccb311"
} else if uptime >= 0.5 {
return "#cc8111"
}
return "#c7130a"
}
+32
View File
@@ -0,0 +1,32 @@
package controller
import (
"testing"
)
func TestGetBadgeColorFromUptime(t *testing.T) {
if getBadgeColorFromUptime(1) != "#40cc11" {
t.Error("expected #40cc11 from an uptime of 1, got", getBadgeColorFromUptime(1))
}
if getBadgeColorFromUptime(0.95) != "#94cc11" {
t.Error("expected #94cc11 from an uptime of 0.95, got", getBadgeColorFromUptime(0.95))
}
if getBadgeColorFromUptime(0.9) != "#ccc311" {
t.Error("expected #c9cc11 from an uptime of 0.9, got", getBadgeColorFromUptime(0.9))
}
if getBadgeColorFromUptime(0.85) != "#ccb311" {
t.Error("expected #ccb311 from an uptime of 0.85, got", getBadgeColorFromUptime(0.85))
}
if getBadgeColorFromUptime(0.75) != "#cc8111" {
t.Error("expected #cc8111 from an uptime of 0.75, got", getBadgeColorFromUptime(0.75))
}
if getBadgeColorFromUptime(0.6) != "#cc8111" {
t.Error("expected #cc8111 from an uptime of 0.6, got", getBadgeColorFromUptime(0.6))
}
if getBadgeColorFromUptime(0.25) != "#c7130a" {
t.Error("expected #c7130a from an uptime of 0.25, got", getBadgeColorFromUptime(0.25))
}
if getBadgeColorFromUptime(0) != "#c7130a" {
t.Error("expected #c7130a from an uptime of 0, got", getBadgeColorFromUptime(0))
}
}
+12 -4
View File
@@ -13,10 +13,10 @@ import (
"time"
"github.com/TwinProduction/gatus/config"
"github.com/TwinProduction/gatus/core"
"github.com/TwinProduction/gatus/security"
"github.com/TwinProduction/gatus/storage"
"github.com/TwinProduction/gatus/storage/store/paging"
"github.com/TwinProduction/gatus/storage/store/common"
"github.com/TwinProduction/gatus/storage/store/common/paging"
"github.com/TwinProduction/gocache"
"github.com/TwinProduction/health"
"github.com/gorilla/mux"
@@ -138,20 +138,28 @@ func serviceStatusesHandler(writer http.ResponseWriter, r *http.Request) {
func serviceStatusHandler(writer http.ResponseWriter, r *http.Request) {
page, pageSize := extractPageAndPageSizeFromRequest(r)
vars := mux.Vars(r)
serviceStatus := storage.Get().GetServiceStatusByKey(vars["key"], paging.NewServiceStatusParams().WithResults(page, pageSize).WithEvents(1, core.MaximumNumberOfEvents).WithUptime())
serviceStatus := storage.Get().GetServiceStatusByKey(vars["key"], paging.NewServiceStatusParams().WithResults(page, pageSize).WithEvents(1, common.MaximumNumberOfEvents))
if serviceStatus == nil {
log.Printf("[controller][serviceStatusHandler] Service with key=%s not found", vars["key"])
writer.WriteHeader(http.StatusNotFound)
_, _ = writer.Write([]byte("not found"))
return
}
uptime7Days, _ := storage.Get().GetUptimeByKey(vars["key"], time.Now().Add(-time.Hour*24*7), time.Now())
uptime24Hours, _ := storage.Get().GetUptimeByKey(vars["key"], time.Now().Add(-time.Hour*24), time.Now())
uptime1Hour, _ := storage.Get().GetUptimeByKey(vars["key"], time.Now().Add(-time.Hour), time.Now())
data := map[string]interface{}{
"serviceStatus": serviceStatus,
// The following fields, while present on core.ServiceStatus, are annotated to remain hidden so that we can
// expose only the necessary data on /api/v1/statuses.
// Since the /api/v1/statuses/{key} endpoint does need this data, however, we explicitly expose it here
"events": serviceStatus.Events,
"uptime": serviceStatus.Uptime,
// TODO: remove this in v3.0.0. Not used by front-end, only used for API. Left here for v2.x.x backward compatibility
"uptime": map[string]float64{
"7d": uptime7Days,
"24h": uptime24Hours,
"1h": uptime1Hour,
},
}
output, err := json.Marshal(data)
if err != nil {
+2 -2
View File
@@ -4,7 +4,7 @@ import (
"net/http"
"strconv"
"github.com/TwinProduction/gatus/core"
"github.com/TwinProduction/gatus/storage/store/common"
)
const (
@@ -15,7 +15,7 @@ const (
DefaultPageSize = 20
// MaximumPageSize is the maximum page size allowed
MaximumPageSize = core.MaximumNumberOfResults
MaximumPageSize = common.MaximumNumberOfResults
)
func extractPageAndPageSizeFromRequest(r *http.Request) (page int, pageSize int) {
+4 -8
View File
@@ -1,13 +1,5 @@
package core
const (
// MaximumNumberOfResults is the maximum number of results that ServiceStatus.Results can have
MaximumNumberOfResults = 100
// MaximumNumberOfEvents is the maximum number of events that ServiceStatus.Events can have
MaximumNumberOfEvents = 50
)
// ServiceStatus contains the evaluation Results of a Service
type ServiceStatus struct {
// Name of the service
@@ -34,6 +26,10 @@ type ServiceStatus struct {
// We don't expose this through JSON, because the main dashboard doesn't need to have this data.
// However, the detailed service page does leverage this by including it to a map that will be
// marshalled alongside the ServiceStatus.
//
// TODO: Get rid of this in favor of using the new store.GetUptimeByKey.
// TODO: For memory, store the uptime in a different map? (is that possible, given that we need to persist it through gocache?)
// Deprecated
Uptime *Uptime `json:"-"`
}
+2 -13
View File
@@ -1,21 +1,8 @@
package core
import (
"time"
)
const (
numberOfHoursInTenDays = 10 * 24
sevenDays = 7 * 24 * time.Hour
)
// Uptime is the struct that contains the relevant data for calculating the uptime as well as the uptime itself
// and some other statistics
type Uptime struct {
LastSevenDays float64 `json:"7d"` // Uptime percentage over the past 7 days
LastTwentyFourHours float64 `json:"24h"` // Uptime percentage over the past 24 hours
LastHour float64 `json:"1h"` // Uptime percentage over the past hour
// SuccessfulExecutionsPerHour is a map containing the number of successes (value)
// for every hourly unix timestamps (key)
// Deprecated
@@ -27,6 +14,8 @@ type Uptime struct {
TotalExecutionsPerHour map[int64]uint64 `json:"-"`
// HourlyStatistics is a map containing metrics collected (value) for every hourly unix timestamps (key)
//
// Used only if the storage type is memory
HourlyStatistics map[int64]*HourlyUptimeStatistics `json:"-"`
}
+1 -1
View File
@@ -57,7 +57,7 @@ func Initialize(cfg *Config) error {
if err != nil {
return err
}
case TypeInMemory:
case TypeMemory:
fallthrough
default:
if len(cfg.File) > 0 {
+4 -4
View File
@@ -32,13 +32,13 @@ func TestInitialize(t *testing.T) {
ExpectedErr: nil,
},
{
Name: "inmemory-no-file",
Cfg: &Config{Type: TypeInMemory},
Name: "memory-no-file",
Cfg: &Config{Type: TypeMemory},
ExpectedErr: nil,
},
{
Name: "inmemory-with-file",
Cfg: &Config{Type: TypeInMemory, File: t.TempDir() + "/TestInitialize_inmemory-with-file.db"},
Name: "memory-with-file",
Cfg: &Config{Type: TypeMemory, File: t.TempDir() + "/TestInitialize_memory-with-file.db"},
ExpectedErr: nil,
},
{
+8
View File
@@ -0,0 +1,8 @@
package common
import "errors"
var (
ErrServiceNotFound = errors.New("service not found") // When a service does not exist in the store
ErrInvalidTimeRange = errors.New("'from' cannot be older than 'to'") // When an invalid time range is provided
)
+9
View File
@@ -0,0 +1,9 @@
package common
const (
// MaximumNumberOfResults is the maximum number of results that a service can have
MaximumNumberOfResults = 100
// MaximumNumberOfEvents is the maximum number of events that a service can have
MaximumNumberOfEvents = 50
)
@@ -2,11 +2,10 @@ package paging
// ServiceStatusParams represents all parameters that can be used for paging purposes
type ServiceStatusParams struct {
EventsPage int // Number of the event page
EventsPageSize int // Size of the event page
ResultsPage int // Number of the result page
ResultsPageSize int // Size of the result page
IncludeUptime bool // Whether to include uptime data
EventsPage int // Number of the event page
EventsPageSize int // Size of the event page
ResultsPage int // Number of the result page
ResultsPageSize int // Size of the result page
}
// NewServiceStatusParams creates a new ServiceStatusParams
@@ -27,9 +26,3 @@ func (params *ServiceStatusParams) WithResults(page, pageSize int) *ServiceStatu
params.ResultsPageSize = pageSize
return params
}
// WithUptime sets the value IncludeUptime to true
func (params *ServiceStatusParams) WithUptime() *ServiceStatusParams {
params.IncludeUptime = true
return params
}
@@ -10,7 +10,6 @@ func TestNewServiceStatusParams(t *testing.T) {
ExpectedEventsPageSize int
ExpectedResultsPage int
ExpectedResultsPageSize int
ExpectedIncludeUptime bool
}
scenarios := []Scenario{
{
@@ -20,7 +19,6 @@ func TestNewServiceStatusParams(t *testing.T) {
ExpectedEventsPageSize: 0,
ExpectedResultsPage: 0,
ExpectedResultsPageSize: 0,
ExpectedIncludeUptime: false,
},
{
Name: "with-events-page-2-size-7",
@@ -29,25 +27,22 @@ func TestNewServiceStatusParams(t *testing.T) {
ExpectedEventsPageSize: 7,
ExpectedResultsPage: 0,
ExpectedResultsPageSize: 0,
ExpectedIncludeUptime: false,
},
{
Name: "with-events-page-4-size-3-uptime",
Params: NewServiceStatusParams().WithEvents(4, 3).WithUptime(),
Params: NewServiceStatusParams().WithEvents(4, 3),
ExpectedEventsPage: 4,
ExpectedEventsPageSize: 3,
ExpectedResultsPage: 0,
ExpectedResultsPageSize: 0,
ExpectedIncludeUptime: true,
},
{
Name: "with-results-page-1-size-20-uptime",
Params: NewServiceStatusParams().WithResults(1, 20).WithUptime(),
Params: NewServiceStatusParams().WithResults(1, 20),
ExpectedEventsPage: 0,
ExpectedEventsPageSize: 0,
ExpectedResultsPage: 1,
ExpectedResultsPageSize: 20,
ExpectedIncludeUptime: true,
},
{
Name: "with-results-page-2-size-10-events-page-3-size-50",
@@ -56,7 +51,6 @@ func TestNewServiceStatusParams(t *testing.T) {
ExpectedEventsPageSize: 50,
ExpectedResultsPage: 2,
ExpectedResultsPageSize: 10,
ExpectedIncludeUptime: false,
},
}
for _, scenario := range scenarios {
@@ -73,9 +67,6 @@ func TestNewServiceStatusParams(t *testing.T) {
if scenario.Params.ResultsPageSize != scenario.ExpectedResultsPageSize {
t.Errorf("expected ResultsPageSize to be %d, was %d", scenario.ExpectedResultsPageSize, scenario.Params.ResultsPageSize)
}
if scenario.Params.IncludeUptime != scenario.ExpectedIncludeUptime {
t.Errorf("expected IncludeUptime to be %v, was %v", scenario.ExpectedIncludeUptime, scenario.Params.IncludeUptime)
}
})
}
}
+32 -1
View File
@@ -6,13 +6,15 @@ import (
"time"
"github.com/TwinProduction/gatus/core"
"github.com/TwinProduction/gatus/storage/store/paging"
"github.com/TwinProduction/gatus/storage/store/common"
"github.com/TwinProduction/gatus/storage/store/common/paging"
"github.com/TwinProduction/gatus/util"
"github.com/TwinProduction/gocache"
)
func init() {
gob.Register(&core.ServiceStatus{})
gob.Register(&core.HourlyUptimeStatistics{})
gob.Register(&core.Uptime{})
gob.Register(&core.Result{})
gob.Register(&core.Event{})
@@ -68,6 +70,35 @@ func (s *Store) GetServiceStatusByKey(key string, params *paging.ServiceStatusPa
return ShallowCopyServiceStatus(serviceStatus.(*core.ServiceStatus), params)
}
// GetUptimeByKey returns the uptime percentage during a time range
func (s *Store) GetUptimeByKey(key string, from, to time.Time) (float64, error) {
if from.After(to) {
return 0, common.ErrInvalidTimeRange
}
serviceStatus := s.cache.GetValue(key)
if serviceStatus == nil || serviceStatus.(*core.ServiceStatus).Uptime == nil {
return 0, common.ErrServiceNotFound
}
successfulExecutions := uint64(0)
totalExecutions := uint64(0)
current := from
for to.Sub(current) >= 0 {
hourlyUnixTimestamp := current.Truncate(time.Hour).Unix()
hourlyStats := serviceStatus.(*core.ServiceStatus).Uptime.HourlyStatistics[hourlyUnixTimestamp]
if hourlyStats == nil || hourlyStats.TotalExecutions == 0 {
current = current.Add(time.Hour)
continue
}
successfulExecutions += hourlyStats.SuccessfulExecutions
totalExecutions += hourlyStats.TotalExecutions
current = current.Add(time.Hour)
}
if totalExecutions == 0 {
return 0, nil
}
return float64(successfulExecutions) / float64(totalExecutions), nil
}
// Insert adds the observed result for the specified service into the store
func (s *Store) Insert(service *core.Service, result *core.Result) {
key := service.Key()
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"time"
"github.com/TwinProduction/gatus/core"
"github.com/TwinProduction/gatus/storage/store/paging"
"github.com/TwinProduction/gatus/storage/store/common/paging"
)
var (
-50
View File
@@ -45,56 +45,6 @@ func processUptimeAfterResult(uptime *core.Uptime, result *core.Result) {
}
}
}
if result.Success {
// Recalculate uptime if at least one of the 1h, 24h or 7d uptime are not 100%
// If they're all 100%, then recalculating the uptime would be useless unless
// the result added was a failure (!result.Success)
if uptime.LastSevenDays != 1 || uptime.LastTwentyFourHours != 1 || uptime.LastHour != 1 {
recalculateUptime(uptime)
}
} else {
// Recalculate uptime if at least one of the 1h, 24h or 7d uptime are not 0%
// If they're all 0%, then recalculating the uptime would be useless unless
// the result added was a success (result.Success)
if uptime.LastSevenDays != 0 || uptime.LastTwentyFourHours != 0 || uptime.LastHour != 0 {
recalculateUptime(uptime)
}
}
}
func recalculateUptime(uptime *core.Uptime) {
uptimeBrackets := make(map[string]uint64)
now := time.Now()
// The oldest uptime bracket starts 7 days ago, so we'll start from there
timestamp := now.Add(-sevenDays)
for now.Sub(timestamp) >= 0 {
hourlyUnixTimestamp := timestamp.Truncate(time.Hour).Unix()
hourlyStats := uptime.HourlyStatistics[hourlyUnixTimestamp]
if hourlyStats == nil || hourlyStats.TotalExecutions == 0 {
timestamp = timestamp.Add(time.Hour)
continue
}
uptimeBrackets["7d_success"] += hourlyStats.SuccessfulExecutions
uptimeBrackets["7d_total"] += hourlyStats.TotalExecutions
if now.Sub(timestamp) <= 24*time.Hour {
uptimeBrackets["24h_success"] += hourlyStats.SuccessfulExecutions
uptimeBrackets["24h_total"] += hourlyStats.TotalExecutions
}
if now.Sub(timestamp) <= time.Hour {
uptimeBrackets["1h_success"] += hourlyStats.SuccessfulExecutions
uptimeBrackets["1h_total"] += hourlyStats.TotalExecutions
}
timestamp = timestamp.Add(time.Hour)
}
if uptimeBrackets["7d_total"] > 0 {
uptime.LastSevenDays = float64(uptimeBrackets["7d_success"]) / float64(uptimeBrackets["7d_total"])
}
if uptimeBrackets["24h_total"] > 0 {
uptime.LastTwentyFourHours = float64(uptimeBrackets["24h_success"]) / float64(uptimeBrackets["24h_total"])
}
if uptimeBrackets["1h_total"] > 0 {
uptime.LastHour = float64(uptimeBrackets["1h_success"]) / float64(uptimeBrackets["1h_total"])
}
}
// XXX: Remove this on v3.0.0
-26
View File
@@ -12,22 +12,16 @@ func TestProcessUptimeAfterResult(t *testing.T) {
serviceStatus := core.NewServiceStatus(service.Key(), service.Group, service.Name)
uptime := serviceStatus.Uptime
checkUptimes(t, serviceStatus, 0.00, 0.00, 0.00)
now := time.Now()
now = time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, now.Location())
processUptimeAfterResult(uptime, &core.Result{Timestamp: now.Add(-7 * 24 * time.Hour), Success: true})
checkUptimes(t, serviceStatus, 1.00, 0.00, 0.00)
processUptimeAfterResult(uptime, &core.Result{Timestamp: now.Add(-6 * 24 * time.Hour), Success: false})
checkUptimes(t, serviceStatus, 0.50, 0.00, 0.00)
processUptimeAfterResult(uptime, &core.Result{Timestamp: now.Add(-8 * 24 * time.Hour), Success: true})
checkUptimes(t, serviceStatus, 0.50, 0.00, 0.00)
processUptimeAfterResult(uptime, &core.Result{Timestamp: now.Add(-24 * time.Hour), Success: true})
processUptimeAfterResult(uptime, &core.Result{Timestamp: now.Add(-12 * time.Hour), Success: true})
checkUptimes(t, serviceStatus, 0.75, 1.00, 0.00)
processUptimeAfterResult(uptime, &core.Result{Timestamp: now.Add(-1 * time.Hour), Success: true, Duration: 10 * time.Millisecond})
checkHourlyStatistics(t, uptime.HourlyStatistics[now.Unix()-now.Unix()%3600-3600], 10, 1, 1)
@@ -37,7 +31,6 @@ func TestProcessUptimeAfterResult(t *testing.T) {
checkHourlyStatistics(t, uptime.HourlyStatistics[now.Unix()-now.Unix()%3600-3600], 535, 3, 1)
processUptimeAfterResult(uptime, &core.Result{Timestamp: now.Add(-10 * time.Minute), Success: false})
checkUptimes(t, serviceStatus, 0.50, 0.50, 0.25)
processUptimeAfterResult(uptime, &core.Result{Timestamp: now.Add(-120 * time.Hour), Success: true})
processUptimeAfterResult(uptime, &core.Result{Timestamp: now.Add(-119 * time.Hour), Success: true})
@@ -47,7 +40,6 @@ func TestProcessUptimeAfterResult(t *testing.T) {
processUptimeAfterResult(uptime, &core.Result{Timestamp: now.Add(-8 * time.Hour), Success: true})
processUptimeAfterResult(uptime, &core.Result{Timestamp: now.Add(-30 * time.Minute), Success: true})
processUptimeAfterResult(uptime, &core.Result{Timestamp: now.Add(-25 * time.Minute), Success: true})
checkUptimes(t, serviceStatus, 0.75, 0.70, 0.50)
}
func TestAddResultUptimeIsCleaningUpAfterItself(t *testing.T) {
@@ -62,29 +54,11 @@ func TestAddResultUptimeIsCleaningUpAfterItself(t *testing.T) {
if len(serviceStatus.Uptime.HourlyStatistics) > numberOfHoursInTenDays {
t.Errorf("At no point in time should there be more than %d entries in serviceStatus.SuccessfulExecutionsPerHour, but there are %d", numberOfHoursInTenDays, len(serviceStatus.Uptime.HourlyStatistics))
}
if now.Sub(timestamp) > time.Hour && serviceStatus.Uptime.LastHour != 0 {
t.Error("most recent timestamp > 1h ago, expected serviceStatus.Uptime.LastHour to be 0, got", serviceStatus.Uptime.LastHour)
}
if now.Sub(timestamp) < time.Hour && serviceStatus.Uptime.LastHour == 0 {
t.Error("most recent timestamp < 1h ago, expected serviceStatus.Uptime.LastHour to NOT be 0, got", serviceStatus.Uptime.LastHour)
}
// Simulate service with an interval of 3 minutes
timestamp = timestamp.Add(3 * time.Minute)
}
}
func checkUptimes(t *testing.T, status *core.ServiceStatus, expectedUptimeDuringLastSevenDays, expectedUptimeDuringLastTwentyFourHours, expectedUptimeDuringLastHour float64) {
if status.Uptime.LastSevenDays != expectedUptimeDuringLastSevenDays {
t.Errorf("expected status.Uptime.LastSevenDays to be %f, got %f", expectedUptimeDuringLastHour, status.Uptime.LastSevenDays)
}
if status.Uptime.LastTwentyFourHours != expectedUptimeDuringLastTwentyFourHours {
t.Errorf("expected status.Uptime.LastTwentyFourHours to be %f, got %f", expectedUptimeDuringLastTwentyFourHours, status.Uptime.LastTwentyFourHours)
}
if status.Uptime.LastHour != expectedUptimeDuringLastHour {
t.Errorf("expected status.Uptime.LastHour to be %f, got %f", expectedUptimeDuringLastHour, status.Uptime.LastHour)
}
}
func checkHourlyStatistics(t *testing.T, hourlyUptimeStatistics *core.HourlyUptimeStatistics, expectedTotalExecutionsResponseTime uint64, expectedTotalExecutions uint64, expectedSuccessfulExecutions uint64) {
if hourlyUptimeStatistics.TotalExecutionsResponseTime != expectedTotalExecutionsResponseTime {
t.Error("TotalExecutionsResponseTime should've been", expectedTotalExecutionsResponseTime, "got", hourlyUptimeStatistics.TotalExecutionsResponseTime)
+6 -10
View File
@@ -2,7 +2,8 @@ package memory
import (
"github.com/TwinProduction/gatus/core"
"github.com/TwinProduction/gatus/storage/store/paging"
"github.com/TwinProduction/gatus/storage/store/common"
"github.com/TwinProduction/gatus/storage/store/common/paging"
)
// ShallowCopyServiceStatus returns a shallow copy of a ServiceStatus with only the results
@@ -28,11 +29,6 @@ func ShallowCopyServiceStatus(ss *core.ServiceStatus, params *paging.ServiceStat
} else {
shallowCopy.Events = ss.Events[eventsStart:eventsEnd]
}
if params.IncludeUptime {
shallowCopy.Uptime.LastHour = ss.Uptime.LastHour
shallowCopy.Uptime.LastTwentyFourHours = ss.Uptime.LastTwentyFourHours
shallowCopy.Uptime.LastSevenDays = ss.Uptime.LastSevenDays
}
return shallowCopy
}
@@ -63,11 +59,11 @@ func AddResult(ss *core.ServiceStatus, result *core.Result) {
// Check if there's any change since the last result
if ss.Results[len(ss.Results)-1].Success != result.Success {
ss.Events = append(ss.Events, core.NewEventFromResult(result))
if len(ss.Events) > core.MaximumNumberOfEvents {
if len(ss.Events) > common.MaximumNumberOfEvents {
// Doing ss.Events[1:] would usually be sufficient, but in the case where for some reason, the slice has
// more than one extra element, we can get rid of all of them at once and thus returning the slice to a
// length of MaximumNumberOfEvents by using ss.Events[len(ss.Events)-MaximumNumberOfEvents:] instead
ss.Events = ss.Events[len(ss.Events)-core.MaximumNumberOfEvents:]
ss.Events = ss.Events[len(ss.Events)-common.MaximumNumberOfEvents:]
}
}
} else {
@@ -75,11 +71,11 @@ func AddResult(ss *core.ServiceStatus, result *core.Result) {
ss.Events = append(ss.Events, core.NewEventFromResult(result))
}
ss.Results = append(ss.Results, result)
if len(ss.Results) > core.MaximumNumberOfResults {
if len(ss.Results) > common.MaximumNumberOfResults {
// Doing ss.Results[1:] would usually be sufficient, but in the case where for some reason, the slice has more
// than one extra element, we can get rid of all of them at once and thus returning the slice to a length of
// MaximumNumberOfResults by using ss.Results[len(ss.Results)-MaximumNumberOfResults:] instead
ss.Results = ss.Results[len(ss.Results)-core.MaximumNumberOfResults:]
ss.Results = ss.Results[len(ss.Results)-common.MaximumNumberOfResults:]
}
processUptimeAfterResult(ss.Uptime, result)
}
+3 -2
View File
@@ -4,13 +4,14 @@ import (
"testing"
"github.com/TwinProduction/gatus/core"
"github.com/TwinProduction/gatus/storage/store/paging"
"github.com/TwinProduction/gatus/storage/store/common"
"github.com/TwinProduction/gatus/storage/store/common/paging"
)
func BenchmarkShallowCopyServiceStatus(b *testing.B) {
service := &testService
serviceStatus := core.NewServiceStatus(service.Key(), service.Group, service.Name)
for i := 0; i < core.MaximumNumberOfResults; i++ {
for i := 0; i < common.MaximumNumberOfResults; i++ {
AddResult(serviceStatus, &testSuccessfulResult)
}
for n := 0; n < b.N; n++ {
+7 -20
View File
@@ -5,20 +5,21 @@ import (
"time"
"github.com/TwinProduction/gatus/core"
"github.com/TwinProduction/gatus/storage/store/paging"
"github.com/TwinProduction/gatus/storage/store/common"
"github.com/TwinProduction/gatus/storage/store/common/paging"
)
func TestAddResult(t *testing.T) {
service := &core.Service{Name: "name", Group: "group"}
serviceStatus := core.NewServiceStatus(service.Key(), service.Group, service.Name)
for i := 0; i < (core.MaximumNumberOfResults+core.MaximumNumberOfEvents)*2; i++ {
for i := 0; i < (common.MaximumNumberOfResults+common.MaximumNumberOfEvents)*2; i++ {
AddResult(serviceStatus, &core.Result{Success: i%2 == 0, Timestamp: time.Now()})
}
if len(serviceStatus.Results) != core.MaximumNumberOfResults {
t.Errorf("expected serviceStatus.Results to not exceed a length of %d", core.MaximumNumberOfResults)
if len(serviceStatus.Results) != common.MaximumNumberOfResults {
t.Errorf("expected serviceStatus.Results to not exceed a length of %d", common.MaximumNumberOfResults)
}
if len(serviceStatus.Events) != core.MaximumNumberOfEvents {
t.Errorf("expected serviceStatus.Events to not exceed a length of %d", core.MaximumNumberOfEvents)
if len(serviceStatus.Events) != common.MaximumNumberOfEvents {
t.Errorf("expected serviceStatus.Events to not exceed a length of %d", common.MaximumNumberOfEvents)
}
// Try to add nil serviceStatus
AddResult(nil, &core.Result{Timestamp: time.Now()})
@@ -62,18 +63,4 @@ func TestShallowCopyServiceStatus(t *testing.T) {
if len(ShallowCopyServiceStatus(serviceStatus, paging.NewServiceStatusParams().WithResults(1, 50)).Results) != 25 {
t.Error("expected to have 25 results, because there's only 25 results")
}
uptime := ShallowCopyServiceStatus(serviceStatus, paging.NewServiceStatusParams().WithUptime()).Uptime
if uptime == nil {
t.Error("expected uptime to not be nil")
} else {
if uptime.LastHour != 1 {
t.Error("expected uptime.LastHour to not be 1, got", uptime.LastHour)
}
if uptime.LastTwentyFourHours != 0.5 {
t.Error("expected uptime.LastTwentyFourHours to not be 0.5, got", uptime.LastTwentyFourHours)
}
if uptime.LastSevenDays != 0.52 {
t.Error("expected uptime.LastSevenDays to not be 0.52, got", uptime.LastSevenDays)
}
}
}
+45 -17
View File
@@ -9,7 +9,8 @@ import (
"time"
"github.com/TwinProduction/gatus/core"
"github.com/TwinProduction/gatus/storage/store/paging"
"github.com/TwinProduction/gatus/storage/store/common"
"github.com/TwinProduction/gatus/storage/store/common/paging"
"github.com/TwinProduction/gatus/util"
_ "modernc.org/sqlite"
)
@@ -19,11 +20,14 @@ import (
//////////////////////////////////////////////////////////////////////////////////////////////////
const (
// arraySeparator is the separator used to separate multiple strings in a single column.
// It's a dirty hack, but it's only used for persisting errors, and since this data will likely only ever be used
// for aesthetic purposes, I deemed it wasn't worth the performance impact of yet another one-to-many table.
arraySeparator = "|~|"
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
uptimeCleanUpThreshold = 10 * 24 * time.Hour // Maximum uptime age before triggering a clean up
eventsCleanUpThreshold = common.MaximumNumberOfEvents + 10 // Maximum number of events before triggering a clean up
resultsCleanUpThreshold = common.MaximumNumberOfResults + 10 // Maximum number of results before triggering a clean up
uptimeRetention = 7 * 24 * time.Hour
)
@@ -35,8 +39,7 @@ var (
// 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")
errNoRowsReturned = errors.New("expected a row to be returned, but none was")
)
// Store that leverages a database
@@ -191,6 +194,31 @@ func (s *Store) GetServiceStatusByKey(key string, params *paging.ServiceStatusPa
return serviceStatus
}
// GetUptimeByKey returns the uptime percentage during a time range
func (s *Store) GetUptimeByKey(key string, from, to time.Time) (float64, error) {
if from.After(to) {
return 0, common.ErrInvalidTimeRange
}
tx, err := s.db.Begin()
if err != nil {
return 0, err
}
serviceID, _, _, err := s.getServiceIDGroupAndNameByKey(tx, key)
if err != nil {
_ = tx.Rollback()
return 0, err
}
uptime, _, err := s.getServiceUptime(tx, serviceID, from, to)
if err != nil {
_ = tx.Rollback()
return 0, err
}
if err = tx.Commit(); err != nil {
_ = tx.Rollback()
}
return uptime, nil
}
// 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()
@@ -200,7 +228,7 @@ func (s *Store) Insert(service *core.Service, result *core.Result) {
//start := time.Now()
serviceID, err := s.getServiceID(tx, service)
if err != nil {
if err == errServiceNotFoundInDatabase {
if err == common.ErrServiceNotFound {
// Service doesn't exist in the database, insert it
if serviceID, err = s.insertService(tx, service); err != nil {
_ = tx.Rollback()
@@ -473,12 +501,12 @@ func (s *Store) getServiceStatusByKey(tx *sql.Tx, key string, parameters *paging
log.Printf("[sqlite][getServiceStatusByKey] Failed to retrieve results for key=%s: %s", key, err.Error())
}
}
if parameters.IncludeUptime {
now := time.Now()
serviceStatus.Uptime.LastHour, _, err = s.getServiceUptime(tx, serviceID, now.Add(-time.Hour), now)
serviceStatus.Uptime.LastTwentyFourHours, _, err = s.getServiceUptime(tx, serviceID, now.Add(-24*time.Hour), now)
serviceStatus.Uptime.LastSevenDays, _, err = s.getServiceUptime(tx, serviceID, now.Add(-7*24*time.Hour), now)
}
//if parameters.IncludeUptime {
// now := time.Now()
// serviceStatus.Uptime.LastHour, _, err = s.getServiceUptime(tx, serviceID, now.Add(-time.Hour), now)
// serviceStatus.Uptime.LastTwentyFourHours, _, err = s.getServiceUptime(tx, serviceID, now.Add(-24*time.Hour), now)
// serviceStatus.Uptime.LastSevenDays, _, err = s.getServiceUptime(tx, serviceID, now.Add(-7*24*time.Hour), now)
//}
return serviceStatus, nil
}
@@ -500,7 +528,7 @@ func (s *Store) getServiceIDGroupAndNameByKey(tx *sql.Tx, key string) (id int64,
}
_ = rows.Close()
if id == 0 {
return 0, "", "", errServiceNotFoundInDatabase
return 0, "", "", common.ErrServiceNotFound
}
return
}
@@ -639,7 +667,7 @@ func (s *Store) getServiceID(tx *sql.Tx, service *core.Service) (int64, error) {
}
_ = rows.Close()
if !found {
return 0, errServiceNotFoundInDatabase
return 0, common.ErrServiceNotFound
}
return id, nil
}
@@ -732,7 +760,7 @@ func (s *Store) deleteOldServiceEvents(tx *sql.Tx, serviceID int64) error {
)
`,
serviceID,
core.MaximumNumberOfEvents,
common.MaximumNumberOfEvents,
)
if err != nil {
return err
@@ -757,7 +785,7 @@ func (s *Store) deleteOldServiceResults(tx *sql.Tx, serviceID int64) error {
)
`,
serviceID,
core.MaximumNumberOfResults,
common.MaximumNumberOfResults,
)
if err != nil {
return err
+16 -6
View File
@@ -5,7 +5,8 @@ import (
"time"
"github.com/TwinProduction/gatus/core"
"github.com/TwinProduction/gatus/storage/store/paging"
"github.com/TwinProduction/gatus/storage/store/common"
"github.com/TwinProduction/gatus/storage/store/common/paging"
)
var (
@@ -157,7 +158,7 @@ func TestStore_InsertCleansUpEventsAndResultsProperly(t *testing.T) {
for i := 0; i < resultsCleanUpThreshold+eventsCleanUpThreshold; i++ {
store.Insert(&testService, &testSuccessfulResult)
store.Insert(&testService, &testUnsuccessfulResult)
ss := store.GetServiceStatusByKey(testService.Key(), paging.NewServiceStatusParams().WithResults(1, core.MaximumNumberOfResults*5).WithEvents(1, core.MaximumNumberOfEvents*5))
ss := store.GetServiceStatusByKey(testService.Key(), paging.NewServiceStatusParams().WithResults(1, common.MaximumNumberOfResults*5).WithEvents(1, common.MaximumNumberOfEvents*5))
if len(ss.Results) > resultsCleanUpThreshold+1 {
t.Errorf("number of results shouldn't have exceeded %d, reached %d", resultsCleanUpThreshold, len(ss.Results))
}
@@ -173,16 +174,25 @@ func TestStore_Persistence(t *testing.T) {
store, _ := NewStore("sqlite", file)
store.Insert(&testService, &testSuccessfulResult)
store.Insert(&testService, &testUnsuccessfulResult)
ssFromOldStore := store.GetServiceStatus(testService.Group, testService.Name, paging.NewServiceStatusParams().WithResults(1, core.MaximumNumberOfResults).WithEvents(1, core.MaximumNumberOfEvents).WithUptime())
if ssFromOldStore == nil || ssFromOldStore.Group != "group" || ssFromOldStore.Name != "name" || len(ssFromOldStore.Events) != 3 || len(ssFromOldStore.Results) != 2 || ssFromOldStore.Uptime.LastHour != 0.5 || ssFromOldStore.Uptime.LastTwentyFourHours != 0.5 || ssFromOldStore.Uptime.LastSevenDays != 0.5 {
if uptime, _ := store.GetUptimeByKey(testService.Key(), time.Now().Add(-time.Hour), time.Now()); uptime != 0.5 {
t.Errorf("the uptime over the past 1h should've been 0.5, got %f", uptime)
}
if uptime, _ := store.GetUptimeByKey(testService.Key(), time.Now().Add(-time.Hour*24), time.Now()); uptime != 0.5 {
t.Errorf("the uptime over the past 24h should've been 0.5, got %f", uptime)
}
if uptime, _ := store.GetUptimeByKey(testService.Key(), time.Now().Add(-time.Hour*24*7), time.Now()); uptime != 0.5 {
t.Errorf("the uptime over the past 7d should've been 0.5, got %f", uptime)
}
ssFromOldStore := store.GetServiceStatus(testService.Group, testService.Name, paging.NewServiceStatusParams().WithResults(1, common.MaximumNumberOfResults).WithEvents(1, common.MaximumNumberOfEvents))
if ssFromOldStore == nil || ssFromOldStore.Group != "group" || ssFromOldStore.Name != "name" || len(ssFromOldStore.Events) != 3 || len(ssFromOldStore.Results) != 2 {
store.Close()
t.Fatal("sanity check failed")
}
store.Close()
store, _ = NewStore("sqlite", file)
defer store.Close()
ssFromNewStore := store.GetServiceStatus(testService.Group, testService.Name, paging.NewServiceStatusParams().WithResults(1, core.MaximumNumberOfResults).WithEvents(1, core.MaximumNumberOfEvents).WithUptime())
if ssFromNewStore == nil || ssFromNewStore.Group != "group" || ssFromNewStore.Name != "name" || len(ssFromNewStore.Events) != 3 || len(ssFromNewStore.Results) != 2 || ssFromNewStore.Uptime.LastHour != 0.5 || ssFromNewStore.Uptime.LastTwentyFourHours != 0.5 || ssFromNewStore.Uptime.LastSevenDays != 0.5 {
ssFromNewStore := store.GetServiceStatus(testService.Group, testService.Name, paging.NewServiceStatusParams().WithResults(1, common.MaximumNumberOfResults).WithEvents(1, common.MaximumNumberOfEvents))
if ssFromNewStore == nil || ssFromNewStore.Group != "group" || ssFromNewStore.Name != "name" || len(ssFromNewStore.Events) != 3 || len(ssFromNewStore.Results) != 2 {
t.Fatal("failed sanity check")
}
if ssFromNewStore == ssFromOldStore {
+5 -1
View File
@@ -2,9 +2,10 @@ package store
import (
"github.com/TwinProduction/gatus/core"
"github.com/TwinProduction/gatus/storage/store/common/paging"
"github.com/TwinProduction/gatus/storage/store/memory"
"github.com/TwinProduction/gatus/storage/store/paging"
"github.com/TwinProduction/gatus/storage/store/sqlite"
"time"
)
// Store is the interface that each stores should implement
@@ -19,6 +20,9 @@ type Store interface {
// GetServiceStatusByKey returns the service status for a given key
GetServiceStatusByKey(key string, params *paging.ServiceStatusParams) *core.ServiceStatus
// GetUptimeByKey returns the uptime percentage during a time range
GetUptimeByKey(key string, from, to time.Time) (float64, error)
// Insert adds the observed result for the specified service into the store
Insert(service *core.Service, result *core.Result)
+1 -1
View File
@@ -5,8 +5,8 @@ import (
"time"
"github.com/TwinProduction/gatus/core"
"github.com/TwinProduction/gatus/storage/store/common/paging"
"github.com/TwinProduction/gatus/storage/store/memory"
"github.com/TwinProduction/gatus/storage/store/paging"
"github.com/TwinProduction/gatus/storage/store/sqlite"
)
+37 -18
View File
@@ -5,8 +5,9 @@ import (
"time"
"github.com/TwinProduction/gatus/core"
"github.com/TwinProduction/gatus/storage/store/common"
"github.com/TwinProduction/gatus/storage/store/common/paging"
"github.com/TwinProduction/gatus/storage/store/memory"
"github.com/TwinProduction/gatus/storage/store/paging"
"github.com/TwinProduction/gatus/storage/store/sqlite"
)
@@ -126,7 +127,7 @@ func TestStore_GetServiceStatusByKey(t *testing.T) {
scenario.Store.Insert(&testService, &firstResult)
scenario.Store.Insert(&testService, &secondResult)
serviceStatus := scenario.Store.GetServiceStatusByKey(testService.Key(), paging.NewServiceStatusParams().WithEvents(1, core.MaximumNumberOfEvents).WithResults(1, core.MaximumNumberOfResults).WithUptime())
serviceStatus := scenario.Store.GetServiceStatusByKey(testService.Key(), paging.NewServiceStatusParams().WithEvents(1, common.MaximumNumberOfEvents).WithResults(1, common.MaximumNumberOfResults))
if serviceStatus == nil {
t.Fatalf("serviceStatus shouldn't have been nil")
}
@@ -142,18 +143,6 @@ func TestStore_GetServiceStatusByKey(t *testing.T) {
if serviceStatus.Results[0].Timestamp.After(serviceStatus.Results[1].Timestamp) {
t.Error("The result at index 0 should've been older than the result at index 1")
}
if serviceStatus.Uptime == nil {
t.Fatalf("serviceStatus.Uptime shouldn't have been nil")
}
if serviceStatus.Uptime.LastHour != 0.5 {
t.Errorf("serviceStatus.Uptime.LastHour should've been 0.5, got %f", serviceStatus.Uptime.LastHour)
}
if serviceStatus.Uptime.LastTwentyFourHours != 0.5 {
t.Errorf("serviceStatus.Uptime.LastTwentyFourHours should've been 0.5, got %f", serviceStatus.Uptime.LastTwentyFourHours)
}
if serviceStatus.Uptime.LastSevenDays != 0.5 {
t.Errorf("serviceStatus.Uptime.LastSevenDays should've been 0.5, got %f", serviceStatus.Uptime.LastSevenDays)
}
scenario.Store.Clear()
})
}
@@ -165,15 +154,15 @@ func TestStore_GetServiceStatusForMissingStatusReturnsNil(t *testing.T) {
for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) {
scenario.Store.Insert(&testService, &testSuccessfulResult)
serviceStatus := scenario.Store.GetServiceStatus("nonexistantgroup", "nonexistantname", paging.NewServiceStatusParams().WithEvents(1, core.MaximumNumberOfEvents).WithResults(1, core.MaximumNumberOfResults).WithUptime())
serviceStatus := scenario.Store.GetServiceStatus("nonexistantgroup", "nonexistantname", paging.NewServiceStatusParams().WithEvents(1, common.MaximumNumberOfEvents).WithResults(1, common.MaximumNumberOfResults))
if serviceStatus != nil {
t.Errorf("Returned service status for group '%s' and name '%s' not nil after inserting the service into the store", testService.Group, testService.Name)
}
serviceStatus = scenario.Store.GetServiceStatus(testService.Group, "nonexistantname", paging.NewServiceStatusParams().WithEvents(1, core.MaximumNumberOfEvents).WithResults(1, core.MaximumNumberOfResults).WithUptime())
serviceStatus = scenario.Store.GetServiceStatus(testService.Group, "nonexistantname", paging.NewServiceStatusParams().WithEvents(1, common.MaximumNumberOfEvents).WithResults(1, common.MaximumNumberOfResults))
if serviceStatus != nil {
t.Errorf("Returned service status for group '%s' and name '%s' not nil after inserting the service into the store", testService.Group, "nonexistantname")
}
serviceStatus = scenario.Store.GetServiceStatus("nonexistantgroup", testService.Name, paging.NewServiceStatusParams().WithEvents(1, core.MaximumNumberOfEvents).WithResults(1, core.MaximumNumberOfResults).WithUptime())
serviceStatus = scenario.Store.GetServiceStatus("nonexistantgroup", testService.Name, paging.NewServiceStatusParams().WithEvents(1, common.MaximumNumberOfEvents).WithResults(1, common.MaximumNumberOfResults))
if serviceStatus != nil {
t.Errorf("Returned service status for group '%s' and name '%s' not nil after inserting the service into the store", "nonexistantgroup", testService.Name)
}
@@ -273,6 +262,36 @@ func TestStore_GetServiceStatusPage1IsHasMoreRecentResultsThanPage2(t *testing.T
}
}
func TestStore_GetUptimeByKey(t *testing.T) {
scenarios := initStoresAndBaseScenarios(t, "TestStore_GetUptimeByKey")
defer cleanUp(scenarios)
firstResult := testSuccessfulResult
firstResult.Timestamp = now.Add(-time.Minute)
secondResult := testUnsuccessfulResult
secondResult.Timestamp = now
for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) {
if _, err := scenario.Store.GetUptimeByKey(testService.Key(), time.Now().Add(-time.Hour), time.Now()); err != common.ErrServiceNotFound {
t.Errorf("should've returned not found because there's nothing yet, got %v", err)
}
scenario.Store.Insert(&testService, &firstResult)
scenario.Store.Insert(&testService, &secondResult)
if uptime, _ := scenario.Store.GetUptimeByKey(testService.Key(), time.Now().Add(-time.Hour), time.Now()); uptime != 0.5 {
t.Errorf("the uptime over the past 1h should've been 0.5, got %f", uptime)
}
if uptime, _ := scenario.Store.GetUptimeByKey(testService.Key(), time.Now().Add(-time.Hour*24), time.Now()); uptime != 0.5 {
t.Errorf("the uptime over the past 24h should've been 0.5, got %f", uptime)
}
if uptime, _ := scenario.Store.GetUptimeByKey(testService.Key(), time.Now().Add(-time.Hour*24*7), time.Now()); uptime != 0.5 {
t.Errorf("the uptime over the past 7d should've been 0.5, got %f", uptime)
}
if _, err := scenario.Store.GetUptimeByKey(testService.Key(), time.Now(), time.Now().Add(-time.Hour)); err == nil {
t.Error("should've returned an error because the parameter 'from' cannot be older than 'to'")
}
})
}
}
func TestStore_Insert(t *testing.T) {
scenarios := initStoresAndBaseScenarios(t, "TestStore_Insert")
defer cleanUp(scenarios)
@@ -285,7 +304,7 @@ func TestStore_Insert(t *testing.T) {
scenario.Store.Insert(&testService, &testSuccessfulResult)
scenario.Store.Insert(&testService, &testUnsuccessfulResult)
ss := scenario.Store.GetServiceStatusByKey(testService.Key(), paging.NewServiceStatusParams().WithEvents(1, core.MaximumNumberOfEvents).WithResults(1, core.MaximumNumberOfResults).WithUptime())
ss := scenario.Store.GetServiceStatusByKey(testService.Key(), paging.NewServiceStatusParams().WithEvents(1, common.MaximumNumberOfEvents).WithResults(1, common.MaximumNumberOfResults))
if ss == nil {
t.Fatalf("Store should've had key '%s', but didn't", testService.Key())
}
+2 -2
View File
@@ -4,6 +4,6 @@ package storage
type Type string
const (
TypeInMemory Type = "inmemory" // In-memory store
TypeSQLite Type = "sqlite" // SQLite store
TypeMemory Type = "memory" // In-memory store
TypeSQLite Type = "sqlite" // SQLite store
)
+5 -19
View File
@@ -14,33 +14,20 @@
/>
<Pagination @page="changePage"/>
</slot>
<div v-if="uptime" class="mt-12">
<div class="mt-12">
<h1 class="text-xl xl:text-3xl font-mono text-gray-400">UPTIME</h1>
<hr />
<div class="flex space-x-4 text-center text-xl xl:text-2xl mt-3">
<div class="flex-1">
{{ prettifyUptime(uptime['7d']) }}
<h2 class="text-sm text-gray-400">Last 7 days</h2>
</div>
<div class="flex-1">
{{ prettifyUptime(uptime['24h']) }}
<h2 class="text-sm text-gray-400">Last 24 hours</h2>
</div>
<div class="flex-1">
{{ prettifyUptime(uptime['1h']) }}
<h2 class="text-sm text-gray-400">Last hour</h2>
</div>
</div>
<hr class="mt-1"/>
<h3 class="text-xl font-mono text-gray-400 mt-1 text-right">BADGES</h3>
<div v-if="serviceStatus && serviceStatus.key" class="flex space-x-4 text-center text-2xl mt-6 relative bottom-12">
<div v-if="serviceStatus && serviceStatus.key" class="flex space-x-4 text-center text-2xl mt-6 relative bottom-2 mb-10">
<div class="flex-1">
<h2 class="text-sm text-gray-400 mb-1">Last 7 days</h2>
<img :src="generateBadgeImageURL('7d')" alt="7d uptime badge" class="mx-auto" />
</div>
<div class="flex-1">
<h2 class="text-sm text-gray-400 mb-1">Last 24 hours</h2>
<img :src="generateBadgeImageURL('24h')" alt="24h uptime badge" class="mx-auto" />
</div>
<div class="flex-1">
<h2 class="text-sm text-gray-400 mb-1">Last hour</h2>
<img :src="generateBadgeImageURL('1h')" alt="1h uptime badge" class="mx-auto" />
</div>
</div>
@@ -159,7 +146,6 @@ export default {
return {
serviceStatus: {},
events: [],
uptime: {"7d": 0, "24h": 0, "1h": 0},
// Since this page isn't at the root, we need to modify the server URL a bit
serverUrl: SERVER_URL === '.' ? '..' : SERVER_URL,
currentPage: 1,
+1 -1
View File
@@ -2,4 +2,4 @@
/*! tailwindcss v2.1.2 | MIT License | https://tailwindcss.com */
/*! modern-normalize v1.1.0 | MIT License | https://github.com/sindresorhus/modern-normalize */html{-moz-tab-size:4;-o-tab-size:4;tab-size:4;line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji}hr{height:0;color:inherit}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,select{text-transform:none}[type=button],button{-webkit-appearance:button}legend{padding:0}progress{vertical-align:baseline}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}button{background-color:transparent;background-image:none}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}fieldset,ol,ul{margin:0;padding:0}ol,ul{list-style:none}html{font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5}body{font-family:inherit;line-height:inherit}*,:after,:before{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}hr{border-top-width:1px}img{border-style:solid}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input:-ms-input-placeholder,textarea:-ms-input-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button{cursor:pointer}table{border-collapse:collapse}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}button,input,optgroup,select,textarea{padding:0;line-height:inherit;color:inherit}code,kbd,pre,samp{font-family:Consolas,Monaco,Courier New,monospace}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgba(243,244,246,var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgba(229,231,235,var(--tw-bg-opacity))}.bg-red-100{--tw-bg-opacity:1;background-color:rgba(254,226,226,var(--tw-bg-opacity))}.bg-red-600{--tw-bg-opacity:1;background-color:rgba(220,38,38,var(--tw-bg-opacity))}.bg-green-100{--tw-bg-opacity:1;background-color:rgba(209,250,229,var(--tw-bg-opacity))}.hover\:bg-gray-100:hover{--tw-bg-opacity:1;background-color:rgba(243,244,246,var(--tw-bg-opacity))}.hover\:bg-gray-200:hover{--tw-bg-opacity:1;background-color:rgba(229,231,235,var(--tw-bg-opacity))}.dark .dark\:bg-gray-700{--tw-bg-opacity:1;background-color:rgba(55,65,81,var(--tw-bg-opacity))}.dark .dark\:bg-gray-800{--tw-bg-opacity:1;background-color:rgba(31,41,55,var(--tw-bg-opacity))}.dark .dark\:bg-gray-900{--tw-bg-opacity:1;background-color:rgba(17,24,39,var(--tw-bg-opacity))}.dark .dark\:hover\:bg-gray-600:hover{--tw-bg-opacity:1;background-color:rgba(75,85,99,var(--tw-bg-opacity))}.dark .dark\:hover\:bg-gray-700:hover{--tw-bg-opacity:1;background-color:rgba(55,65,81,var(--tw-bg-opacity))}.border-gray-200{--tw-border-opacity:1;border-color:rgba(229,231,235,var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity:1;border-color:rgba(209,213,219,var(--tw-border-opacity))}.border-gray-400{--tw-border-opacity:1;border-color:rgba(156,163,175,var(--tw-border-opacity))}.border-gray-500{--tw-border-opacity:1;border-color:rgba(107,114,128,var(--tw-border-opacity))}.border-red-500{--tw-border-opacity:1;border-color:rgba(239,68,68,var(--tw-border-opacity))}.border-green-600{--tw-border-opacity:1;border-color:rgba(5,150,105,var(--tw-border-opacity))}.dark .dark\:border-gray-500{--tw-border-opacity:1;border-color:rgba(107,114,128,var(--tw-border-opacity))}.rounded-none{border-radius:0}.rounded{border-radius:.25rem}.rounded-xl{border-radius:.75rem}.rounded-full{border-radius:9999px}.border-dashed{border-style:dashed}.border{border-width:1px}.border-t{border-top-width:1px}.border-r{border-right-width:1px}.border-l{border-left-width:1px}.cursor-pointer{cursor:pointer}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.table{display:table}.hidLine truncated
/*! modern-normalize v1.1.0 | MIT License | https://github.com/sindresorhus/modern-normalize */html{-moz-tab-size:4;-o-tab-size:4;tab-size:4;line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji}hr{height:0;color:inherit}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,select{text-transform:none}[type=button],button{-webkit-appearance:button}legend{padding:0}progress{vertical-align:baseline}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}button{background-color:transparent;background-image:none}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}fieldset,ol,ul{margin:0;padding:0}ol,ul{list-style:none}html{font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5}body{font-family:inherit;line-height:inherit}*,:after,:before{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}hr{border-top-width:1px}img{border-style:solid}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input:-ms-input-placeholder,textarea:-ms-input-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button{cursor:pointer}table{border-collapse:collapse}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}button,input,optgroup,select,textarea{padding:0;line-height:inherit;color:inherit}code,kbd,pre,samp{font-family:Consolas,Monaco,Courier New,monospace}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgba(243,244,246,var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgba(229,231,235,var(--tw-bg-opacity))}.bg-red-100{--tw-bg-opacity:1;background-color:rgba(254,226,226,var(--tw-bg-opacity))}.bg-red-600{--tw-bg-opacity:1;background-color:rgba(220,38,38,var(--tw-bg-opacity))}.bg-green-100{--tw-bg-opacity:1;background-color:rgba(209,250,229,var(--tw-bg-opacity))}.hover\:bg-gray-100:hover{--tw-bg-opacity:1;background-color:rgba(243,244,246,var(--tw-bg-opacity))}.hover\:bg-gray-200:hover{--tw-bg-opacity:1;background-color:rgba(229,231,235,var(--tw-bg-opacity))}.dark .dark\:bg-gray-700{--tw-bg-opacity:1;background-color:rgba(55,65,81,var(--tw-bg-opacity))}.dark .dark\:bg-gray-800{--tw-bg-opacity:1;background-color:rgba(31,41,55,var(--tw-bg-opacity))}.dark .dark\:bg-gray-900{--tw-bg-opacity:1;background-color:rgba(17,24,39,var(--tw-bg-opacity))}.dark .dark\:hover\:bg-gray-600:hover{--tw-bg-opacity:1;background-color:rgba(75,85,99,var(--tw-bg-opacity))}.dark .dark\:hover\:bg-gray-700:hover{--tw-bg-opacity:1;background-color:rgba(55,65,81,var(--tw-bg-opacity))}.border-gray-200{--tw-border-opacity:1;border-color:rgba(229,231,235,var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity:1;border-color:rgba(209,213,219,var(--tw-border-opacity))}.border-gray-400{--tw-border-opacity:1;border-color:rgba(156,163,175,var(--tw-border-opacity))}.border-gray-500{--tw-border-opacity:1;border-color:rgba(107,114,128,var(--tw-border-opacity))}.border-red-500{--tw-border-opacity:1;border-color:rgba(239,68,68,var(--tw-border-opacity))}.border-green-600{--tw-border-opacity:1;border-color:rgba(5,150,105,var(--tw-border-opacity))}.dark .dark\:border-gray-500{--tw-border-opacity:1;border-color:rgba(107,114,128,var(--tw-border-opacity))}.rounded-none{border-radius:0}.rounded{border-radius:.25rem}.rounded-xl{border-radius:.75rem}.rounded-full{border-radius:9999px}.border-dashed{border-style:dashed}.border{border-width:1px}.border-t{border-top-width:1px}.border-r{border-right-width:1px}.border-l{border-left-width:1px}.cursor-pointer{cursor:pointer}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.table{display:table}.hidLine truncated
+1 -1
View File
@@ -1 +1 @@
(function(){"use strict";var e={6014:function(e,t,r){r.d(t,{L:function(){return at}});r(6992),r(8674),r(9601),r(7727);var s=r(9963),n=r(6252),o=r.p+"img/logo.png",a={class:"container container-xs relative mx-auto xl:rounded xl:border xl:shadow-xl xl:my-5 p-5 pb-12 xl:pb-5 text-left dark:bg-gray-800 dark:text-gray-200 dark:border-gray-500",id:"global"},i=(0,n.Wm)("div",{class:"mb-2"},[(0,n.Wm)("div",{class:"flex flex-wrap"},[(0,n.Wm)("div",{class:"w-3/4 text-left my-auto"},[(0,n.Wm)("div",{class:"text-3xl xl:text-5xl lg:text-4xl font-light"},"Health Status")]),(0,n.Wm)("div",{class:"w-1/4 flex justify-end"},[(0,n.Wm)("img",{src:o,alt:"Gatus",class:"object-scale-down",style:{"max-width":"100px","min-width":"50px","min-height":"50px"}})])])],-1);function l(e,t,r,s,o,l){var u=(0,n.up)("router-view"),g=(0,n.up)("Tooltip"),c=(0,n.up)("Social");return(0,n.wg)(),(0,n.j4)(n.HY,null,[(0,n.Wm)("div",a,[i,(0,n.Wm)(u,{onShowTooltip:l.showTooltip},null,8,["onShowTooltip"])]),(0,n.Wm)(g,{result:o.tooltip.result,event:o.tooltip.event},null,8,["result","event"]),(0,n.Wm)(c)],64)}var u=r.p+"img/github.png",g=(0,n.HX)("data-v-1cbbc992");(0,n.dD)("data-v-1cbbc992");var c={id:"social"},h=(0,n.Wm)("a",{href:"https://github.com/TwinProduction/gatus",target:"_blank",title:"Gatus on GitHub"},[(0,n.Wm)("img",{src:u,alt:"GitHub",width:"32",height:"auto"})],-1);(0,n.Cn)();var d=g((function(e,t,r,s,o,a){return(0,n.wg)(),(0,n.j4)("div",c,[h])})),m={name:"Social"};m.render=d,m.__scopeId="data-v-1cbbc992";var f=m,p=(r(6977),r(3577)),v=(0,n.Wm)("div",{class:"tooltip-title"},"Timestamp:",-1),A={id:"tooltip-timestamp"},w=(0,n.Wm)("div",{class:"tooltip-title"},"Response time:",-1),y={id:"tooltip-response-time"},x=(0,n.Wm)("div",{class:"tooltip-title"},"Conditions:",-1),T={id:"tooltip-conditions"},I=(0,n.Wm)("br",null,null,-1),b={key:0,id:"tooltip-errors-container"},k=(0,n.Wm)("div",{class:"tooltip-title"},"Errors:",-1),R={id:"tooltip-errors"},S=(0,n.Wm)("br",null,null,-1);function W(e,t,r,s,o,a){return(0,n.wg)(),(0,n.j4)("div",{id:"tooltip",ref:"tooltip",class:o.hidden?"invisible":"",style:"top:"+o.top+"px; left:"+o.left+"px"},[r.result?(0,n.WI)(e.$slots,"default",{key:0},(function(){return[v,(0,n.Wm)("code",A,(0,p.zw)(a.prettifyTimestamp(r.result.timestamp)),1),w,(0,n.Wm)("code",y,(0,p.zw)((r.result.duration/1e6).toFixed(0))+"ms",1),x,(0,n.Wm)("code",T,[((0,n.wg)(!0),(0,n.j4)(n.HY,null,(0,n.Ko)(r.result.conditionResults,(function(t){return(0,n.WI)(e.$slots,"default",{key:t},(function(){return[(0,n.Uk)((0,p.zw)(t.success?"✓":"X")+" ~ "+(0,p.zw)(t.condition),1),I]}))})),128))]),r.result.errors&&r.result.errors.length?((0,n.wg)(),(0,n.j4)("div",b,[k,(0,n.Wm)("code",R,[((0,n.wg)(!0),(0,n.j4)(n.HY,null,(0,n.Ko)(r.result.errors,(function(t){return(0,n.WI)(e.$slots,"default",{key:t},(function(){return[(0,n.Uk)(" - "+(0,p.zw)(t),1),S]}))})),128))])])):(0,n.ry)("",!0)]})):(0,n.ry)("",!0)],6)}r(4916),r(5306);var C={name:"Services",props:{event:Event,result:Object},methods:{prettifyTimestamp:function(e){var t=new Date(e),r=t.getFullYear(),s=(t.getMonth()+1<10?"0":"")+(t.getMonth()+1),n=(t.getDate()<10?"0":"")+t.getDate(),o=(t.getHours()<10?"0":"")+t.getHours(),a=(t.getMinutes()<10?"0":"")+t.getMinutes(),i=(t.getSeconds()<10?"0":"")+t.getSeconds();return r+"-"+s+"-"+n+" "+o+":"+a+":"+i},htmlEntities:function(e){return String(e).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&apos;")},reposition:function(){if(this.event&&this.event.type)if("mouseenter"===this.event.type){var e=this.event.target.getBoundingClientRect().y+30,t=this.event.target.getBoundingClientRect().x,r=this.$refs.tooltip.getBoundingClientRect();t+window.scrollX+r.width+50>document.body.getBoundingClientRect().width&&(t=this.event.target.getBoundingClientRect().x-r.width+this.event.target.getBoundingClientRect().width,t<0&&(t+=-t)),e+window.scrollY+r.height+50>document.body.getBoundingClientRect().height&&e>=0&&(e=this.event.target.getBoundingClientRect().y-(r.height+10),e<0&&(e=this.event.target.getBoundingClientRect().y+30)),this.top=e,this.left=t}else"mouseleave"===this.event.type&&(this.hidden=!0)}},watch:{event:function(e){e&&e.type&&("mouseenter"===e.type?this.hidden=!1:"mouseleave"===e.type&&(this.hidden=!0))}},updated:function(){this.reposition()},created:function(){this.reposition()},data:function(){return{hidden:!0,top:0,left:0}}};C.render=W;var B=C,P={name:"App",components:{Social:f,Tooltip:B},methods:{showTooltip:function(e,t){this.tooltip={result:e,event:t}}},data:function(){return{tooltip:{}}}};P.render=l;var D=P,E=r(2119);function H(e,t,r,s,o,a){var i=(0,n.up)("Services"),l=(0,n.up)("Pagination"),u=(0,n.up)("Settings");return(0,n.wg)(),(0,n.j4)(n.HY,null,[(0,n.Wm)(i,{serviceStatuses:o.serviceStatuses,showStatusOnHover:!0,onShowTooltip:a.showTooltip,onToggleShowAverageResponseTime:a.toggleShowAverageResponseTime,showAverageResponseTime:o.showAverageResponseTime},null,8,["serviceStatuses","onShowTooltip","onToggleShowALine truncated
(function(){"use strict";var e={8121:function(e,t,r){r.d(t,{L:function(){return et}});r(6992),r(8674),r(9601),r(7727);var s=r(9963),n=r(6252),o=r.p+"img/logo.png",a={class:"container container-xs relative mx-auto xl:rounded xl:border xl:shadow-xl xl:my-5 p-5 pb-12 xl:pb-5 text-left dark:bg-gray-800 dark:text-gray-200 dark:border-gray-500",id:"global"},i=(0,n.Wm)("div",{class:"mb-2"},[(0,n.Wm)("div",{class:"flex flex-wrap"},[(0,n.Wm)("div",{class:"w-3/4 text-left my-auto"},[(0,n.Wm)("div",{class:"text-3xl xl:text-5xl lg:text-4xl font-light"},"Health Status")]),(0,n.Wm)("div",{class:"w-1/4 flex justify-end"},[(0,n.Wm)("img",{src:o,alt:"Gatus",class:"object-scale-down",style:{"max-width":"100px","min-width":"50px","min-height":"50px"}})])])],-1);function l(e,t,r,s,o,l){var u=(0,n.up)("router-view"),g=(0,n.up)("Tooltip"),c=(0,n.up)("Social");return(0,n.wg)(),(0,n.j4)(n.HY,null,[(0,n.Wm)("div",a,[i,(0,n.Wm)(u,{onShowTooltip:l.showTooltip},null,8,["onShowTooltip"])]),(0,n.Wm)(g,{result:o.tooltip.result,event:o.tooltip.event},null,8,["result","event"]),(0,n.Wm)(c)],64)}var u=r.p+"img/github.png",g=(0,n.HX)("data-v-1cbbc992");(0,n.dD)("data-v-1cbbc992");var c={id:"social"},h=(0,n.Wm)("a",{href:"https://github.com/TwinProduction/gatus",target:"_blank",title:"Gatus on GitHub"},[(0,n.Wm)("img",{src:u,alt:"GitHub",width:"32",height:"auto"})],-1);(0,n.Cn)();var d=g((function(e,t,r,s,o,a){return(0,n.wg)(),(0,n.j4)("div",c,[h])})),m={name:"Social"};m.render=d,m.__scopeId="data-v-1cbbc992";var f=m,v=(r(6977),r(3577)),p=(0,n.Wm)("div",{class:"tooltip-title"},"Timestamp:",-1),A={id:"tooltip-timestamp"},w=(0,n.Wm)("div",{class:"tooltip-title"},"Response time:",-1),y={id:"tooltip-response-time"},T=(0,n.Wm)("div",{class:"tooltip-title"},"Conditions:",-1),x={id:"tooltip-conditions"},I=(0,n.Wm)("br",null,null,-1),b={key:0,id:"tooltip-errors-container"},R=(0,n.Wm)("div",{class:"tooltip-title"},"Errors:",-1),k={id:"tooltip-errors"},S=(0,n.Wm)("br",null,null,-1);function W(e,t,r,s,o,a){return(0,n.wg)(),(0,n.j4)("div",{id:"tooltip",ref:"tooltip",class:o.hidden?"invisible":"",style:"top:"+o.top+"px; left:"+o.left+"px"},[r.result?(0,n.WI)(e.$slots,"default",{key:0},(function(){return[p,(0,n.Wm)("code",A,(0,v.zw)(a.prettifyTimestamp(r.result.timestamp)),1),w,(0,n.Wm)("code",y,(0,v.zw)((r.result.duration/1e6).toFixed(0))+"ms",1),T,(0,n.Wm)("code",x,[((0,n.wg)(!0),(0,n.j4)(n.HY,null,(0,n.Ko)(r.result.conditionResults,(function(t){return(0,n.WI)(e.$slots,"default",{key:t},(function(){return[(0,n.Uk)((0,v.zw)(t.success?"✓":"X")+" ~ "+(0,v.zw)(t.condition),1),I]}))})),128))]),r.result.errors&&r.result.errors.length?((0,n.wg)(),(0,n.j4)("div",b,[R,(0,n.Wm)("code",k,[((0,n.wg)(!0),(0,n.j4)(n.HY,null,(0,n.Ko)(r.result.errors,(function(t){return(0,n.WI)(e.$slots,"default",{key:t},(function(){return[(0,n.Uk)(" - "+(0,v.zw)(t),1),S]}))})),128))])])):(0,n.ry)("",!0)]})):(0,n.ry)("",!0)],6)}r(4916),r(5306);var C={name:"Services",props:{event:Event,result:Object},methods:{prettifyTimestamp:function(e){var t=new Date(e),r=t.getFullYear(),s=(t.getMonth()+1<10?"0":"")+(t.getMonth()+1),n=(t.getDate()<10?"0":"")+t.getDate(),o=(t.getHours()<10?"0":"")+t.getHours(),a=(t.getMinutes()<10?"0":"")+t.getMinutes(),i=(t.getSeconds()<10?"0":"")+t.getSeconds();return r+"-"+s+"-"+n+" "+o+":"+a+":"+i},htmlEntities:function(e){return String(e).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&apos;")},reposition:function(){if(this.event&&this.event.type)if("mouseenter"===this.event.type){var e=this.event.target.getBoundingClientRect().y+30,t=this.event.target.getBoundingClientRect().x,r=this.$refs.tooltip.getBoundingClientRect();t+window.scrollX+r.width+50>document.body.getBoundingClientRect().width&&(t=this.event.target.getBoundingClientRect().x-r.width+this.event.target.getBoundingClientRect().width,t<0&&(t+=-t)),e+window.scrollY+r.height+50>document.body.getBoundingClientRect().height&&e>=0&&(e=this.event.target.getBoundingClientRect().y-(r.height+10),e<0&&(e=this.event.target.getBoundingClientRect().y+30)),this.top=e,this.left=t}else"mouseleave"===this.event.type&&(this.hidden=!0)}},watch:{event:function(e){e&&e.type&&("mouseenter"===e.type?this.hidden=!1:"mouseleave"===e.type&&(this.hidden=!0))}},updated:function(){this.reposition()},created:function(){this.reposition()},data:function(){return{hidden:!0,top:0,left:0}}};C.render=W;var B=C,P={name:"App",components:{Social:f,Tooltip:B},methods:{showTooltip:function(e,t){this.tooltip={result:e,event:t}}},data:function(){return{tooltip:{}}}};P.render=l;var D=P,E=r(2119);function H(e,t,r,s,o,a){var i=(0,n.up)("Services"),l=(0,n.up)("Pagination"),u=(0,n.up)("Settings");return(0,n.wg)(),(0,n.j4)(n.HY,null,[(0,n.Wm)(i,{serviceStatuses:o.serviceStatuses,showStatusOnHover:!0,onShowTooltip:a.showTooltip,onToggleShowAverageResponseTime:a.toggleShowAverageResponseTime,showAverageResponseTime:o.showAverageResponseTime},null,8,["serviceStatuses","onShowTooltip","onToggleShowALine truncated
+1 -1
View File
@@ -1 +1 @@
(function(){"use strict";var e={14:function(e,t,s){s.d(t,{L:function(){return nt}});var r=s(963),a=s(252),o=s.p+"img/logo.png";const n={class:"container container-xs relative mx-auto xl:rounded xl:border xl:shadow-xl xl:my-5 p-5 pb-12 xl:pb-5 text-left dark:bg-gray-800 dark:text-gray-200 dark:border-gray-500",id:"global"},i=(0,a.Wm)("div",{class:"mb-2"},[(0,a.Wm)("div",{class:"flex flex-wrap"},[(0,a.Wm)("div",{class:"w-3/4 text-left my-auto"},[(0,a.Wm)("div",{class:"text-3xl xl:text-5xl lg:text-4xl font-light"},"Health Status")]),(0,a.Wm)("div",{class:"w-1/4 flex justify-end"},[(0,a.Wm)("img",{src:o,alt:"Gatus",class:"object-scale-down",style:{"max-width":"100px","min-width":"50px","min-height":"50px"}})])])],-1);function l(e,t,s,r,o,l){const g=(0,a.up)("router-view"),h=(0,a.up)("Tooltip"),d=(0,a.up)("Social");return(0,a.wg)(),(0,a.j4)(a.HY,null,[(0,a.Wm)("div",n,[i,(0,a.Wm)(g,{onShowTooltip:l.showTooltip},null,8,["onShowTooltip"])]),(0,a.Wm)(h,{result:o.tooltip.result,event:o.tooltip.event},null,8,["result","event"]),(0,a.Wm)(d)],64)}var g=s.p+"img/github.png";const h=(0,a.HX)("data-v-1cbbc992");(0,a.dD)("data-v-1cbbc992");const d={id:"social"},u=(0,a.Wm)("a",{href:"https://github.com/TwinProduction/gatus",target:"_blank",title:"Gatus on GitHub"},[(0,a.Wm)("img",{src:g,alt:"GitHub",width:"32",height:"auto"})],-1);(0,a.Cn)();const c=h(((e,t,s,r,o,n)=>((0,a.wg)(),(0,a.j4)("div",d,[u]))));var m={name:"Social"};m.render=c,m.__scopeId="data-v-1cbbc992";var p=m,A=s(577);const v=(0,a.Wm)("div",{class:"tooltip-title"},"Timestamp:",-1),f={id:"tooltip-timestamp"},w=(0,a.Wm)("div",{class:"tooltip-title"},"Response time:",-1),y={id:"tooltip-response-time"},x=(0,a.Wm)("div",{class:"tooltip-title"},"Conditions:",-1),T={id:"tooltip-conditions"},I=(0,a.Wm)("br",null,null,-1),b={key:0,id:"tooltip-errors-container"},k=(0,a.Wm)("div",{class:"tooltip-title"},"Errors:",-1),R={id:"tooltip-errors"},S=(0,a.Wm)("br",null,null,-1);function W(e,t,s,r,o,n){return(0,a.wg)(),(0,a.j4)("div",{id:"tooltip",ref:"tooltip",class:o.hidden?"invisible":"",style:"top:"+o.top+"px; left:"+o.left+"px"},[s.result?(0,a.WI)(e.$slots,"default",{key:0},(()=>[v,(0,a.Wm)("code",f,(0,A.zw)(n.prettifyTimestamp(s.result.timestamp)),1),w,(0,a.Wm)("code",y,(0,A.zw)((s.result.duration/1e6).toFixed(0))+"ms",1),x,(0,a.Wm)("code",T,[((0,a.wg)(!0),(0,a.j4)(a.HY,null,(0,a.Ko)(s.result.conditionResults,(t=>(0,a.WI)(e.$slots,"default",{key:t},(()=>[(0,a.Uk)((0,A.zw)(t.success?"✓":"X")+" ~ "+(0,A.zw)(t.condition),1),I])))),128))]),s.result.errors&&s.result.errors.length?((0,a.wg)(),(0,a.j4)("div",b,[k,(0,a.Wm)("code",R,[((0,a.wg)(!0),(0,a.j4)(a.HY,null,(0,a.Ko)(s.result.errors,(t=>(0,a.WI)(e.$slots,"default",{key:t},(()=>[(0,a.Uk)(" - "+(0,A.zw)(t),1),S])))),128))])])):(0,a.ry)("",!0)])):(0,a.ry)("",!0)],6)}var C={name:"Services",props:{event:Event,result:Object},methods:{prettifyTimestamp(e){let t=new Date(e),s=t.getFullYear(),r=(t.getMonth()+1<10?"0":"")+(t.getMonth()+1),a=(t.getDate()<10?"0":"")+t.getDate(),o=(t.getHours()<10?"0":"")+t.getHours(),n=(t.getMinutes()<10?"0":"")+t.getMinutes(),i=(t.getSeconds()<10?"0":"")+t.getSeconds();return s+"-"+r+"-"+a+" "+o+":"+n+":"+i},htmlEntities(e){return String(e).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&apos;")},reposition(){if(this.event&&this.event.type)if("mouseenter"===this.event.type){let e=this.event.target.getBoundingClientRect().y+30,t=this.event.target.getBoundingClientRect().x,s=this.$refs.tooltip.getBoundingClientRect();t+window.scrollX+s.width+50>document.body.getBoundingClientRect().width&&(t=this.event.target.getBoundingClientRect().x-s.width+this.event.target.getBoundingClientRect().width,t<0&&(t+=-t)),e+window.scrollY+s.height+50>document.body.getBoundingClientRect().height&&e>=0&&(e=this.event.target.getBoundingClientRect().y-(s.height+10),e<0&&(e=this.event.target.getBoundingClientRect().y+30)),this.top=e,this.left=t}else"mouseleave"===this.event.type&&(this.hidden=!0)}},watch:{event:function(e){e&&e.type&&("mouseenter"===e.type?this.hidden=!1:"mouseleave"===e.type&&(this.hidden=!0))}},updated(){this.reposition()},created(){this.reposition()},data(){return{hidden:!0,top:0,left:0}}};C.render=W;var B=C,P={name:"App",components:{Social:p,Tooltip:B},methods:{showTooltip(e,t){this.tooltip={result:e,event:t}}},data(){return{tooltip:{}}}};P.render=l;var D=P,E=s(119);function H(e,t,s,r,o,n){const i=(0,a.up)("Services"),l=(0,a.up)("Pagination"),g=(0,a.up)("Settings");return(0,a.wg)(),(0,a.j4)(a.HY,null,[(0,a.Wm)(i,{serviceStatuses:o.serviceStatuses,showStatusOnHover:!0,onShowTooltip:n.showTooltip,onToggleShowAverageResponseTime:n.toggleShowAverageResponseTime,showAverageResponseTime:o.showAverageResponseTime},null,8,["serviceStatuses","onShowTooltip","onToggleShowAverageResponseTime","showAverageResponseTime"]),(0,a.Wm)(l,{onPage:n.changePage},null,8,["onPage"]),(0,a.Wm)(g,{onRefreshData:n.fetchData},null,8,["onRefreshData"])],64)}const O={id:"settings"Line truncated
(function(){"use strict";var e={121:function(e,t,s){s.d(t,{L:function(){return et}});var r=s(963),a=s(252),o=s.p+"img/logo.png";const n={class:"container container-xs relative mx-auto xl:rounded xl:border xl:shadow-xl xl:my-5 p-5 pb-12 xl:pb-5 text-left dark:bg-gray-800 dark:text-gray-200 dark:border-gray-500",id:"global"},i=(0,a.Wm)("div",{class:"mb-2"},[(0,a.Wm)("div",{class:"flex flex-wrap"},[(0,a.Wm)("div",{class:"w-3/4 text-left my-auto"},[(0,a.Wm)("div",{class:"text-3xl xl:text-5xl lg:text-4xl font-light"},"Health Status")]),(0,a.Wm)("div",{class:"w-1/4 flex justify-end"},[(0,a.Wm)("img",{src:o,alt:"Gatus",class:"object-scale-down",style:{"max-width":"100px","min-width":"50px","min-height":"50px"}})])])],-1);function l(e,t,s,r,o,l){const g=(0,a.up)("router-view"),h=(0,a.up)("Tooltip"),d=(0,a.up)("Social");return(0,a.wg)(),(0,a.j4)(a.HY,null,[(0,a.Wm)("div",n,[i,(0,a.Wm)(g,{onShowTooltip:l.showTooltip},null,8,["onShowTooltip"])]),(0,a.Wm)(h,{result:o.tooltip.result,event:o.tooltip.event},null,8,["result","event"]),(0,a.Wm)(d)],64)}var g=s.p+"img/github.png";const h=(0,a.HX)("data-v-1cbbc992");(0,a.dD)("data-v-1cbbc992");const d={id:"social"},u=(0,a.Wm)("a",{href:"https://github.com/TwinProduction/gatus",target:"_blank",title:"Gatus on GitHub"},[(0,a.Wm)("img",{src:g,alt:"GitHub",width:"32",height:"auto"})],-1);(0,a.Cn)();const c=h(((e,t,s,r,o,n)=>((0,a.wg)(),(0,a.j4)("div",d,[u]))));var m={name:"Social"};m.render=c,m.__scopeId="data-v-1cbbc992";var A=m,p=s(577);const v=(0,a.Wm)("div",{class:"tooltip-title"},"Timestamp:",-1),f={id:"tooltip-timestamp"},w=(0,a.Wm)("div",{class:"tooltip-title"},"Response time:",-1),y={id:"tooltip-response-time"},T=(0,a.Wm)("div",{class:"tooltip-title"},"Conditions:",-1),x={id:"tooltip-conditions"},I=(0,a.Wm)("br",null,null,-1),b={key:0,id:"tooltip-errors-container"},R=(0,a.Wm)("div",{class:"tooltip-title"},"Errors:",-1),k={id:"tooltip-errors"},S=(0,a.Wm)("br",null,null,-1);function W(e,t,s,r,o,n){return(0,a.wg)(),(0,a.j4)("div",{id:"tooltip",ref:"tooltip",class:o.hidden?"invisible":"",style:"top:"+o.top+"px; left:"+o.left+"px"},[s.result?(0,a.WI)(e.$slots,"default",{key:0},(()=>[v,(0,a.Wm)("code",f,(0,p.zw)(n.prettifyTimestamp(s.result.timestamp)),1),w,(0,a.Wm)("code",y,(0,p.zw)((s.result.duration/1e6).toFixed(0))+"ms",1),T,(0,a.Wm)("code",x,[((0,a.wg)(!0),(0,a.j4)(a.HY,null,(0,a.Ko)(s.result.conditionResults,(t=>(0,a.WI)(e.$slots,"default",{key:t},(()=>[(0,a.Uk)((0,p.zw)(t.success?"✓":"X")+" ~ "+(0,p.zw)(t.condition),1),I])))),128))]),s.result.errors&&s.result.errors.length?((0,a.wg)(),(0,a.j4)("div",b,[R,(0,a.Wm)("code",k,[((0,a.wg)(!0),(0,a.j4)(a.HY,null,(0,a.Ko)(s.result.errors,(t=>(0,a.WI)(e.$slots,"default",{key:t},(()=>[(0,a.Uk)(" - "+(0,p.zw)(t),1),S])))),128))])])):(0,a.ry)("",!0)])):(0,a.ry)("",!0)],6)}var C={name:"Services",props:{event:Event,result:Object},methods:{prettifyTimestamp(e){let t=new Date(e),s=t.getFullYear(),r=(t.getMonth()+1<10?"0":"")+(t.getMonth()+1),a=(t.getDate()<10?"0":"")+t.getDate(),o=(t.getHours()<10?"0":"")+t.getHours(),n=(t.getMinutes()<10?"0":"")+t.getMinutes(),i=(t.getSeconds()<10?"0":"")+t.getSeconds();return s+"-"+r+"-"+a+" "+o+":"+n+":"+i},htmlEntities(e){return String(e).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&apos;")},reposition(){if(this.event&&this.event.type)if("mouseenter"===this.event.type){let e=this.event.target.getBoundingClientRect().y+30,t=this.event.target.getBoundingClientRect().x,s=this.$refs.tooltip.getBoundingClientRect();t+window.scrollX+s.width+50>document.body.getBoundingClientRect().width&&(t=this.event.target.getBoundingClientRect().x-s.width+this.event.target.getBoundingClientRect().width,t<0&&(t+=-t)),e+window.scrollY+s.height+50>document.body.getBoundingClientRect().height&&e>=0&&(e=this.event.target.getBoundingClientRect().y-(s.height+10),e<0&&(e=this.event.target.getBoundingClientRect().y+30)),this.top=e,this.left=t}else"mouseleave"===this.event.type&&(this.hidden=!0)}},watch:{event:function(e){e&&e.type&&("mouseenter"===e.type?this.hidden=!1:"mouseleave"===e.type&&(this.hidden=!0))}},updated(){this.reposition()},created(){this.reposition()},data(){return{hidden:!0,top:0,left:0}}};C.render=W;var B=C,P={name:"App",components:{Social:A,Tooltip:B},methods:{showTooltip(e,t){this.tooltip={result:e,event:t}}},data(){return{tooltip:{}}}};P.render=l;var D=P,E=s(119);function H(e,t,s,r,o,n){const i=(0,a.up)("Services"),l=(0,a.up)("Pagination"),g=(0,a.up)("Settings");return(0,a.wg)(),(0,a.j4)(a.HY,null,[(0,a.Wm)(i,{serviceStatuses:o.serviceStatuses,showStatusOnHover:!0,onShowTooltip:n.showTooltip,onToggleShowAverageResponseTime:n.toggleShowAverageResponseTime,showAverageResponseTime:o.showAverageResponseTime},null,8,["serviceStatuses","onShowTooltip","onToggleShowAverageResponseTime","showAverageResponseTime"]),(0,a.Wm)(l,{onPage:n.changePage},null,8,["onPage"]),(0,a.Wm)(g,{onRefreshData:n.fetchData},null,8,["onRefreshData"])],64)}const O={id:"settingsLine truncated