Add aggregate configuration support and update documentation
- Introduced AggregateConfig to manage aggregation settings in the gateway configuration. - Added validation for reserved alias 'agg' and included tests for aggregate alias handling. - Updated config.example.yaml to demonstrate aggregate configuration options. - Enhanced README.md to include information about the new aggregation endpoint and its usage. - Modified gateway.go to integrate the new aggregate handler for processing aggregation requests.
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
package aggregate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/telemt/telemt-api/internal/config"
|
||||
)
|
||||
|
||||
const pathPrefix = "/api/agg"
|
||||
|
||||
// Handler serves GET /api/agg/* aggregate endpoints.
|
||||
type Handler struct {
|
||||
Parsed *config.Parsed
|
||||
Client *http.Client
|
||||
}
|
||||
|
||||
// NewHandler builds an aggregate handler; client must use a non-nil Transport (e.g. gateway shared transport).
|
||||
func NewHandler(p *config.Parsed, client *http.Client) *Handler {
|
||||
return &Handler{Parsed: p, Client: client}
|
||||
}
|
||||
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
_ = json.NewEncoder(w).Encode(errEnvelope("method_not_allowed", "only GET is allowed"))
|
||||
return
|
||||
}
|
||||
sub := strings.TrimPrefix(r.URL.Path, pathPrefix)
|
||||
sub = strings.TrimPrefix(sub, "/")
|
||||
switch sub {
|
||||
case "summary":
|
||||
h.handleSummary(w, r)
|
||||
case "traffic":
|
||||
h.handleTraffic(w, r)
|
||||
case "unique-ips":
|
||||
h.handleUniqueIPs(w, r)
|
||||
case "users":
|
||||
h.handleUsers(w, r)
|
||||
default:
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
_ = json.NewEncoder(w).Encode(errEnvelope("not_found", "unknown aggregate path"))
|
||||
}
|
||||
}
|
||||
|
||||
func errEnvelope(code, msg string) map[string]any {
|
||||
return map[string]any{
|
||||
"ok": false,
|
||||
"error": map[string]string{
|
||||
"code": code,
|
||||
"message": msg,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) resolveAliases(r *http.Request) ([]string, error) {
|
||||
q := r.URL.Query().Get("aliases")
|
||||
if strings.TrimSpace(q) != "" {
|
||||
var out []string
|
||||
for _, p := range strings.Split(q, ",") {
|
||||
a := strings.TrimSpace(p)
|
||||
if a == "" {
|
||||
continue
|
||||
}
|
||||
if h.Parsed.ByAlias[a] == nil {
|
||||
return nil, &resolveError{msg: "unknown alias: " + a}
|
||||
}
|
||||
out = append(out, a)
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil, &resolveError{msg: "aliases query produced empty list"}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
cfg := h.Parsed.Config.Aggregate
|
||||
if cfg != nil && len(cfg.IncludeAliases) > 0 {
|
||||
for _, a := range cfg.IncludeAliases {
|
||||
a = strings.TrimSpace(a)
|
||||
if a == "" {
|
||||
continue
|
||||
}
|
||||
if h.Parsed.ByAlias[a] == nil {
|
||||
return nil, &resolveError{msg: "aggregate.include_aliases: unknown alias: " + a}
|
||||
}
|
||||
}
|
||||
out := make([]string, 0, len(cfg.IncludeAliases))
|
||||
for _, a := range cfg.IncludeAliases {
|
||||
a = strings.TrimSpace(a)
|
||||
if a == "" {
|
||||
continue
|
||||
}
|
||||
out = append(out, a)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
out := make([]string, 0, len(h.Parsed.Config.Servers))
|
||||
for i := range h.Parsed.Config.Servers {
|
||||
out = append(out, h.Parsed.Config.Servers[i].Alias)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
type resolveError struct {
|
||||
msg string
|
||||
}
|
||||
|
||||
func (e *resolveError) Error() string { return e.msg }
|
||||
|
||||
func (h *Handler) handleSummary(w http.ResponseWriter, r *http.Request) {
|
||||
aliases, err := h.resolveAliases(r)
|
||||
if err != nil {
|
||||
writeBadRequest(w, err)
|
||||
return
|
||||
}
|
||||
topN := 10
|
||||
if v := r.URL.Query().Get("top_n"); v != "" {
|
||||
if n, err := strconv.Atoi(v); err == nil && n > 0 {
|
||||
topN = n
|
||||
}
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 60*time.Second)
|
||||
defer cancel()
|
||||
results := FetchStatsUsers(ctx, h.Client, h.Parsed, aliases)
|
||||
data := BuildSummary(results, topN)
|
||||
writeOK(w, data)
|
||||
}
|
||||
|
||||
func (h *Handler) handleTraffic(w http.ResponseWriter, r *http.Request) {
|
||||
aliases, err := h.resolveAliases(r)
|
||||
if err != nil {
|
||||
writeBadRequest(w, err)
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 60*time.Second)
|
||||
defer cancel()
|
||||
results := FetchStatsUsers(ctx, h.Client, h.Parsed, aliases)
|
||||
data := BuildTraffic(results)
|
||||
writeOK(w, data)
|
||||
}
|
||||
|
||||
func (h *Handler) handleUniqueIPs(w http.ResponseWriter, r *http.Request) {
|
||||
aliases, err := h.resolveAliases(r)
|
||||
if err != nil {
|
||||
writeBadRequest(w, err)
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 60*time.Second)
|
||||
defer cancel()
|
||||
results := FetchStatsUsers(ctx, h.Client, h.Parsed, aliases)
|
||||
data := BuildUniqueIPs(results)
|
||||
writeOK(w, data)
|
||||
}
|
||||
|
||||
func (h *Handler) handleUsers(w http.ResponseWriter, r *http.Request) {
|
||||
aliases, err := h.resolveAliases(r)
|
||||
if err != nil {
|
||||
writeBadRequest(w, err)
|
||||
return
|
||||
}
|
||||
includeLinks := strings.EqualFold(r.URL.Query().Get("include_links"), "true")
|
||||
minOct := uint64(0)
|
||||
if v := r.URL.Query().Get("min_total_octets"); v != "" {
|
||||
if n, err := strconv.ParseUint(v, 10, 64); err == nil {
|
||||
minOct = n
|
||||
}
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 60*time.Second)
|
||||
defer cancel()
|
||||
results := FetchStatsUsers(ctx, h.Client, h.Parsed, aliases)
|
||||
data := BuildUsers(results, includeLinks, minOct)
|
||||
writeOK(w, data)
|
||||
}
|
||||
|
||||
func writeBadRequest(w http.ResponseWriter, err error) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_ = json.NewEncoder(w).Encode(errEnvelope("bad_request", err.Error()))
|
||||
}
|
||||
|
||||
func writeOK(w http.ResponseWriter, data any) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"ok": true, "data": data})
|
||||
}
|
||||
Reference in New Issue
Block a user