mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 15:51:26 +00:00
* refactor(db): port operations, settings, audit, activity, and misc service metadata to sqlc Covers the cross-cutting surface: operations board reads, app settings, notification producers, activity/audit inserts, builds upsert/cancel, git clone credential resolution, invites, environments, registry auth, project/service variables, HA resolver, boot sweeps, and admin user ops. Dynamic filtered lists and Better Auth table probes stay raw by design. Authored By: TDvorak <[email protected]> * refactor(db): port apwhy gateway handlers and proxy data plane to sqlc Final tranche — every static call site now uses generated queries. Raw SQL remains only where the statement is assembled at runtime (filtered lists, sparse updates, admin count closure) or probes Better Auth's auth_users table, which lives outside the local schema. Authored By: TDvorak <[email protected]>
672 lines
18 KiB
Go
672 lines
18 KiB
Go
package api
|
|
|
|
import (
|
|
"containr/internal/database"
|
|
"containr/internal/database/sqlcdb"
|
|
"context"
|
|
"crypto/rand"
|
|
"database/sql"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/go-playground/validator/v10"
|
|
"github.com/google/uuid"
|
|
"github.com/lib/pq"
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
// Default quota limits applied when the request omits them.
|
|
const (
|
|
DefaultRPMLimit = 60
|
|
DefaultMonthlyQuota = 1000
|
|
)
|
|
|
|
// Validator instance for request validation
|
|
var validate = validator.New()
|
|
|
|
// APIError represents a structured API error response
|
|
type APIError struct {
|
|
Code string `json:"code"`
|
|
Message string `json:"message"`
|
|
Details interface{} `json:"details,omitempty"`
|
|
}
|
|
|
|
// APIResponse represents a standardized API response
|
|
type APIResponse struct {
|
|
Success bool `json:"success"`
|
|
Data interface{} `json:"data,omitempty"`
|
|
Error *APIError `json:"error,omitempty"`
|
|
Meta *Meta `json:"meta,omitempty"`
|
|
}
|
|
|
|
// Meta contains pagination and metadata
|
|
type Meta struct {
|
|
Page int `json:"page,omitempty"`
|
|
PerPage int `json:"per_page,omitempty"`
|
|
Total int `json:"total,omitempty"`
|
|
TotalPages int `json:"total_pages,omitempty"`
|
|
}
|
|
|
|
// ServiceRequest represents the request payload for creating/updating services
|
|
type ServiceRequest struct {
|
|
Name string `json:"name" binding:"required,max=100" validate:"required,min=1,max=100"`
|
|
UpstreamURL string `json:"upstreamUrl" binding:"required,max=500" validate:"required,url,max=500"`
|
|
RoutePrefix string `json:"routePrefix" binding:"required,max=200" validate:"required,min=1,max=200"`
|
|
Enabled *bool `json:"enabled,omitempty"`
|
|
RPMLimit *int `json:"rpmLimit,omitempty" validate:"omitempty,min=1,max=10000"`
|
|
MonthlyQuota *int `json:"monthlyQuota,omitempty" validate:"omitempty,min=1,max=10000000"`
|
|
}
|
|
|
|
// APIKeyRequest represents the request payload for creating/updating API keys
|
|
type APIKeyRequest struct {
|
|
Name string `json:"name" binding:"required,max=100" validate:"required,min=1,max=100"`
|
|
Plan string `json:"plan" validate:"omitempty,oneof=free pro business enterprise"`
|
|
Enabled *bool `json:"enabled,omitempty"`
|
|
RPMLimit *int `json:"rpmLimit,omitempty" validate:"omitempty,min=1,max=10000"`
|
|
MonthlyQuota *int `json:"monthlyQuota,omitempty" validate:"omitempty,min=1,max=10000000"`
|
|
}
|
|
|
|
// generateAPIKey generates a cryptographically secure API key
|
|
func generateAPIKey() (string, error) {
|
|
bytes := make([]byte, 32)
|
|
if _, err := rand.Read(bytes); err != nil {
|
|
return "", fmt.Errorf("failed to generate random bytes: %w", err)
|
|
}
|
|
return "ap_" + hex.EncodeToString(bytes), nil
|
|
}
|
|
|
|
// hashAPIKey creates a bcrypt hash for the API key
|
|
func hashAPIKey(key string) (string, error) {
|
|
hash, err := bcrypt.GenerateFromPassword([]byte(key), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to hash API key: %w", err)
|
|
}
|
|
return string(hash), nil
|
|
}
|
|
|
|
// validateAPIKeyPlan validates and returns default values for API key plans
|
|
func validateAPIKeyPlan(plan string) (string, int, int, error) {
|
|
if plan == "" {
|
|
plan = "free"
|
|
}
|
|
|
|
switch plan {
|
|
case "free":
|
|
return plan, 60, 1000, nil
|
|
case "pro":
|
|
return plan, 600, 50000, nil
|
|
case "business":
|
|
return plan, 3000, 300000, nil
|
|
case "enterprise":
|
|
return plan, 10000, 10000000, nil
|
|
default:
|
|
return "", 0, 0, fmt.Errorf("invalid plan: %s", plan)
|
|
}
|
|
}
|
|
|
|
// sendJSONResponse sends a standardized JSON response
|
|
func sendJSONResponse(c *gin.Context, statusCode int, response APIResponse) {
|
|
c.JSON(statusCode, response)
|
|
}
|
|
|
|
// sendErrorResponse sends a standardized error response
|
|
func sendErrorResponse(c *gin.Context, statusCode int, code, message string, details interface{}) {
|
|
response := APIResponse{
|
|
Success: false,
|
|
Error: &APIError{
|
|
Code: code,
|
|
Message: message,
|
|
Details: details,
|
|
},
|
|
}
|
|
sendJSONResponse(c, statusCode, response)
|
|
}
|
|
|
|
// sendSuccessResponse sends a standardized success response
|
|
func sendSuccessResponse(c *gin.Context, statusCode int, data interface{}) {
|
|
response := APIResponse{
|
|
Success: true,
|
|
Data: data,
|
|
}
|
|
sendJSONResponse(c, statusCode, response)
|
|
}
|
|
|
|
// validateRequest validates the request payload using the validator
|
|
func validateRequest(c *gin.Context, req interface{}) error {
|
|
if err := c.ShouldBindJSON(req); err != nil {
|
|
return fmt.Errorf("invalid request body: %w", err)
|
|
}
|
|
|
|
if err := validate.Struct(req); err != nil {
|
|
return fmt.Errorf("validation failed: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// isUniqueViolation reports whether err is a Postgres unique-constraint violation.
|
|
func isUniqueViolation(err error) bool {
|
|
var pqErr *pq.Error
|
|
return errors.As(err, &pqErr) && pqErr.Code == "23505"
|
|
}
|
|
|
|
// handleAPwhyServiceValidate probes the upstream health path and records the
|
|
// result on the service row. Ported from the standalone APwhy server.
|
|
func handleAPwhyServiceValidate(c *gin.Context) {
|
|
serviceID := c.Param("id")
|
|
db := c.MustGet("db").(*database.DB)
|
|
ctx := c.Request.Context()
|
|
|
|
svcUUID, parseErr := uuid.Parse(serviceID)
|
|
var svc sqlcdb.GetAPServiceForValidateRow
|
|
var err error
|
|
if parseErr == nil {
|
|
svc, err = sqlcdb.New(db.DB).GetAPServiceForValidate(ctx, svcUUID)
|
|
}
|
|
if parseErr != nil || err != nil {
|
|
sendErrorResponse(c, http.StatusNotFound, "SERVICE_NOT_FOUND", "Service not found", nil)
|
|
return
|
|
}
|
|
upstreamURL, healthPath := svc.UpstreamUrl, svc.HealthPath
|
|
authHeader, authValue := svc.UpstreamAuthHeader, svc.UpstreamAuthValue
|
|
|
|
timeout := 8000
|
|
if svc.RequestTimeoutMs.Valid && svc.RequestTimeoutMs.Int32 > 0 {
|
|
timeout = int(svc.RequestTimeoutMs.Int32)
|
|
}
|
|
target := strings.TrimRight(upstreamURL, "/") + "/" + strings.TrimLeft(healthPath, "/")
|
|
|
|
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, target, nil)
|
|
if authHeader.Valid && authHeader.String != "" {
|
|
req.Header.Set(authHeader.String, authValue.String)
|
|
}
|
|
|
|
client := &http.Client{Timeout: time.Duration(timeout) * time.Millisecond}
|
|
res, probeErr := client.Do(req)
|
|
|
|
status := http.StatusBadGateway
|
|
message := ""
|
|
ok := false
|
|
if probeErr != nil {
|
|
message = probeErr.Error()
|
|
} else {
|
|
defer res.Body.Close()
|
|
status = res.StatusCode
|
|
message = fmt.Sprintf("HTTP %d", res.StatusCode)
|
|
ok = res.StatusCode >= 200 && res.StatusCode < 300
|
|
}
|
|
|
|
validationStatus := "failed"
|
|
if ok {
|
|
validationStatus = "healthy"
|
|
}
|
|
|
|
q := sqlcdb.New(db.DB)
|
|
_ = q.SetAPServiceValidation(ctx, sqlcdb.SetAPServiceValidationParams{
|
|
LastValidationStatus: sql.NullString{String: validationStatus, Valid: true},
|
|
LastValidationMessage: sql.NullString{String: message, Valid: true},
|
|
ID: svcUUID,
|
|
})
|
|
|
|
if !ok {
|
|
_ = q.InsertServiceIncident(ctx, sqlcdb.InsertServiceIncidentParams{
|
|
ServiceID: uuid.NullUUID{UUID: svcUUID, Valid: true},
|
|
Message: message,
|
|
HttpStatus: sql.NullInt32{Int32: int32(status), Valid: true},
|
|
})
|
|
}
|
|
|
|
sendSuccessResponse(c, http.StatusOK, map[string]interface{}{
|
|
"validation": map[string]interface{}{
|
|
"ok": ok,
|
|
"status": status,
|
|
"message": message,
|
|
},
|
|
})
|
|
}
|
|
|
|
// handleAPwhyServicesList returns a list of API services
|
|
func handleAPwhyServicesList(c *gin.Context) {
|
|
db := c.MustGet("db").(*database.DB)
|
|
ctx := context.Background()
|
|
|
|
// Query services from database
|
|
rows, err := sqlcdb.New(db.DB).ListAPServices(ctx)
|
|
if err != nil {
|
|
sendErrorResponse(c, http.StatusInternalServerError, "DATABASE_ERROR",
|
|
"Failed to query services", err.Error())
|
|
return
|
|
}
|
|
|
|
services := make([]map[string]interface{}, 0, len(rows))
|
|
for _, r := range rows {
|
|
services = append(services, map[string]interface{}{
|
|
"id": r.ID.String(),
|
|
"name": r.Name,
|
|
"slug": r.Slug,
|
|
"upstreamUrl": r.UpstreamUrl,
|
|
"routePrefix": r.RoutePrefix,
|
|
"enabled": r.Enabled != 0,
|
|
"createdAt": r.CreatedAt,
|
|
"updatedAt": r.UpdatedAt,
|
|
})
|
|
}
|
|
|
|
sendSuccessResponse(c, http.StatusOK, map[string]interface{}{
|
|
"services": services,
|
|
"count": len(services),
|
|
})
|
|
}
|
|
|
|
// handleAPwhyServicesCreate creates a new API service
|
|
func handleAPwhyServicesCreate(c *gin.Context) {
|
|
var req ServiceRequest
|
|
|
|
if err := validateRequest(c, &req); err != nil {
|
|
sendErrorResponse(c, http.StatusBadRequest, "VALIDATION_ERROR",
|
|
"Invalid request parameters", err.Error())
|
|
return
|
|
}
|
|
|
|
db := c.MustGet("db").(*database.DB)
|
|
ctx := context.Background()
|
|
|
|
slug := strings.ToLower(strings.ReplaceAll(req.Name, " ", "-"))
|
|
|
|
var rpmLimit, monthlyQuota int
|
|
if req.RPMLimit != nil {
|
|
rpmLimit = *req.RPMLimit
|
|
} else {
|
|
rpmLimit = DefaultRPMLimit
|
|
}
|
|
|
|
if req.MonthlyQuota != nil {
|
|
monthlyQuota = *req.MonthlyQuota
|
|
} else {
|
|
monthlyQuota = DefaultMonthlyQuota
|
|
}
|
|
|
|
// id defaults to gen_random_uuid(); enabled defaults to 1.
|
|
svcID, err := sqlcdb.New(db.DB).InsertAPService(ctx, sqlcdb.InsertAPServiceParams{
|
|
Name: req.Name,
|
|
Slug: slug,
|
|
UpstreamUrl: req.UpstreamURL,
|
|
RoutePrefix: req.RoutePrefix,
|
|
RpmLimit: sql.NullInt32{Int32: int32(rpmLimit), Valid: true},
|
|
MonthlyQuota: sql.NullInt32{Int32: int32(monthlyQuota), Valid: true},
|
|
})
|
|
|
|
if err != nil {
|
|
if isUniqueViolation(err) {
|
|
sendErrorResponse(c, http.StatusConflict, "CONFLICT",
|
|
"A service with this slug or route prefix already exists", err.Error())
|
|
return
|
|
}
|
|
sendErrorResponse(c, http.StatusInternalServerError, "DATABASE_ERROR",
|
|
"Failed to create service", err.Error())
|
|
return
|
|
}
|
|
|
|
serviceData := map[string]interface{}{
|
|
"id": svcID.String(),
|
|
"name": req.Name,
|
|
"slug": slug,
|
|
"upstreamUrl": req.UpstreamURL,
|
|
"routePrefix": req.RoutePrefix,
|
|
"enabled": true,
|
|
"rpmLimit": rpmLimit,
|
|
"monthlyQuota": monthlyQuota,
|
|
"createdAt": time.Now().UTC().Format(time.RFC3339),
|
|
}
|
|
|
|
sendSuccessResponse(c, http.StatusCreated, map[string]interface{}{
|
|
"service": serviceData,
|
|
"message": "Service created successfully",
|
|
})
|
|
}
|
|
|
|
// handleAPwhyServicesPatch updates an existing API service
|
|
func handleAPwhyServicesPatch(c *gin.Context) {
|
|
serviceID := c.Param("id")
|
|
|
|
var input struct {
|
|
Enabled *bool `json:"enabled"`
|
|
}
|
|
|
|
if err := c.ShouldBindJSON(&input); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{
|
|
"ok": false,
|
|
"error": "Invalid input: " + err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
db := c.MustGet("db").(*database.DB)
|
|
|
|
if input.Enabled != nil {
|
|
enabled := int32(0)
|
|
if *input.Enabled {
|
|
enabled = 1
|
|
}
|
|
svcUUID, _ := uuid.Parse(serviceID)
|
|
err := sqlcdb.New(db.DB).SetAPServiceEnabled(context.Background(), sqlcdb.SetAPServiceEnabledParams{
|
|
Enabled: enabled,
|
|
ID: svcUUID,
|
|
})
|
|
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"ok": false,
|
|
"error": "Failed to update service: " + err.Error(),
|
|
})
|
|
return
|
|
}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"ok": true,
|
|
"data": gin.H{"id": serviceID, "updated": true},
|
|
})
|
|
}
|
|
|
|
// handleAPwhyKeysList returns a list of API keys
|
|
func handleAPwhyKeysList(c *gin.Context) {
|
|
db := c.MustGet("db").(*database.DB)
|
|
|
|
rows, err := sqlcdb.New(db.DB).ListAPKeys(context.Background())
|
|
if err != nil {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"ok": true,
|
|
"data": []interface{}{},
|
|
})
|
|
return
|
|
}
|
|
|
|
keys := make([]map[string]interface{}, 0, len(rows))
|
|
for _, r := range rows {
|
|
key := map[string]interface{}{
|
|
"id": r.ID.String(),
|
|
"name": r.Name,
|
|
"keyPrefix": r.KeyPrefix,
|
|
"plan": r.Plan,
|
|
"enabled": r.Enabled != 0,
|
|
"createdAt": r.CreatedAt,
|
|
"updatedAt": r.UpdatedAt,
|
|
}
|
|
|
|
if r.RpmLimit.Valid {
|
|
key["rpmLimit"] = int64(r.RpmLimit.Int32)
|
|
}
|
|
if r.MonthlyQuota.Valid {
|
|
key["monthlyQuota"] = int64(r.MonthlyQuota.Int32)
|
|
}
|
|
|
|
keys = append(keys, key)
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"ok": true,
|
|
"data": keys,
|
|
})
|
|
}
|
|
|
|
// handleAPwhyKeysCreate creates a new API key
|
|
func handleAPwhyKeysCreate(c *gin.Context) {
|
|
var input struct {
|
|
Name string `json:"name" binding:"required"`
|
|
Plan string `json:"plan"`
|
|
}
|
|
|
|
if err := c.ShouldBindJSON(&input); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{
|
|
"ok": false,
|
|
"error": "Invalid input: " + err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
if input.Plan == "" {
|
|
input.Plan = "free"
|
|
}
|
|
|
|
db := c.MustGet("db").(*database.DB)
|
|
|
|
// Generate API key and hash
|
|
apiKey, err := generateAPIKey()
|
|
if err != nil {
|
|
sendErrorResponse(c, http.StatusInternalServerError, "GENERATION_ERROR",
|
|
"Failed to generate API key", err.Error())
|
|
return
|
|
}
|
|
|
|
keyHash, err := hashAPIKey(apiKey)
|
|
if err != nil {
|
|
sendErrorResponse(c, http.StatusInternalServerError, "HASH_ERROR",
|
|
"Failed to hash API key", err.Error())
|
|
return
|
|
}
|
|
|
|
keyPrefix := apiKey[:8]
|
|
|
|
// Set default limits based on plan
|
|
plan, rpmLimit, monthlyQuota, err := validateAPIKeyPlan(input.Plan)
|
|
if err != nil {
|
|
sendErrorResponse(c, http.StatusBadRequest, "VALIDATION_ERROR",
|
|
"Invalid plan", err.Error())
|
|
return
|
|
}
|
|
|
|
// id defaults to gen_random_uuid(); enabled defaults to 1.
|
|
keyID, err := sqlcdb.New(db.DB).InsertAPKey(context.Background(), sqlcdb.InsertAPKeyParams{
|
|
Name: input.Name,
|
|
KeyHash: keyHash,
|
|
KeyPrefix: keyPrefix,
|
|
Plan: plan,
|
|
RpmLimit: sql.NullInt32{Int32: int32(rpmLimit), Valid: true},
|
|
MonthlyQuota: sql.NullInt32{Int32: int32(monthlyQuota), Valid: true},
|
|
})
|
|
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"ok": false,
|
|
"error": "Failed to create key: " + err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, gin.H{
|
|
"ok": true,
|
|
"data": gin.H{
|
|
"id": keyID.String(),
|
|
"name": input.Name,
|
|
"plan": plan,
|
|
"key": apiKey, // Only return the actual key once
|
|
"keyPrefix": keyPrefix,
|
|
"enabled": true,
|
|
"rpmLimit": rpmLimit,
|
|
"monthlyQuota": monthlyQuota,
|
|
},
|
|
})
|
|
}
|
|
|
|
// handleAPwhyKeysPatch updates an existing API key
|
|
func handleAPwhyKeysPatch(c *gin.Context) {
|
|
keyID := c.Param("id")
|
|
|
|
var input struct {
|
|
Enabled *bool `json:"enabled"`
|
|
}
|
|
|
|
if err := c.ShouldBindJSON(&input); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{
|
|
"ok": false,
|
|
"error": "Invalid input: " + err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
db := c.MustGet("db").(*database.DB)
|
|
|
|
if input.Enabled != nil {
|
|
enabled := int32(0)
|
|
if *input.Enabled {
|
|
enabled = 1
|
|
}
|
|
keyUUID, _ := uuid.Parse(keyID)
|
|
err := sqlcdb.New(db.DB).SetAPKeyEnabled(context.Background(), sqlcdb.SetAPKeyEnabledParams{
|
|
Enabled: enabled,
|
|
ID: keyUUID,
|
|
})
|
|
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"ok": false,
|
|
"error": "Failed to update key: " + err.Error(),
|
|
})
|
|
return
|
|
}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"ok": true,
|
|
"data": gin.H{"id": keyID, "updated": true},
|
|
})
|
|
}
|
|
|
|
// handleAPwhyAnalyticsOps returns operational analytics
|
|
func handleAPwhyAnalyticsOps(c *gin.Context) {
|
|
db := c.MustGet("db").(*database.DB)
|
|
|
|
// Get counts from database
|
|
q := sqlcdb.New(db.DB)
|
|
ctx := context.Background()
|
|
|
|
totalServices, err := q.CountAPServices(ctx)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"ok": false,
|
|
"error": "Failed to count services: " + err.Error(),
|
|
})
|
|
return
|
|
}
|
|
totalKeys, err := q.CountAPKeys(ctx)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"ok": false,
|
|
"error": "Failed to count api keys: " + err.Error(),
|
|
})
|
|
return
|
|
}
|
|
totalUsers, err := q.CountUsers(ctx)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"ok": false,
|
|
"error": "Failed to count users: " + err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
totalRequests, err := q.SumUsageCounters(ctx)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"ok": false,
|
|
"error": "Failed to aggregate request counters: " + err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
requestsToday, err := q.SumRequestMetricSince(ctx, "day")
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"ok": false,
|
|
"error": "Failed to aggregate today's requests: " + err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
requestsThisMonth, err := q.SumRequestMetricSince(ctx, "month")
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"ok": false,
|
|
"error": "Failed to aggregate monthly requests: " + err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"ok": true,
|
|
"data": gin.H{
|
|
"total_requests": totalRequests,
|
|
"total_services": totalServices,
|
|
"total_keys": totalKeys,
|
|
"total_users": totalUsers,
|
|
"requests_today": requestsToday,
|
|
"requests_this_month": requestsThisMonth,
|
|
},
|
|
})
|
|
}
|
|
|
|
// handleAPwhyAnalyticsTraffic returns traffic analytics
|
|
func handleAPwhyAnalyticsTraffic(c *gin.Context) {
|
|
db := c.MustGet("db").(*database.DB)
|
|
|
|
q := sqlcdb.New(db.DB)
|
|
ctx := context.Background()
|
|
|
|
topServices := make([]map[string]interface{}, 0)
|
|
if serviceRows, err := q.ListTopAPServices(ctx); err == nil {
|
|
for _, r := range serviceRows {
|
|
topServices = append(topServices, map[string]interface{}{
|
|
"service_id": r.ID.String(),
|
|
"name": r.Name,
|
|
"requests": r.TotalRequests,
|
|
})
|
|
}
|
|
}
|
|
|
|
requestsByDay := make([]map[string]interface{}, 0)
|
|
if trafficRows, err := q.ListRequestsByDay(ctx); err == nil {
|
|
for _, r := range trafficRows {
|
|
requestsByDay = append(requestsByDay, map[string]interface{}{
|
|
"day": r.DayBucket,
|
|
"requests": r.Total,
|
|
})
|
|
}
|
|
}
|
|
|
|
statusCodes := make([]map[string]interface{}, 0)
|
|
if statusRows, err := q.ListIncidentStatusCodes(ctx); err == nil {
|
|
for _, r := range statusRows {
|
|
statusCodes = append(statusCodes, map[string]interface{}{
|
|
"status_code": r.StatusCode,
|
|
"count": r.Total,
|
|
})
|
|
}
|
|
}
|
|
|
|
clientEvents := make([]map[string]interface{}, 0)
|
|
if eventRows, err := q.ListClientEventPaths(ctx); err == nil {
|
|
for _, r := range eventRows {
|
|
clientEvents = append(clientEvents, map[string]interface{}{
|
|
"path": r.Path,
|
|
"count": r.Total,
|
|
})
|
|
}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"ok": true,
|
|
"data": gin.H{
|
|
"top_services": topServices,
|
|
"requests_by_day": requestsByDay,
|
|
"status_codes": statusCodes,
|
|
"client_events": clientEvents,
|
|
},
|
|
})
|
|
}
|