Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

### Enhancements:

- feat(observability/loggingendpointerrors): add support for `Logging Endpoint Errors` `GET` operation ([#800](https://github.com/fastly/go-fastly/pull/800))

### Dependencies:

### Bug fixes:
Expand Down
55 changes: 55 additions & 0 deletions fastly/fixtures/observability_endpoint_error_stream/get.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
---
version: 1
interactions:
- request:
body: ""
form: {}
headers:
User-Agent:
- FastlyGo/14.0.0 (+github.com/fastly/go-fastly; go1.25.7)
url: https://api.fastly.com/observability/service/kKJb5bOFI47uHeBVluGfX1/logging/errors?from=1775587245&to=1775587545
method: GET
response:
body: |
{"s":27,"t":1775587254943532,"o":"logging_error","r":"","m":"request failed","e":"Broken Log","details":"{\"level\":\"error\",\"error\":\"non-temporary request err: Get \\\"https://my-broken.logging.org/.well-known/fastly/logging/challenge\\\": lookup my-broken.logging.org. on 127.0.0.1:53: no such host\",\"name\":\"prebatch http challenge check\"}"}
{"s":18,"t":1775587256981715,"o":"logging_error","r":"","m":"request failed","e":"Broken Log","details":"{\"level\":\"error\",\"error\":\"non-temporary request err: Get \\\"https://my-broken.logging.org/.well-known/fastly/logging/challenge\\\": lookup my-broken.logging.org. on 127.0.0.1:53: no such host\",\"name\":\"prebatch http challenge check\"}"}
{"s":28,"t":1775587292616056,"o":"logging_error","r":"","m":"request failed","e":"Broken Log","details":"{\"level\":\"error\",\"error\":\"non-temporary request err: Get \\\"https://my-broken.logging.org/.well-known/fastly/logging/challenge\\\": lookup my-broken.logging.org. on 127.0.0.1:53: no such host\",\"name\":\"prebatch http challenge check\"}"}
{"s":19,"t":1775587315723327,"o":"logging_error","r":"","m":"request failed","e":"Broken Log","details":"{\"level\":\"error\",\"error\":\"non-temporary request err: Get \\\"https://my-broken.logging.org/.well-known/fastly/logging/challenge\\\": lookup my-broken.logging.org. on 127.0.0.1:53: no such host\",\"name\":\"prebatch http challenge check\"}"}
{"s":18,"t":1775587343917296,"o":"logging_error","r":"","m":"request failed","e":"Broken Log","details":"{\"level\":\"error\",\"error\":\"non-temporary request err: Get \\\"https://my-broken.logging.org/.well-known/fastly/logging/challenge\\\": lookup my-broken.logging.org. on 127.0.0.1:53: no such host\",\"name\":\"prebatch http challenge check\"}"}
{"s":18,"t":1775587383921446,"o":"logging_error","r":"","m":"request failed","e":"Broken Log","details":"{\"level\":\"error\",\"error\":\"non-temporary request err: Get \\\"https://my-broken.logging.org/.well-known/fastly/logging/challenge\\\": lookup my-broken.logging.org. on 127.0.0.1:53: no such host\",\"name\":\"prebatch http challenge check\"}"}
{"s":16,"t":1775587403717981,"o":"logging_error","r":"","m":"request failed","e":"Broken Log","details":"{\"level\":\"error\",\"error\":\"non-temporary request err: Get \\\"https://my-broken.logging.org/.well-known/fastly/logging/challenge\\\": lookup my-broken.logging.org. on 127.0.0.1:53: no such host\",\"name\":\"prebatch http challenge check\"}"}
{"s":18,"t":1775587437645880,"o":"logging_error","r":"","m":"request failed","e":"Broken Log","details":"{\"level\":\"error\",\"error\":\"non-temporary request err: Get \\\"https://my-broken.logging.org/.well-known/fastly/logging/challenge\\\": lookup my-broken.logging.org. on 127.0.0.1:53: no such host\",\"name\":\"prebatch http challenge check\"}"}
{"s":15,"t":1775587465219462,"o":"logging_error","r":"","m":"request failed","e":"Broken Log","details":"{\"level\":\"error\",\"error\":\"non-temporary request err: Get \\\"https://my-broken.logging.org/.well-known/fastly/logging/challenge\\\": lookup my-broken.logging.org. on 127.0.0.1:53: no such host\",\"name\":\"prebatch http challenge check\"}"}
{"s":15,"t":1775587529106194,"o":"logging_error","r":"","m":"request failed","e":"Broken Log","details":"{\"level\":\"error\",\"error\":\"non-temporary request err: Get \\\"https://my-broken.logging.org/.well-known/fastly/logging/challenge\\\": lookup my-broken.logging.org. on 127.0.0.1:53: no such host\",\"name\":\"prebatch http challenge check\"}"}
headers:
Accept-Ranges:
- bytes
Cache-Control:
- no-store
Date:
- Tue, 07 Apr 2026 18:56:02 GMT
Link:
- </observability/service/kKJb5bOFI47uHeBVluGfX1/logging/errors%3Ffrom=1775587250>;
rel="next", </observability/service/kKJb5bOFI47uHeBVluGfX1/logging/errors%3Ffrom=1775587230>;
rel="prev"
Pragma:
- no-cache
Server:
- fastly
Strict-Transport-Security:
- max-age=31536000
Vary:
- Fastly-Key,Accept-Encoding,Accept
Via:
- 1.1 varnish, 1.1 varnish
X-Cache:
- MISS, MISS
X-Cache-Hits:
- 0, 0
X-Served-By:
- cache-chi-kigq8000149-CHI, cache-ewr-kewr1740068-EWR
X-Timer:
- S1775588162.231974,VS0,VE55
status: 200 OK
code: 200
duration: ""
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
---
version: 1
interactions:
- request:
body: ""
form: {}
headers:
User-Agent:
- FastlyGo/14.0.0 (+github.com/fastly/go-fastly; go1.25.7)
url: https://api.fastly.com/observability/service/kKJb5bOFI47uHeBVluGfX1/logging/errors?filter%5Bendpoint%5D=Broken+Log&from=1775587245&to=1775587545
method: GET
response:
body: |
{"s":27,"t":1775587254943532,"o":"logging_error","r":"","m":"request failed","e":"Broken Log","details":"{\"level\":\"error\",\"error\":\"non-temporary request err: Get \\\"https://my-broken.logging.org/.well-known/fastly/logging/challenge\\\": lookup my-broken.logging.org. on 127.0.0.1:53: no such host\",\"name\":\"prebatch http challenge check\"}"}
{"s":18,"t":1775587256981715,"o":"logging_error","r":"","m":"request failed","e":"Broken Log","details":"{\"level\":\"error\",\"error\":\"non-temporary request err: Get \\\"https://my-broken.logging.org/.well-known/fastly/logging/challenge\\\": lookup my-broken.logging.org. on 127.0.0.1:53: no such host\",\"name\":\"prebatch http challenge check\"}"}
{"s":28,"t":1775587292616056,"o":"logging_error","r":"","m":"request failed","e":"Broken Log","details":"{\"level\":\"error\",\"error\":\"non-temporary request err: Get \\\"https://my-broken.logging.org/.well-known/fastly/logging/challenge\\\": lookup my-broken.logging.org. on 127.0.0.1:53: no such host\",\"name\":\"prebatch http challenge check\"}"}
{"s":19,"t":1775587315723327,"o":"logging_error","r":"","m":"request failed","e":"Broken Log","details":"{\"level\":\"error\",\"error\":\"non-temporary request err: Get \\\"https://my-broken.logging.org/.well-known/fastly/logging/challenge\\\": lookup my-broken.logging.org. on 127.0.0.1:53: no such host\",\"name\":\"prebatch http challenge check\"}"}
{"s":18,"t":1775587343917296,"o":"logging_error","r":"","m":"request failed","e":"Broken Log","details":"{\"level\":\"error\",\"error\":\"non-temporary request err: Get \\\"https://my-broken.logging.org/.well-known/fastly/logging/challenge\\\": lookup my-broken.logging.org. on 127.0.0.1:53: no such host\",\"name\":\"prebatch http challenge check\"}"}
{"s":18,"t":1775587383921446,"o":"logging_error","r":"","m":"request failed","e":"Broken Log","details":"{\"level\":\"error\",\"error\":\"non-temporary request err: Get \\\"https://my-broken.logging.org/.well-known/fastly/logging/challenge\\\": lookup my-broken.logging.org. on 127.0.0.1:53: no such host\",\"name\":\"prebatch http challenge check\"}"}
{"s":16,"t":1775587403717981,"o":"logging_error","r":"","m":"request failed","e":"Broken Log","details":"{\"level\":\"error\",\"error\":\"non-temporary request err: Get \\\"https://my-broken.logging.org/.well-known/fastly/logging/challenge\\\": lookup my-broken.logging.org. on 127.0.0.1:53: no such host\",\"name\":\"prebatch http challenge check\"}"}
{"s":18,"t":1775587437645880,"o":"logging_error","r":"","m":"request failed","e":"Broken Log","details":"{\"level\":\"error\",\"error\":\"non-temporary request err: Get \\\"https://my-broken.logging.org/.well-known/fastly/logging/challenge\\\": lookup my-broken.logging.org. on 127.0.0.1:53: no such host\",\"name\":\"prebatch http challenge check\"}"}
{"s":15,"t":1775587465219462,"o":"logging_error","r":"","m":"request failed","e":"Broken Log","details":"{\"level\":\"error\",\"error\":\"non-temporary request err: Get \\\"https://my-broken.logging.org/.well-known/fastly/logging/challenge\\\": lookup my-broken.logging.org. on 127.0.0.1:53: no such host\",\"name\":\"prebatch http challenge check\"}"}
{"s":15,"t":1775587529106194,"o":"logging_error","r":"","m":"request failed","e":"Broken Log","details":"{\"level\":\"error\",\"error\":\"non-temporary request err: Get \\\"https://my-broken.logging.org/.well-known/fastly/logging/challenge\\\": lookup my-broken.logging.org. on 127.0.0.1:53: no such host\",\"name\":\"prebatch http challenge check\"}"}
headers:
Accept-Ranges:
- bytes
Cache-Control:
- no-store
Date:
- Tue, 07 Apr 2026 18:56:02 GMT
Link:
- </observability/service/kKJb5bOFI47uHeBVluGfX1/logging/errors%3Ffrom=1775587250>;
rel="next", </observability/service/kKJb5bOFI47uHeBVluGfX1/logging/errors%3Ffrom=1775587230>;
rel="prev"
Pragma:
- no-cache
Server:
- fastly
Strict-Transport-Security:
- max-age=31536000
Vary:
- Fastly-Key,Accept-Encoding,Accept
Via:
- 1.1 varnish, 1.1 varnish
X-Cache:
- MISS, MISS
X-Cache-Hits:
- 0, 0
X-Served-By:
- cache-chi-klot8100057-CHI, cache-ewr-kewr1740068-EWR
X-Timer:
- S1775588162.232007,VS0,VE91
status: 200 OK
code: 200
duration: ""
77 changes: 77 additions & 0 deletions fastly/observability_endpoint_error_stream.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package fastly

import (
"bufio"
"context"
"encoding/json"
"strconv"
"strings"
)

type LoggingEndpointErrorsInput struct {
// From is a unix-formatted timestamp to start the log stream from. Required for the initial request only.
// If not used in conjunction with the `to` parameter, the range of the query will be a 10-second bucket.
From *uint64
// To is a unix-formatted timestamp to end the log stream. The maximum range between `from` and `to` is 1 hour;
// requests exceeding this limit will return an error.
To *uint64
// Filter is a comma-separated list of logging endpoint names to filter the error stream.
Filter []string
// ServiceID is an alphanumeric string identifying the service (required).
ServiceID string
}

type LoggingEndpointErrorsResponse struct {
Errors []LoggingEndpointError
}

type LoggingEndpointError struct {
SequenceNumber uint64 `json:"s"`
Timestamp uint64 `json:"t"`
Stream string `json:"o"`
RequestID string `json:"r"`
Message string `json:"m"`
Endpoint string `json:"e"`
Comment thread
kpfleming marked this conversation as resolved.
Outdated
Details string `json:"details"`
}

func (c *Client) GetLoggingEndpointErrors(ctx context.Context, i *LoggingEndpointErrorsInput) (*LoggingEndpointErrorsResponse, error) {
if i.ServiceID == "" {
return nil, ErrMissingServiceID
}

path := ToSafeURL("observability", "service", i.ServiceID, "logging", "errors")
requestOptions := CreateRequestOptions()

if i.From != nil {
requestOptions.Params["from"] = strconv.FormatUint(*i.From, 10)
}
if i.To != nil {
requestOptions.Params["to"] = strconv.FormatUint(*i.To, 10)
}
if len(i.Filter) > 0 {
requestOptions.Params["filter[endpoint]"] = strings.Join(i.Filter, ",")
}

resp, err := c.Get(ctx, path, requestOptions)
if err != nil {
return nil, err
}
defer resp.Body.Close()

var result LoggingEndpointErrorsResponse
scanner := bufio.NewScanner(resp.Body)
for scanner.Scan() {
var errorLog LoggingEndpointError
if err := json.Unmarshal(scanner.Bytes(), &errorLog); err != nil {
return nil, err
}
result.Errors = append(result.Errors, errorLog)
}

if err := scanner.Err(); err != nil {
return nil, err
}

return &result, nil
}
68 changes: 68 additions & 0 deletions fastly/observability_endpoint_error_stream_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package fastly

import (
"context"
"errors"
"testing"
)

func TestClient_GetLoggingEndpointErrors_validation(t *testing.T) {
var err error
_, err = TestClient.GetLoggingEndpointErrors(context.TODO(), &LoggingEndpointErrorsInput{
ServiceID: "",
})
if !errors.Is(err, ErrMissingServiceID) {
t.Errorf("bad error: %s", err)
}
}

func TestClient_GetLoggingEndpointErrors(t *testing.T) {
t.Parallel()

var err error
var result *LoggingEndpointErrorsResponse

// Get logging endpoint errors
Record(t, "observability_endpoint_error_stream/get", func(c *Client) {
result, err = c.GetLoggingEndpointErrors(context.TODO(), &LoggingEndpointErrorsInput{
ServiceID: TestDeliveryServiceID,
// Timestamps will need to be updated here if you wish to record the API response
// body. Streamed errors are only maintained for a given period of time.
From: ToPointer(uint64(1775587245)),
To: ToPointer(uint64(1775587545)),
})
})
if err != nil {
t.Fatal(err)
}

if result == nil {
t.Fatal("expected non-nil result")
}
}

func TestClient_GetLoggingEndpointErrors_with_filters(t *testing.T) {
t.Parallel()

var err error
var result *LoggingEndpointErrorsResponse

// Get logging endpoint errors with filters
Record(t, "observability_endpoint_error_stream/get_with_filters", func(c *Client) {
result, err = c.GetLoggingEndpointErrors(context.TODO(), &LoggingEndpointErrorsInput{
ServiceID: TestDeliveryServiceID,
// Timestamps will need to be updated here if you wish to record the API response
// body. Streamed errors are only maintained for a given period of time.
From: ToPointer(uint64(1775587245)),
To: ToPointer(uint64(1775587545)),
Filter: []string{"Broken Log"},
})
})
if err != nil {
t.Fatal(err)
}

if result == nil {
t.Fatal("expected non-nil result")
}
}
Loading