Add CORS support and response caching to aggregate endpoints
- Introduced CORS configuration options in config.example.yaml, allowing specification of allowed origins for cross-origin requests. - Enhanced the aggregate handler to support response caching with a configurable TTL, improving performance for repeated requests. - Updated the aggregate API to return a structured response indicating whether any upstream requests failed, enhancing error handling and response clarity. - Modified documentation in AGGREGATE.md and README.md to reflect the new CORS and caching features. - Added tests to validate the new functionality in the aggregate handler.
This commit is contained in:
+79
-95
@@ -15,105 +15,89 @@ import (
|
||||
|
||||
const statsUsersPath = "stats/users"
|
||||
|
||||
// FetchStatsUsers calls GET {base}{path_prefix}/stats/users for each alias.
|
||||
// UpstreamCallMeta describes one upstream Telemt GET outcome (before typed data).
|
||||
type UpstreamCallMeta struct {
|
||||
OK bool `json:"ok"`
|
||||
HTTPStatus int `json:"http_status,omitempty"`
|
||||
LatencyMs int64 `json:"latency_ms,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Revision string `json:"revision,omitempty"`
|
||||
}
|
||||
|
||||
type telemtEnvelope[T any] struct {
|
||||
OK bool `json:"ok"`
|
||||
Data T `json:"data"`
|
||||
Revision string `json:"revision"`
|
||||
}
|
||||
|
||||
// FetchTelemtGET performs GET {base}{path_prefix}/{relPath} and decodes Telemt success envelope into T.
|
||||
// On upstream/network errors, returns zero T and meta with OK=false; meta.Error is set.
|
||||
func FetchTelemtGET[T any](ctx context.Context, client *http.Client, parsed *config.Parsed, alias string, relPath string) (out T, meta UpstreamCallMeta) {
|
||||
var zero T
|
||||
srv := parsed.ByAlias[alias]
|
||||
if srv == nil {
|
||||
return zero, UpstreamCallMeta{OK: false, Error: "unknown alias"}
|
||||
}
|
||||
u, err := url.Parse(srv.BaseURL)
|
||||
if err != nil {
|
||||
return zero, UpstreamCallMeta{OK: false, Error: err.Error()}
|
||||
}
|
||||
target := joinPathPrefix(u, srv.PathPrefix, relPath)
|
||||
start := time.Now()
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, target.String(), nil)
|
||||
if err != nil {
|
||||
return zero, UpstreamCallMeta{OK: false, LatencyMs: time.Since(start).Milliseconds(), Error: err.Error()}
|
||||
}
|
||||
if auth := parsed.AuthByAlias[alias]; auth != "" {
|
||||
req.Header.Set("Authorization", auth)
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
latency := time.Since(start).Milliseconds()
|
||||
if err != nil {
|
||||
return zero, UpstreamCallMeta{OK: false, LatencyMs: latency, Error: err.Error()}
|
||||
}
|
||||
body, readErr := io.ReadAll(io.LimitReader(resp.Body, 64<<20))
|
||||
_ = resp.Body.Close()
|
||||
if readErr != nil {
|
||||
return zero, UpstreamCallMeta{OK: false, HTTPStatus: resp.StatusCode, LatencyMs: latency, Error: readErr.Error()}
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return zero, UpstreamCallMeta{
|
||||
OK: false, HTTPStatus: resp.StatusCode, LatencyMs: latency,
|
||||
Error: fmt.Sprintf("http %d: %s", resp.StatusCode, truncate(string(body), 200)),
|
||||
}
|
||||
}
|
||||
var env telemtEnvelope[json.RawMessage]
|
||||
if err := json.Unmarshal(body, &env); err != nil {
|
||||
return zero, UpstreamCallMeta{OK: false, HTTPStatus: resp.StatusCode, LatencyMs: latency, Error: "invalid json: " + err.Error()}
|
||||
}
|
||||
if !env.OK {
|
||||
return zero, UpstreamCallMeta{OK: false, HTTPStatus: resp.StatusCode, LatencyMs: latency, Error: "upstream ok=false"}
|
||||
}
|
||||
var data T
|
||||
if err := json.Unmarshal(env.Data, &data); err != nil {
|
||||
return zero, UpstreamCallMeta{OK: false, HTTPStatus: resp.StatusCode, LatencyMs: latency, Error: "decode data: " + err.Error()}
|
||||
}
|
||||
return data, UpstreamCallMeta{OK: true, HTTPStatus: resp.StatusCode, LatencyMs: latency, Revision: env.Revision}
|
||||
}
|
||||
|
||||
// FetchStatsUsers calls GET stats/users for each alias using FetchTelemtGET.
|
||||
func FetchStatsUsers(ctx context.Context, client *http.Client, parsed *config.Parsed, aliases []string) []ServerFetchResult {
|
||||
out := make([]ServerFetchResult, 0, len(aliases))
|
||||
for _, alias := range aliases {
|
||||
srv := parsed.ByAlias[alias]
|
||||
if srv == nil {
|
||||
out = append(out, ServerFetchResult{
|
||||
Alias: alias,
|
||||
OK: false,
|
||||
Error: "unknown alias",
|
||||
})
|
||||
continue
|
||||
}
|
||||
u, err := url.Parse(srv.BaseURL)
|
||||
if err != nil {
|
||||
out = append(out, ServerFetchResult{
|
||||
Alias: alias,
|
||||
OK: false,
|
||||
Error: err.Error(),
|
||||
})
|
||||
continue
|
||||
}
|
||||
target := joinPathPrefix(u, srv.PathPrefix, statsUsersPath)
|
||||
start := time.Now()
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, target.String(), nil)
|
||||
if err != nil {
|
||||
out = append(out, ServerFetchResult{
|
||||
Alias: alias,
|
||||
OK: false,
|
||||
LatencyMs: time.Since(start).Milliseconds(),
|
||||
Error: err.Error(),
|
||||
})
|
||||
continue
|
||||
}
|
||||
if auth := parsed.AuthByAlias[alias]; auth != "" {
|
||||
req.Header.Set("Authorization", auth)
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
latency := time.Since(start).Milliseconds()
|
||||
if err != nil {
|
||||
out = append(out, ServerFetchResult{
|
||||
Alias: alias,
|
||||
OK: false,
|
||||
LatencyMs: latency,
|
||||
Error: err.Error(),
|
||||
})
|
||||
continue
|
||||
}
|
||||
body, readErr := io.ReadAll(io.LimitReader(resp.Body, 64<<20))
|
||||
_ = resp.Body.Close()
|
||||
if readErr != nil {
|
||||
out = append(out, ServerFetchResult{
|
||||
Alias: alias,
|
||||
OK: false,
|
||||
HTTPStatus: resp.StatusCode,
|
||||
LatencyMs: latency,
|
||||
Error: readErr.Error(),
|
||||
})
|
||||
continue
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
out = append(out, ServerFetchResult{
|
||||
Alias: alias,
|
||||
OK: false,
|
||||
HTTPStatus: resp.StatusCode,
|
||||
LatencyMs: latency,
|
||||
Error: fmt.Sprintf("http %d: %s", resp.StatusCode, truncate(string(body), 200)),
|
||||
})
|
||||
continue
|
||||
}
|
||||
var env statsUsersEnvelope
|
||||
if err := json.Unmarshal(body, &env); err != nil {
|
||||
out = append(out, ServerFetchResult{
|
||||
Alias: alias,
|
||||
OK: false,
|
||||
HTTPStatus: resp.StatusCode,
|
||||
LatencyMs: latency,
|
||||
Error: "invalid json: " + err.Error(),
|
||||
})
|
||||
continue
|
||||
}
|
||||
if !env.OK {
|
||||
out = append(out, ServerFetchResult{
|
||||
Alias: alias,
|
||||
OK: false,
|
||||
HTTPStatus: resp.StatusCode,
|
||||
LatencyMs: latency,
|
||||
Error: "upstream ok=false",
|
||||
})
|
||||
continue
|
||||
}
|
||||
out = append(out, ServerFetchResult{
|
||||
users, meta := FetchTelemtGET[[]UserInfo](ctx, client, parsed, alias, statsUsersPath)
|
||||
fr := ServerFetchResult{
|
||||
Alias: alias,
|
||||
OK: true,
|
||||
HTTPStatus: resp.StatusCode,
|
||||
LatencyMs: latency,
|
||||
Revision: env.Revision,
|
||||
Users: env.Data,
|
||||
})
|
||||
OK: meta.OK,
|
||||
HTTPStatus: meta.HTTPStatus,
|
||||
LatencyMs: meta.LatencyMs,
|
||||
Error: meta.Error,
|
||||
Revision: meta.Revision,
|
||||
}
|
||||
if meta.OK {
|
||||
fr.Users = users
|
||||
}
|
||||
out = append(out, fr)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user