statuscakedev / statuscake-go Goto Github PK
View Code? Open in Web Editor NEWStatusCake Go SDK
Home Page: https://developers.statuscake.com
License: MIT License
StatusCake Go SDK
Home Page: https://developers.statuscake.com
License: MIT License
I suspect this is an API issue rather than within the client, but figured this would be the best place to report it for now.
I've been working on writing a custom Terraform provider for StatusCake using this client & the beta api, and it seems that the BasicUser
& BasicPass
properties for an uptime request cannot be update by the api client:
package main
import (
"context"
"github.com/StatusCakeDev/statuscake-go"
"log"
"os"
)
func main() {
apiToken, _ := os.LookupEnv("STATUSCAKE_API_KEY")
client := statuscake.NewAPIClient(apiToken)
res, err := client.CreateUptimeTest(context.TODO()).
Name("my test").
TestType("HTTP").
WebsiteURL("https://www.example.com").
CheckRate(300).
Execute()
if err != nil {
panic(err)
}
log.Println("Successfully created Uptime test")
err = client.UpdateUptimeTest(context.TODO(), res.Data.NewID).
BasicUser("user").
BasicPass("pass").
Execute()
if err != nil {
panic(err)
}
log.Println("Successfully updated Uptime test")
}
terraform-provider-statuscake2 on main took 3s
❯ go run te.go
2021/10/03 16:22:55 Successfully created Uptime test
panic: Invalid request. No changes were requested. Please check your inputs and try again.
goroutine 1 [running]:
main.main()
/c/Users/G-Rath/workspace/projects-personal/terraform-provider-statuscake2/te.go:34 +0x6f7
exit status 2
Is your feature request related to a problem? Please describe.
statuscale-go client has interfaces like UptimeAPI
, which is great. It allows us to mock them in our unit tests. The problem is with structs like APICreateUptimeTestRequest
or APIUpdateUptimeTestRequest
, where fields are private and there are not read-access methods. So we cannot really validate in tests whether properties have valid values. Workaround would be to use httpmock, which raises a question of the necessity of the statuscake-go client in general. In this case we could use REST api.
Describe the solution you'd like
It would be great to have properties exported in structs like APICreateUptimeTestRequest
or APIUpdateUptimeTestRequest
Please add support of the heartbeat API so the terraform provider can benefit from it (see here the issue for StatusCakeDev/terraform-provider-statuscake#74 )
Describe the bug
StatusCake client supports retry, but it's working only for requests without request body. In the case of requests with a body, client tries to read from an empty (already read) buffer during retries. So following (retried) requests fail because the ContentLength header does not match the body size (body is empty).
To Reproduce
Simple test to reproduce a bug:
package statuscake
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/StatusCakeDev/statuscake-go"
"github.com/stretchr/testify/assert"
)
// this works (GET method)
func Test_GetUptimeTest_Retry(t *testing.T) {
t.Run("Get UptimeTest retry", func(t *testing.T) {
ts := createMockServer(3)
defer ts.Close()
client := statuscake.NewClient(
statuscake.WithHost(ts.URL),
statuscake.WithMaxRetries(5),
)
ut, err := client.GetUptimeTest(context.Background(), "my-test-id").Execute()
assert.NoError(t, err, "Expecting no error")
assert.NotNil(t, ut, "Expecting UptimeTest object")
})
}
// this does not work (PUT method)
func Test_UpdateUptimeTest_Retry(t *testing.T) {
t.Run("Update UptimeTest retry", func(t *testing.T) {
ts := createMockServer(3)
defer ts.Close()
client := statuscake.NewClient(
statuscake.WithHost(ts.URL),
statuscake.WithMaxRetries(5),
)
err := client.UpdateUptimeTest(context.Background(), "my-test-id").Paused(true).Execute()
assert.NoError(t, err, "Expecting no error")
})
}
func createMockServer(networkFailures int) *httptest.Server {
counter := 1
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if counter <= networkFailures { // simulate network issue - close connection
hj, _ := w.(http.Hijacker)
conn, _, _ := hj.Hijack()
_ = conn.Close()
} else if r.Method == http.MethodGet { // get test
w.Header().Add("Content-Type", contentType)
w.WriteHeader(200)
_, _ = w.Write([]byte(`{"data":{"id":"my-test-id","paused":false,"name":"Google","test_type":"HTTP","website_url":"https://www.google.com","check_rate":900,"confirmation":2,"contact_groups":[],"do_not_find":false,"enable_ssl_alert":false,"follow_redirects":true,"include_header":false,"servers":[],"processing":false,"status":"up","tags":[],"timeout":15,"trigger_rate":0,"uptime":100,"use_jar":false,"last_tested_at":"2022-08-18T06:36:53+00:00","next_location":"UNSET","status_codes":["204"]}}`))
} else if r.Method == http.MethodPut { // update test
w.Header().Add("Content-Type", contentType)
w.WriteHeader(204)
}
counter++
}))
return ts
}
Expected behavior
Retry should also work for update methods.
Device:
Describe the bug
""
).Additional context
We are running a small go tool to perform OAuth token updates on StatusCake uptime tests.
To Reproduce
We've since found the Debug
option within the library but it's too verbose to run all the time. We've built in flags so we can try to get more detail next time if it becomes reproducible/consistent.
Most recently it was Updating
an uptime test. We've seen it a fair few times on the GetUptimeTest
too, please note the edge case catches in the code below.
You could use the StatusCake API changes that resulted in support request 22508200886017? They triggered it persistently. My hunch is it's a response that doesn't correspond to the expected response format, but that's a complete guess based on similar issues I've seen elsewhere in client libs.
Expected behavior
Library errors should allow clients to provide context failure to StatusCake. An ""
error return is probably never be acceptable.
Detail
I've smashed the relevant bits out of a few places into the below representative code:
client, err := NewStatusCakeClient(statusCakeCfg)
if err != nil {
return "", err
}
testResp, err := client.GetUptimeTest(ctx, id).Execute()
if err != nil {
return "", fmt.Errorf("failed to get Uptime Test: %w", err)
}
if testResp.Data.Name == "" {
return "", fmt.Errorf("failed to get Uptime Test: %w", errors.New("response returned without error but has no name"))
}
if testResp.Data.ID == "" {
return "", fmt.Errorf("failed to get Uptime Test: %w", errors.New("response returned without error but has no ID"))
}
test := testResp.Data
l.Info().Str("name", t.Name).Msgf("Found test: %s", t.Name)
token, err := GetOAuthTokens(ctx, l, oathConfig, refreshToken)
if err != nil {
return "", fmt.Errorf("failed to get new token: %w", err)
}
if token.AccessToken == "" {
return token.RefreshToken, errors.New("access token was empty")
}
l.Debug().Str("token", token.AccessToken).Str("refreshtoken", token.RefreshToken).Msgf("Got new token: %s", token.AccessToken)
bytes, err := json.Marshal(&struct {
Content string `json:"content-type"`
Cache string `json:"cache-control"`
Authorization string `json:"authorization"`
}{
Content: "application/json",
Cache: "no-cache",
Authorization: fmt.Sprintf("Bearer %s", token.AccessToken),
})
header := string(bytes)
l.Debug().Msgf("Got new header state: %s", header)
if err != nil {
return token.RefreshToken, fmt.Errorf("failed to encode new custom header state: %w", err)
}
r := client.UpdateUptimeTest(ctx, id).CustomHeader(customHeader)
if err := r.Execute(); err != nil {
return token.RefreshToken, fmt.Errorf("failed to update uptime test: %w", err)
}
return token.RefreshToken, nil
Application logs look like the following, note the lack of error detail, I'd expect an error there that described the issue.
2024-04-17T11:31:29.750+01:00 INF Found test: Product - Prod - API every=600000 id=123456 name="Product - Prod - API"
2024-04-17T11:31:29.891+01:00 ERR Failed to update Uptime Test authentication: failed to update uptime test: error="failed to update uptime test: " every=600000 id=123456
Hello guys, I tried to fetch uptime history using start and end method but it gives me error json unmarshal. Here's my code:
fmt.Println(s.Unix(), e.Unix())
res, err := client.ListUptimeTestHistory(context.Background(), "6128010").
Start(1632268800).End(1632355140).Limit(100).
Execute()
if err != nil {
panic(err)
}
data, _ := json.MarshalIndent(res.Data, "", "🐱")
fmt.Print(string(data))
And here is the error:
panic: json: cannot unmarshal array into Go struct field UptimeTestHistory.data of type map[string]statuscake.UptimeTestHistoryResult: failed to deserialise response body
I don't know if it's not the way I should use the method or it's bug on the code itself. I don't find any example with Start and End method both in documentation and github. I'm using Go 1.17 on darwin.
Thanks for the effort and making this available, much appreciated!
I've got an OAuth2 API that I need to monitor so I'm just slapping something together to keep the custom header token refreshed. I've been successfully using this library for the other tasks (checking the Uptime Test exists, getting a list of Uptime Tests etc) but have run into issues with the UpdateUptimeTest
call. This is the first application/x-www-form-urlencoded
call I've needed to make from your library.
Calling the UpdateUptimeTest
library function seems to 500 and error (below) even when doing it by "hand" with the same configuration works correctly.
2022-10-25T20:43:33.113+01:00 FTL Failed to update Uptime Test authentication: failed to update uptime test: Unexpected error. Try again later. error="failed to update uptime test: Unexpected error. Try again later."
I've made the call successfully in order to confirm that the issue is not with the API. Below is a chunk of that code, the uncommented block is just calling the API using a http.Client
, the lower commented out section is the library call to do the same thing (which I can't get to work, error above).
type StatusCakeClient struct {
c *statuscake.Client
h *http.Client
cfg *config.StatusCake
}
// UpdateUptimeTestCustomHeader replaces the custom header of the supplied Uptime Test.
func (s *StatusCakeClient) UpdateUptimeTestCustomHeader(ctx context.Context, id, customHeader string) (err error) {
requestBody := url.Values{}
requestBody.Add("custom_header", customHeader)
req, err := http.NewRequestWithContext(ctx, http.MethodPut, fmt.Sprintf("https://api.statuscake.com/v1/uptime/%v", id), strings.NewReader(requestBody.Encode()))
if err != nil {
return fmt.Errorf("failed to construct Uptime Test update request: %w", err)
}
req.Header.Set("content-type", "application/x-www-form-urlencoded")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.cfg.Token))
resp, err := s.h.Do(req)
defer func() {
if resp != nil && resp.Body != nil {
if closeErr := resp.Body.Close(); closeErr != nil && err == nil {
err = fmt.Errorf("failed to close response body: %w", closeErr)
}
}
}()
if err != nil {
return fmt.Errorf("failed to send Uptime Test update request: %w", err)
}
if resp.StatusCode != 204 {
bytes, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("%w (%v) and couldn't read body: %v", ErrNot20XResponse, resp.StatusCode, err)
}
return fmt.Errorf("%w (%v): %s", ErrNot20XResponse, resp.StatusCode, string(bytes))
}
return nil
// Library call doesn't seem to work, 500s?
//
// r := s.c.UpdateUptimeTest(ctx, id)
// r.CustomHeader(customHeader)
// if err := r.Execute(); err != nil {
// return fmt.Errorf("failed to update uptime test: %w", err)
// }
//
// return nil
}
Am I calling this wrong from within the library?
It seems that the include_header
property is not included in the "get single uptime test" response, which means it's not possible to check what value it is set to.
This makes it awkward to handle in e.g. a terraform provider, because we either have to mark the attribute as requiring a completely new resource to be created, or just hope what is in our state is correct (and maybe always include it when updating that resource) which means we can't detect drift.
If you create a new uptime test with an empty array for status_codes
, the api instead adds all the status codes.
This means you can only remove all statuscodes with an update request.
It would be good if there way a way to tell the api to not create an uptime test with any statuscodes.
Describe the bug
The behaviour of the retry feature is a little bit weird. The result of the last retried call is ignored and the error is returned.
To Reproduce
A simple test to reproduce:
package statuscake
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/StatusCakeDev/statuscake-go"
"github.com/stretchr/testify/assert"
)
func Test_ListUptimeTests_Retry(t *testing.T) {
t.Run("Test ListUptime retry", func(t *testing.T) {
serverCallCounter := 0
ts := createMockServer(1, &serverCallCounter) // first call will fail, but the second will be successful
defer ts.Close()
client := statuscake.NewClient(
statuscake.WithHost(ts.URL),
statuscake.WithMaxRetries(1), // we expect one (failed) call + one (success) retry in test
)
_, err := client.ListUptimeTests(context.Background()).Execute()
assert.Equal(t, 2, serverCallCounter) // client called API twice, that's OK
assert.NoError(t, err, "Expecting no error") // test should pass, retry was successful, but error is returned :sad-panda:
})
}
func createMockServer(networkFailures int, counter *int) *httptest.Server {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
*counter++
if *counter <= networkFailures { // simulate network issue - close connection
hj, _ := w.(http.Hijacker)
conn, _, _ := hj.Hijack()
_ = conn.Close()
} else if r.Method == http.MethodGet { // get test
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(200)
_, _ = w.Write([]byte(`{"data":[],"metadata":{"page":1,"per_page":25,"page_count":1,"total_count":0}}`))
}
}))
return ts
}
Expected behavior
Device:
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.