Files
telemt-api/internal/aggregate/handlers.go
T
Denozordec 4a4f15bd0b
Publish telemt-api gateway Docker image / test (push) Successful in 27s
Publish telemt-api gateway Docker image / build-and-push (push) Successful in 1m26s
Update aggregate API to use binary megabytes and enhance documentation
- Changed API responses and internal calculations to use binary megabytes (MiB) instead of octets for traffic metrics.
- Updated relevant endpoints in AGGREGATE.md to reflect the new metric units.
- Modified handler and merge logic to accommodate the new data structure and ensure accurate traffic reporting.
- Enhanced tests to validate the changes in traffic calculations and summary data.
- Deprecated the use of total_octets in favor of total_megabytes for consistency across the API.
2026-03-30 01:04:00 +07:00

194 lines
5.1 KiB
Go

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")
var minOct uint64
if v := r.URL.Query().Get("min_total_megabytes"); v != "" {
if n, err := strconv.ParseFloat(v, 64); err == nil && n > 0 {
minOct = uint64(n * float64(mebibyte))
}
} else 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})
}