Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
52 changes: 52 additions & 0 deletions fastly/fixtures/observability_endpoint_error_stream/get.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
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=1775668708&to=1775669008
method: GET
response:
body: |
{"sequence_number":71,"error_time_us":1775668766119058,"stream":"logging_error","message":"request failed","endpoint":"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\"}"}
{"sequence_number":68,"error_time_us":1775668798631058,"stream":"logging_error","message":"request failed","endpoint":"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\"}"}
{"sequence_number":67,"error_time_us":1775668825364809,"stream":"logging_error","message":"request failed","endpoint":"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\"}"}
{"sequence_number":78,"error_time_us":1775668842623139,"stream":"logging_error","message":"request failed","endpoint":"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\"}"}
{"sequence_number":80,"error_time_us":1775668886383537,"stream":"logging_error","message":"request failed","endpoint":"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\"}"}
{"sequence_number":71,"error_time_us":1775668948458569,"stream":"logging_error","message":"request failed","endpoint":"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\"}"}
{"sequence_number":62,"error_time_us":1775668979579772,"stream":"logging_error","message":"request failed","endpoint":"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:
- Wed, 08 Apr 2026 17:29:12 GMT
Link:
- </observability/service/kKJb5bOFI47uHeBVluGfX1/logging/errors%3Ffrom=1775668710>;
rel="next", </observability/service/kKJb5bOFI47uHeBVluGfX1/logging/errors%3Ffrom=1775668690>;
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-kigq8000105-CHI, cache-ewr-kewr1740054-EWR
X-Timer:
- S1775669353.882944,VS0,VE91
status: 200 OK
code: 200
duration: ""
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
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=1775668708&to=1775669008
method: GET
response:
body: |
{"sequence_number":71,"error_time_us":1775668766119058,"stream":"logging_error","message":"request failed","endpoint":"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\"}"}
{"sequence_number":68,"error_time_us":1775668798631058,"stream":"logging_error","message":"request failed","endpoint":"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\"}"}
{"sequence_number":67,"error_time_us":1775668825364809,"stream":"logging_error","message":"request failed","endpoint":"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\"}"}
{"sequence_number":78,"error_time_us":1775668842623139,"stream":"logging_error","message":"request failed","endpoint":"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\"}"}
{"sequence_number":80,"error_time_us":1775668886383537,"stream":"logging_error","message":"request failed","endpoint":"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\"}"}
{"sequence_number":71,"error_time_us":1775668948458569,"stream":"logging_error","message":"request failed","endpoint":"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\"}"}
{"sequence_number":62,"error_time_us":1775668979579772,"stream":"logging_error","message":"request failed","endpoint":"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:
- Wed, 08 Apr 2026 17:29:12 GMT
Link:
- </observability/service/kKJb5bOFI47uHeBVluGfX1/logging/errors%3Ffrom=1775668710>;
rel="next", </observability/service/kKJb5bOFI47uHeBVluGfX1/logging/errors%3Ffrom=1775668690>;
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-klot8100054-CHI, cache-ewr-kewr1740054-EWR
X-Timer:
- S1775669353.882944,VS0,VE63
status: 200 OK
code: 200
duration: ""
76 changes: 76 additions & 0 deletions fastly/observability_logging_endpoint_errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package fastly

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

type LoggingEndpointErrorsInput struct {
Comment thread
kpfleming marked this conversation as resolved.
// 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:"sequence_number"`
Timestamp uint64 `json:"error_time_us"`
Stream string `json:"stream"`
Message string `json:"message"`
Endpoint string `json:"endpoint"`
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_logging_endpoint_errors_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(1775668708)),
To: ToPointer(uint64(1775669008)),
})
})
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(1775668708)),
To: ToPointer(uint64(1775669008)),
Filter: []string{"Broken Log"},
})
})
if err != nil {
t.Fatal(err)
}

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