Files
telemt-api/internal/aggregate/fetch.go
T
Denozordec 04c257a84e
Publish telemt-api gateway Docker image / test (push) Successful in 25s
Publish telemt-api gateway Docker image / build-and-push (push) Successful in 1m19s
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.
2026-03-30 10:02:16 +07:00

133 lines
4.0 KiB
Go

package aggregate
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"github.com/telemt/telemt-api/internal/config"
)
const statsUsersPath = "stats/users"
// 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 {
users, meta := FetchTelemtGET[[]UserInfo](ctx, client, parsed, alias, statsUsersPath)
fr := ServerFetchResult{
Alias: alias,
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
}
func joinPathPrefix(base *url.URL, pathPrefix, rest string) *url.URL {
rel := strings.Trim(pathPrefix, "/")
if rest != "" {
if rel != "" {
rel = rel + "/" + rest
} else {
rel = rest
}
}
var parts []string
for _, seg := range strings.Split(rel, "/") {
if seg != "" {
parts = append(parts, seg)
}
}
if len(parts) == 0 {
out := *base
return &out
}
return base.JoinPath(parts...)
}
func truncate(s string, n int) string {
if len(s) <= n {
return s
}
return s[:n] + "…"
}