Compare commits

..

5 Commits

Author SHA1 Message Date
TwiN
7c27fcb895 fix: Convert key to lowercase when looking up endpoint by key (#1150) 2025-07-08 12:21:56 -04:00
TwiN
3db5894e90 fix: Limit the pageSize to maximum-number-of-results on first page (#1149) 2025-07-08 12:08:27 -04:00
Bryan Cross
9b1d15c9e0 feat(api): Add optional duration to external endpoint results (#1092)
* feat(api): Add optional duration to external endpoint results

* Fix failing tests

* Parse duration regardless of success

* Use len instead of equality

* Update README.md

* Include error in output

* Fix result numbering

* Update README.md

* Update api/external_endpoint.go

---------

Co-authored-by: TwiN <twin@linux.com>
2025-07-08 11:53:57 -04:00
TwiN
1855718e46 ci: Increase workflow timeouts because GHA instances seems to have gotten slower 2025-07-01 18:28:49 -04:00
TwiN
d5f2d92e8e fix(ui): Explicitly omit MaximumNumberOfResults from being parsed to yaml in UI config 2025-07-01 18:04:37 -04:00
13 changed files with 55 additions and 26 deletions

View File

@@ -18,7 +18,7 @@ jobs:
build:
name: benchmark
runs-on: ubuntu-latest
timeout-minutes: 5
timeout-minutes: 15
steps:
- uses: actions/setup-go@v5
with:

View File

@@ -8,7 +8,7 @@ on:
jobs:
publish-custom:
runs-on: ubuntu-latest
timeout-minutes: 20
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
- name: Set up QEMU

View File

@@ -3,7 +3,7 @@ on: [workflow_dispatch]
jobs:
publish-experimental:
runs-on: ubuntu-latest
timeout-minutes: 20
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
- name: Set up QEMU

View File

@@ -11,7 +11,7 @@ jobs:
publish-latest:
runs-on: ubuntu-latest
if: ${{ (github.event.workflow_run.conclusion == 'success') && (github.event.workflow_run.head_repository.full_name == github.repository) }}
timeout-minutes: 90
timeout-minutes: 120
steps:
- uses: actions/checkout@v4
- name: Set up QEMU

View File

@@ -6,7 +6,7 @@ jobs:
publish-release:
name: publish-release
runs-on: ubuntu-latest
timeout-minutes: 120
timeout-minutes: 240
steps:
- uses: actions/checkout@v4
- name: Set up QEMU

View File

@@ -11,7 +11,7 @@ on:
jobs:
test-ui:
runs-on: ubuntu-latest
timeout-minutes: 10
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- run: make frontend-install-dependencies

View File

@@ -14,7 +14,7 @@ on:
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 10
timeout-minutes: 30
steps:
- uses: actions/setup-go@v5
with:

View File

@@ -321,13 +321,14 @@ external-endpoints:
To push the status of an external endpoint, the request would have to look like this:
```
POST /api/v1/endpoints/{key}/external?success={success}&error={error}
POST /api/v1/endpoints/{key}/external?success={success}&error={error}&duration={duration}
```
Where:
- `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,`, `.` and `#` replaced by `-`.
- Using the example configuration above, the key would be `core_ext-ep-test`.
- `{success}` is a boolean (`true` or `false`) value indicating whether the health check was successful or not.
- `{error}`: a string describing the reason for a failed health check. If {success} is false, this should contain the error message; if the check is successful, it can be omitted or left empty.
- `{error}` (optional): a string describing the reason for a failed health check. If {success} is false, this should contain the error message; if the check is successful.
- `{duration}` (optional): the time that the request took as a duration string (e.g. 10s).
You must also pass the token as a `Bearer` token in the `Authorization` header.

View File

@@ -46,6 +46,14 @@ func CreateExternalEndpointResult(cfg *config.Config) fiber.Handler {
Success: c.QueryBool("success"),
Errors: []string{},
}
if len(c.Query("duration")) > 0 {
parsedDuration, err := time.ParseDuration(c.Query("duration"))
if err != nil {
logr.Errorf("[api.CreateExternalEndpointResult] Invalid duration from string=%s with error: %s", c.Query("duration"), err.Error())
return c.Status(400).SendString("invalid duration: " + err.Error())
}
result.Duration = parsedDuration
}
if !result.Success && c.Query("error") != "" {
result.Errors = append(result.Errors, c.Query("error"))
}

View File

@@ -70,6 +70,12 @@ func TestCreateExternalEndpointResult(t *testing.T) {
AuthorizationHeaderBearerToken: "Bearer token",
ExpectedCode: 400,
},
{
Name: "bad-duration-value",
Path: "/api/v1/endpoints/g_n/external?success=true&duration=invalid",
AuthorizationHeaderBearerToken: "Bearer token",
ExpectedCode: 400,
},
{
Name: "good-token-success-true",
Path: "/api/v1/endpoints/g_n/external?success=true",
@@ -82,6 +88,12 @@ func TestCreateExternalEndpointResult(t *testing.T) {
AuthorizationHeaderBearerToken: "Bearer token",
ExpectedCode: 200,
},
{
Name: "good-duration-success-true",
Path: "/api/v1/endpoints/g_n/external?success=true&duration=10s",
AuthorizationHeaderBearerToken: "Bearer token",
ExpectedCode: 200,
},
{
Name: "good-token-success-false",
Path: "/api/v1/endpoints/g_n/external?success=false",
@@ -118,7 +130,7 @@ func TestCreateExternalEndpointResult(t *testing.T) {
})
}
t.Run("verify-end-results", func(t *testing.T) {
endpointStatus, err := store.Get().GetEndpointStatus("g", "n", paging.NewEndpointStatusParams().WithResults(1, 10))
endpointStatus, err := store.Get().GetEndpointStatus("g", "n", paging.NewEndpointStatusParams().WithResults(1, 11))
if err != nil {
t.Errorf("failed to get endpoint status: %s", err.Error())
return
@@ -126,8 +138,8 @@ func TestCreateExternalEndpointResult(t *testing.T) {
if endpointStatus.Key != "g_n" {
t.Errorf("expected key to be g_n but got %s", endpointStatus.Key)
}
if len(endpointStatus.Results) != 5 {
t.Errorf("expected 3 results but got %d", len(endpointStatus.Results))
if len(endpointStatus.Results) != 6 {
t.Errorf("expected 6 results but got %d", len(endpointStatus.Results))
}
if !endpointStatus.Results[0].Success {
t.Errorf("expected first result to be successful")
@@ -138,8 +150,8 @@ func TestCreateExternalEndpointResult(t *testing.T) {
if len(endpointStatus.Results[1].Errors) > 0 {
t.Errorf("expected second result to have no errors")
}
if endpointStatus.Results[2].Success {
t.Errorf("expected third result to be unsuccessful")
if endpointStatus.Results[2].Duration == 0 || endpointStatus.Results[2].Duration.Seconds() != 10 {
t.Errorf("expected third result to have a duration of 10 seconds")
}
if endpointStatus.Results[3].Success {
t.Errorf("expected fourth result to be unsuccessful")
@@ -147,8 +159,11 @@ func TestCreateExternalEndpointResult(t *testing.T) {
if endpointStatus.Results[4].Success {
t.Errorf("expected fifth result to be unsuccessful")
}
if len(endpointStatus.Results[4].Errors) == 0 || endpointStatus.Results[4].Errors[0] != "failed" {
t.Errorf("expected fifth result to have errors: failed")
if endpointStatus.Results[5].Success {
t.Errorf("expected sixth result to be unsuccessful")
}
if len(endpointStatus.Results[5].Errors) == 0 || endpointStatus.Results[5].Errors[0] != "failed" {
t.Errorf("expected sixth result to have errors: failed")
}
externalEndpointFromConfig := cfg.GetExternalEndpointByKey("g_n")
if externalEndpointFromConfig.NumberOfFailuresInARow != 3 {

View File

@@ -34,11 +34,13 @@ func extractPageAndPageSizeFromRequest(c *fiber.Ctx, maximumNumberOfResults int)
if err != nil {
pageSize = DefaultPageSize
}
if pageSize > maximumNumberOfResults {
pageSize = maximumNumberOfResults
} else if pageSize < 1 {
pageSize = DefaultPageSize
}
}
if page == 1 && pageSize > maximumNumberOfResults {
// If the page is 1 and the page size is greater than the maximum number of results, return
// no more than the maximum number of results
pageSize = maximumNumberOfResults
} else if pageSize < 1 {
pageSize = DefaultPageSize
}
return
}

View File

@@ -106,7 +106,7 @@ type Config struct {
func (config *Config) GetEndpointByKey(key string) *endpoint.Endpoint {
for i := 0; i < len(config.Endpoints); i++ {
ep := config.Endpoints[i]
if ep.Key() == key {
if ep.Key() == strings.ToLower(key) {
return ep
}
}
@@ -116,7 +116,7 @@ func (config *Config) GetEndpointByKey(key string) *endpoint.Endpoint {
func (config *Config) GetExternalEndpointByKey(key string) *endpoint.ExternalEndpoint {
for i := 0; i < len(config.ExternalEndpoints); i++ {
ee := config.ExternalEndpoints[i]
if ee.Key() == key {
if ee.Key() == strings.ToLower(key) {
return ee
}
}
@@ -411,8 +411,8 @@ func validateAlertingConfig(alertingConfig *alerting.Config, endpoints []*endpoi
alert.TypeGitea,
alert.TypeGoogleChat,
alert.TypeGotify,
alert.TypeHomeAssistant,
alert.TypeIlert,
alert.TypeHomeAssistant,
alert.TypeIlert,
alert.TypeIncidentIO,
alert.TypeJetBrainsSpace,
alert.TypeMatrix,

View File

@@ -35,7 +35,10 @@ type Config struct {
CustomCSS string `yaml:"custom-css,omitempty"` // Custom CSS to include in the page
DarkMode *bool `yaml:"dark-mode,omitempty"` // DarkMode is a flag to enable dark mode by default
MaximumNumberOfResults int // MaximumNumberOfResults to display on the page, it's not configurable because we're passing it from the storage config
//////////////////////////////////////////////
// Non-configurable - used for UI rendering //
//////////////////////////////////////////////
MaximumNumberOfResults int `yaml:"-"` // MaximumNumberOfResults to display on the page, it's not configurable because we're passing it from the storage config
}
func (cfg *Config) IsDarkMode() bool {