1654 lines
58 KiB
Go
1654 lines
58 KiB
Go
// Package handlers provides HTTP request handlers for the Bifrost HTTP transport.
|
|
// This file contains logging-related handlers for log search, stats, and management.
|
|
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/bytedance/sonic"
|
|
"github.com/fasthttp/router"
|
|
"github.com/maximhq/bifrost/core/schemas"
|
|
"github.com/maximhq/bifrost/framework/configstore/tables"
|
|
"github.com/maximhq/bifrost/framework/logstore"
|
|
"github.com/maximhq/bifrost/plugins/logging"
|
|
"github.com/maximhq/bifrost/transports/bifrost-http/lib"
|
|
"github.com/valyala/fasthttp"
|
|
"golang.org/x/sync/errgroup"
|
|
)
|
|
|
|
// LoggingHandler manages HTTP requests for logging operations
|
|
type LoggingHandler struct {
|
|
logManager logging.LogManager
|
|
redactedKeysManager RedactedKeysManager
|
|
config *lib.Config
|
|
}
|
|
|
|
// Keep session log page size in one place so the session sheet limit is easy to tune later.
|
|
const sessionLogPageLimit = 500
|
|
|
|
func parseParentRequestIDFilter(ctx *fasthttp.RequestCtx) string {
|
|
if parentRequestID := string(ctx.QueryArgs().Peek("parent_request_id")); strings.TrimSpace(parentRequestID) != "" {
|
|
return parentRequestID
|
|
}
|
|
if sessionID := string(ctx.QueryArgs().Peek("session_id")); strings.TrimSpace(sessionID) != "" {
|
|
return sessionID
|
|
}
|
|
return ""
|
|
}
|
|
|
|
type RedactedKeysManager interface {
|
|
GetAllRedactedKeys(ctx context.Context, ids []string) []schemas.Key
|
|
GetAllRedactedVirtualKeys(ctx context.Context, ids []string) []tables.TableVirtualKey
|
|
GetAllRedactedRoutingRules(ctx context.Context, ids []string) []tables.TableRoutingRule
|
|
}
|
|
|
|
// NewLoggingHandler creates a new logging handler instance
|
|
func NewLoggingHandler(logManager logging.LogManager, redactedKeysManager RedactedKeysManager, config *lib.Config) *LoggingHandler {
|
|
return &LoggingHandler{
|
|
logManager: logManager,
|
|
redactedKeysManager: redactedKeysManager,
|
|
config: config,
|
|
}
|
|
}
|
|
|
|
func (h *LoggingHandler) shouldHideDeletedVirtualKeysInFilters() bool {
|
|
if h == nil || h.config == nil {
|
|
return false
|
|
}
|
|
return h.config.ClientConfig.HideDeletedVirtualKeysInFilters
|
|
}
|
|
|
|
// RegisterRoutes registers all logging-related routes
|
|
func (h *LoggingHandler) RegisterRoutes(r *router.Router, middlewares ...schemas.BifrostHTTPMiddleware) {
|
|
// LLM Log retrieval with filtering, search, and pagination
|
|
r.GET("/api/logs", lib.ChainMiddlewares(h.getLogs, middlewares...))
|
|
r.GET("/api/logs/sessions/{session_id}/summary", lib.ChainMiddlewares(h.getLogSessionSummaryByID, middlewares...))
|
|
r.GET("/api/logs/sessions/{session_id}", lib.ChainMiddlewares(h.getLogSessionByID, middlewares...))
|
|
r.GET("/api/logs/{id}", lib.ChainMiddlewares(h.getLogByID, middlewares...))
|
|
r.GET("/api/logs/stats", lib.ChainMiddlewares(h.getLogsStats, middlewares...))
|
|
r.GET("/api/logs/histogram", lib.ChainMiddlewares(h.getLogsHistogram, middlewares...))
|
|
r.GET("/api/logs/histogram/tokens", lib.ChainMiddlewares(h.getLogsTokenHistogram, middlewares...))
|
|
r.GET("/api/logs/histogram/cost", lib.ChainMiddlewares(h.getLogsCostHistogram, middlewares...))
|
|
r.GET("/api/logs/histogram/models", lib.ChainMiddlewares(h.getLogsModelHistogram, middlewares...))
|
|
r.GET("/api/logs/histogram/latency", lib.ChainMiddlewares(h.getLogsLatencyHistogram, middlewares...))
|
|
r.GET("/api/logs/histogram/cost/by-provider", lib.ChainMiddlewares(h.getLogsProviderCostHistogram, middlewares...))
|
|
r.GET("/api/logs/histogram/tokens/by-provider", lib.ChainMiddlewares(h.getLogsProviderTokenHistogram, middlewares...))
|
|
r.GET("/api/logs/histogram/latency/by-provider", lib.ChainMiddlewares(h.getLogsProviderLatencyHistogram, middlewares...))
|
|
r.GET("/api/logs/histogram/cost/by-dimension", lib.ChainMiddlewares(h.getLogsDimensionCostHistogram, middlewares...))
|
|
r.GET("/api/logs/histogram/tokens/by-dimension", lib.ChainMiddlewares(h.getLogsDimensionTokenHistogram, middlewares...))
|
|
r.GET("/api/logs/histogram/latency/by-dimension", lib.ChainMiddlewares(h.getLogsDimensionLatencyHistogram, middlewares...))
|
|
r.GET("/api/logs/dropped", lib.ChainMiddlewares(h.getDroppedRequests, middlewares...))
|
|
r.GET("/api/logs/filterdata", lib.ChainMiddlewares(h.getAvailableFilterData, middlewares...))
|
|
r.GET("/api/logs/rankings", lib.ChainMiddlewares(h.getModelRankings, middlewares...))
|
|
r.DELETE("/api/logs", lib.ChainMiddlewares(h.deleteLogs, middlewares...))
|
|
r.POST("/api/logs/recalculate-cost", lib.ChainMiddlewares(h.recalculateLogCosts, middlewares...))
|
|
|
|
// MCP Tool Log retrieval with filtering, search, and pagination
|
|
r.GET("/api/mcp-logs", lib.ChainMiddlewares(h.getMCPLogs, middlewares...))
|
|
r.GET("/api/mcp-logs/stats", lib.ChainMiddlewares(h.getMCPLogsStats, middlewares...))
|
|
r.GET("/api/mcp-logs/filterdata", lib.ChainMiddlewares(h.getMCPLogsFilterData, middlewares...))
|
|
r.GET("/api/mcp-logs/histogram", lib.ChainMiddlewares(h.getMCPHistogram, middlewares...))
|
|
r.GET("/api/mcp-logs/histogram/cost", lib.ChainMiddlewares(h.getMCPCostHistogram, middlewares...))
|
|
r.GET("/api/mcp-logs/histogram/top-tools", lib.ChainMiddlewares(h.getMCPTopTools, middlewares...))
|
|
r.DELETE("/api/mcp-logs", lib.ChainMiddlewares(h.deleteMCPLogs, middlewares...))
|
|
}
|
|
|
|
// getLogSessionByID handles GET /api/logs/sessions/{session_id} - Get logs in a single session.
|
|
func (h *LoggingHandler) getLogSessionByID(ctx *fasthttp.RequestCtx) {
|
|
rawSessionID, ok := ctx.UserValue("session_id").(string)
|
|
if !ok || strings.TrimSpace(rawSessionID) == "" {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "session_id is required")
|
|
return
|
|
}
|
|
|
|
pagination := &logstore.PaginationOptions{
|
|
Limit: sessionLogPageLimit,
|
|
Offset: 0,
|
|
SortBy: "timestamp",
|
|
Order: "asc",
|
|
}
|
|
if limit := string(ctx.QueryArgs().Peek("limit")); limit != "" {
|
|
i, err := strconv.Atoi(limit)
|
|
if err != nil {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "invalid limit")
|
|
return
|
|
}
|
|
if i <= 0 {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "limit must be greater than 0")
|
|
return
|
|
}
|
|
if i > sessionLogPageLimit {
|
|
SendError(ctx, fasthttp.StatusBadRequest, fmt.Sprintf("limit cannot exceed %d", sessionLogPageLimit))
|
|
return
|
|
}
|
|
pagination.Limit = i
|
|
}
|
|
if offset := string(ctx.QueryArgs().Peek("offset")); offset != "" {
|
|
i, err := strconv.Atoi(offset)
|
|
if err != nil {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "invalid offset")
|
|
return
|
|
}
|
|
if i < 0 {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "offset cannot be negative")
|
|
return
|
|
}
|
|
pagination.Offset = i
|
|
}
|
|
if order := string(ctx.QueryArgs().Peek("order")); order != "" {
|
|
if order != "asc" && order != "desc" {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "order must be 'asc' or 'desc'")
|
|
return
|
|
}
|
|
pagination.Order = order
|
|
}
|
|
|
|
result, err := h.logManager.GetSessionLogs(ctx, rawSessionID, pagination)
|
|
if err != nil {
|
|
logger.Error("failed to fetch session logs: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("Session fetch failed: %v", err))
|
|
return
|
|
}
|
|
|
|
selectedKeyIDs := make(map[string]struct{})
|
|
virtualKeyIDs := make(map[string]struct{})
|
|
routingRuleIDs := make(map[string]struct{})
|
|
for _, log := range result.Logs {
|
|
if log.SelectedKeyID != "" {
|
|
selectedKeyIDs[log.SelectedKeyID] = struct{}{}
|
|
}
|
|
if log.VirtualKeyID != nil && *log.VirtualKeyID != "" {
|
|
virtualKeyIDs[*log.VirtualKeyID] = struct{}{}
|
|
}
|
|
if log.RoutingRuleID != nil && *log.RoutingRuleID != "" {
|
|
routingRuleIDs[*log.RoutingRuleID] = struct{}{}
|
|
}
|
|
}
|
|
|
|
toSlice := func(m map[string]struct{}) []string {
|
|
if len(m) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]string, 0, len(m))
|
|
for id := range m {
|
|
out = append(out, id)
|
|
}
|
|
return out
|
|
}
|
|
|
|
redactedKeys := h.redactedKeysManager.GetAllRedactedKeys(ctx, toSlice(selectedKeyIDs))
|
|
redactedVirtualKeys := h.redactedKeysManager.GetAllRedactedVirtualKeys(ctx, toSlice(virtualKeyIDs))
|
|
redactedRoutingRules := h.redactedKeysManager.GetAllRedactedRoutingRules(ctx, toSlice(routingRuleIDs))
|
|
|
|
for i, log := range result.Logs {
|
|
if log.SelectedKeyID != "" && log.SelectedKeyName != "" {
|
|
result.Logs[i].SelectedKey = findRedactedKey(redactedKeys, log.SelectedKeyID, log.SelectedKeyName)
|
|
}
|
|
if log.VirtualKeyID != nil && log.VirtualKeyName != nil && *log.VirtualKeyID != "" && *log.VirtualKeyName != "" {
|
|
result.Logs[i].VirtualKey = findRedactedVirtualKey(redactedVirtualKeys, *log.VirtualKeyID, *log.VirtualKeyName)
|
|
}
|
|
if log.RoutingRuleID != nil && log.RoutingRuleName != nil && *log.RoutingRuleID != "" && *log.RoutingRuleName != "" {
|
|
result.Logs[i].RoutingRule = findRedactedRoutingRule(redactedRoutingRules, *log.RoutingRuleID, *log.RoutingRuleName)
|
|
}
|
|
}
|
|
|
|
SendJSON(ctx, result)
|
|
}
|
|
|
|
// getLogSessionSummaryByID handles GET /api/logs/sessions/{session_id}/summary - Get aggregate totals for a single session.
|
|
func (h *LoggingHandler) getLogSessionSummaryByID(ctx *fasthttp.RequestCtx) {
|
|
rawSessionID, ok := ctx.UserValue("session_id").(string)
|
|
if !ok || strings.TrimSpace(rawSessionID) == "" {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "session_id is required")
|
|
return
|
|
}
|
|
|
|
result, err := h.logManager.GetSessionSummary(ctx, rawSessionID)
|
|
if err != nil {
|
|
logger.Error("failed to fetch session summary: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("Session summary fetch failed: %v", err))
|
|
return
|
|
}
|
|
|
|
SendJSON(ctx, result)
|
|
}
|
|
|
|
// getLogs handles GET /api/logs - Get logs with filtering, search, and pagination via query parameters
|
|
func (h *LoggingHandler) getLogs(ctx *fasthttp.RequestCtx) {
|
|
// Parse query parameters into filters
|
|
filters := &logstore.SearchFilters{}
|
|
pagination := &logstore.PaginationOptions{}
|
|
|
|
// Extract filters from query parameters
|
|
if providers := string(ctx.QueryArgs().Peek("providers")); providers != "" {
|
|
filters.Providers = parseCommaSeparated(providers)
|
|
}
|
|
if models := string(ctx.QueryArgs().Peek("models")); models != "" {
|
|
filters.Models = parseCommaSeparated(models)
|
|
}
|
|
if aliases := string(ctx.QueryArgs().Peek("aliases")); aliases != "" {
|
|
filters.Aliases = parseCommaSeparated(aliases)
|
|
}
|
|
if statuses := string(ctx.QueryArgs().Peek("status")); statuses != "" {
|
|
filters.Status = parseCommaSeparated(statuses)
|
|
}
|
|
if objects := string(ctx.QueryArgs().Peek("objects")); objects != "" {
|
|
filters.Objects = parseCommaSeparated(objects)
|
|
}
|
|
if parentRequestID := parseParentRequestIDFilter(ctx); parentRequestID != "" {
|
|
filters.ParentRequestID = parentRequestID
|
|
}
|
|
if selectedKeyIDs := string(ctx.QueryArgs().Peek("selected_key_ids")); selectedKeyIDs != "" {
|
|
filters.SelectedKeyIDs = parseCommaSeparated(selectedKeyIDs)
|
|
}
|
|
if virtualKeyIDs := string(ctx.QueryArgs().Peek("virtual_key_ids")); virtualKeyIDs != "" {
|
|
filters.VirtualKeyIDs = parseCommaSeparated(virtualKeyIDs)
|
|
}
|
|
if routingRuleIDs := string(ctx.QueryArgs().Peek("routing_rule_ids")); routingRuleIDs != "" {
|
|
filters.RoutingRuleIDs = parseCommaSeparated(routingRuleIDs)
|
|
}
|
|
if teamIDs := string(ctx.QueryArgs().Peek("team_ids")); teamIDs != "" {
|
|
filters.TeamIDs = parseCommaSeparated(teamIDs)
|
|
}
|
|
if customerIDs := string(ctx.QueryArgs().Peek("customer_ids")); customerIDs != "" {
|
|
filters.CustomerIDs = parseCommaSeparated(customerIDs)
|
|
}
|
|
if userIDs := string(ctx.QueryArgs().Peek("user_ids")); userIDs != "" {
|
|
filters.UserIDs = parseCommaSeparated(userIDs)
|
|
}
|
|
if businessUnitIDs := string(ctx.QueryArgs().Peek("business_unit_ids")); businessUnitIDs != "" {
|
|
filters.BusinessUnitIDs = parseCommaSeparated(businessUnitIDs)
|
|
}
|
|
if routingEngines := string(ctx.QueryArgs().Peek("routing_engine_used")); routingEngines != "" {
|
|
filters.RoutingEngineUsed = parseCommaSeparated(routingEngines)
|
|
}
|
|
if startTime := string(ctx.QueryArgs().Peek("start_time")); startTime != "" {
|
|
if t, err := time.Parse(time.RFC3339, startTime); err == nil {
|
|
filters.StartTime = &t
|
|
}
|
|
}
|
|
if endTime := string(ctx.QueryArgs().Peek("end_time")); endTime != "" {
|
|
if t, err := time.Parse(time.RFC3339, endTime); err == nil {
|
|
filters.EndTime = &t
|
|
}
|
|
}
|
|
if minLatency := string(ctx.QueryArgs().Peek("min_latency")); minLatency != "" {
|
|
if f, err := strconv.ParseFloat(minLatency, 64); err == nil {
|
|
filters.MinLatency = &f
|
|
}
|
|
}
|
|
if maxLatency := string(ctx.QueryArgs().Peek("max_latency")); maxLatency != "" {
|
|
if val, err := strconv.ParseFloat(maxLatency, 64); err == nil {
|
|
filters.MaxLatency = &val
|
|
}
|
|
}
|
|
if minTokens := string(ctx.QueryArgs().Peek("min_tokens")); minTokens != "" {
|
|
if val, err := strconv.Atoi(minTokens); err == nil {
|
|
filters.MinTokens = &val
|
|
}
|
|
}
|
|
if maxTokens := string(ctx.QueryArgs().Peek("max_tokens")); maxTokens != "" {
|
|
if val, err := strconv.Atoi(maxTokens); err == nil {
|
|
filters.MaxTokens = &val
|
|
}
|
|
}
|
|
if cost := string(ctx.QueryArgs().Peek("min_cost")); cost != "" {
|
|
if val, err := strconv.ParseFloat(cost, 64); err == nil {
|
|
filters.MinCost = &val
|
|
}
|
|
}
|
|
if maxCost := string(ctx.QueryArgs().Peek("max_cost")); maxCost != "" {
|
|
if val, err := strconv.ParseFloat(maxCost, 64); err == nil {
|
|
filters.MaxCost = &val
|
|
}
|
|
}
|
|
if missingCost := string(ctx.QueryArgs().Peek("missing_cost_only")); missingCost != "" {
|
|
if val, err := strconv.ParseBool(missingCost); err == nil {
|
|
filters.MissingCostOnly = val
|
|
}
|
|
}
|
|
if contentSearch := string(ctx.QueryArgs().Peek("content_search")); contentSearch != "" {
|
|
filters.ContentSearch = contentSearch
|
|
}
|
|
parseMetadataFilters(ctx, filters)
|
|
|
|
// Extract pagination parameters
|
|
pagination.Limit = 50 // Default limit
|
|
if limit := string(ctx.QueryArgs().Peek("limit")); limit != "" {
|
|
if i, err := strconv.Atoi(limit); err == nil {
|
|
if i <= 0 {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "limit must be greater than 0")
|
|
return
|
|
}
|
|
if i > 1000 {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "limit cannot exceed 1000")
|
|
return
|
|
}
|
|
pagination.Limit = i
|
|
}
|
|
}
|
|
|
|
pagination.Offset = 0 // Default offset
|
|
if offset := string(ctx.QueryArgs().Peek("offset")); offset != "" {
|
|
if i, err := strconv.Atoi(offset); err == nil {
|
|
if i < 0 {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "offset cannot be negative")
|
|
return
|
|
}
|
|
pagination.Offset = i
|
|
}
|
|
}
|
|
|
|
// Sort parameters
|
|
pagination.SortBy = "timestamp" // Default sort field
|
|
if sortBy := string(ctx.QueryArgs().Peek("sort_by")); sortBy != "" {
|
|
if sortBy == "timestamp" || sortBy == "latency" || sortBy == "tokens" || sortBy == "cost" {
|
|
pagination.SortBy = sortBy
|
|
}
|
|
}
|
|
|
|
pagination.Order = "desc" // Default sort order
|
|
if order := string(ctx.QueryArgs().Peek("order")); order != "" {
|
|
if order == "asc" || order == "desc" {
|
|
pagination.Order = order
|
|
}
|
|
}
|
|
|
|
result, err := h.logManager.Search(ctx, filters, pagination)
|
|
if err != nil {
|
|
logger.Error("failed to search logs: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("Search failed: %v", err))
|
|
return
|
|
}
|
|
|
|
selectedKeyIDs := make(map[string]struct{})
|
|
virtualKeyIDs := make(map[string]struct{})
|
|
routingRuleIDs := make(map[string]struct{})
|
|
for _, log := range result.Logs {
|
|
if log.SelectedKeyID != "" {
|
|
selectedKeyIDs[log.SelectedKeyID] = struct{}{}
|
|
}
|
|
if log.VirtualKeyID != nil && *log.VirtualKeyID != "" {
|
|
virtualKeyIDs[*log.VirtualKeyID] = struct{}{}
|
|
}
|
|
if log.RoutingRuleID != nil && *log.RoutingRuleID != "" {
|
|
routingRuleIDs[*log.RoutingRuleID] = struct{}{}
|
|
}
|
|
}
|
|
|
|
toSlice := func(m map[string]struct{}) []string {
|
|
if len(m) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]string, 0, len(m))
|
|
for id := range m {
|
|
out = append(out, id)
|
|
}
|
|
return out
|
|
}
|
|
|
|
redactedKeys := h.redactedKeysManager.GetAllRedactedKeys(ctx, toSlice(selectedKeyIDs))
|
|
redactedVirtualKeys := h.redactedKeysManager.GetAllRedactedVirtualKeys(ctx, toSlice(virtualKeyIDs))
|
|
redactedRoutingRules := h.redactedKeysManager.GetAllRedactedRoutingRules(ctx, toSlice(routingRuleIDs))
|
|
|
|
// Add selected key, virtual key, and routing rule to the result
|
|
for i, log := range result.Logs {
|
|
if log.SelectedKeyID != "" && log.SelectedKeyName != "" {
|
|
result.Logs[i].SelectedKey = findRedactedKey(redactedKeys, log.SelectedKeyID, log.SelectedKeyName)
|
|
}
|
|
if log.VirtualKeyID != nil && log.VirtualKeyName != nil && *log.VirtualKeyID != "" && *log.VirtualKeyName != "" {
|
|
result.Logs[i].VirtualKey = findRedactedVirtualKey(redactedVirtualKeys, *log.VirtualKeyID, *log.VirtualKeyName)
|
|
}
|
|
if log.RoutingRuleID != nil && log.RoutingRuleName != nil && *log.RoutingRuleID != "" && *log.RoutingRuleName != "" {
|
|
result.Logs[i].RoutingRule = findRedactedRoutingRule(redactedRoutingRules, *log.RoutingRuleID, *log.RoutingRuleName)
|
|
}
|
|
}
|
|
|
|
SendJSON(ctx, result)
|
|
}
|
|
|
|
// getLogByID handles GET /api/logs/{id} - Get a single log entry by ID including raw_request and raw_response
|
|
func (h *LoggingHandler) getLogByID(ctx *fasthttp.RequestCtx) {
|
|
id, ok := ctx.UserValue("id").(string)
|
|
if !ok || id == "" {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "log id is required")
|
|
return
|
|
}
|
|
|
|
log, err := h.logManager.GetLog(ctx, id)
|
|
if err != nil {
|
|
if errors.Is(err, logstore.ErrNotFound) {
|
|
SendError(ctx, fasthttp.StatusNotFound, "log not found")
|
|
return
|
|
}
|
|
SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("failed to get log: %v", err))
|
|
return
|
|
}
|
|
|
|
// Assemble virtual key, selected key, and routing rule objects (gorm:"-" fields not
|
|
// populated by GetLog) so the detail view receives the same structure as the list endpoint.
|
|
if log.SelectedKeyID != "" && log.SelectedKeyName != "" {
|
|
redactedKeys := h.redactedKeysManager.GetAllRedactedKeys(ctx, []string{log.SelectedKeyID})
|
|
log.SelectedKey = findRedactedKey(redactedKeys, log.SelectedKeyID, log.SelectedKeyName)
|
|
}
|
|
if log.VirtualKeyID != nil && log.VirtualKeyName != nil && *log.VirtualKeyID != "" && *log.VirtualKeyName != "" {
|
|
redactedVirtualKeys := h.redactedKeysManager.GetAllRedactedVirtualKeys(ctx, []string{*log.VirtualKeyID})
|
|
log.VirtualKey = findRedactedVirtualKey(redactedVirtualKeys, *log.VirtualKeyID, *log.VirtualKeyName)
|
|
}
|
|
if log.RoutingRuleID != nil && log.RoutingRuleName != nil && *log.RoutingRuleID != "" && *log.RoutingRuleName != "" {
|
|
redactedRoutingRules := h.redactedKeysManager.GetAllRedactedRoutingRules(ctx, []string{*log.RoutingRuleID})
|
|
log.RoutingRule = findRedactedRoutingRule(redactedRoutingRules, *log.RoutingRuleID, *log.RoutingRuleName)
|
|
}
|
|
|
|
SendJSON(ctx, log)
|
|
}
|
|
|
|
// getLogsStats handles GET /api/logs/stats - Get statistics for logs with filtering
|
|
func (h *LoggingHandler) getLogsStats(ctx *fasthttp.RequestCtx) {
|
|
// Parse query parameters into filters (same as getLogs)
|
|
filters := &logstore.SearchFilters{}
|
|
|
|
// Extract filters from query parameters
|
|
if providers := string(ctx.QueryArgs().Peek("providers")); providers != "" {
|
|
filters.Providers = parseCommaSeparated(providers)
|
|
}
|
|
if models := string(ctx.QueryArgs().Peek("models")); models != "" {
|
|
filters.Models = parseCommaSeparated(models)
|
|
}
|
|
if aliases := string(ctx.QueryArgs().Peek("aliases")); aliases != "" {
|
|
filters.Aliases = parseCommaSeparated(aliases)
|
|
}
|
|
if statuses := string(ctx.QueryArgs().Peek("status")); statuses != "" {
|
|
filters.Status = parseCommaSeparated(statuses)
|
|
}
|
|
if objects := string(ctx.QueryArgs().Peek("objects")); objects != "" {
|
|
filters.Objects = parseCommaSeparated(objects)
|
|
}
|
|
if parentRequestID := parseParentRequestIDFilter(ctx); parentRequestID != "" {
|
|
filters.ParentRequestID = parentRequestID
|
|
}
|
|
if selectedKeyIDs := string(ctx.QueryArgs().Peek("selected_key_ids")); selectedKeyIDs != "" {
|
|
filters.SelectedKeyIDs = parseCommaSeparated(selectedKeyIDs)
|
|
}
|
|
if virtualKeyIDs := string(ctx.QueryArgs().Peek("virtual_key_ids")); virtualKeyIDs != "" {
|
|
filters.VirtualKeyIDs = parseCommaSeparated(virtualKeyIDs)
|
|
}
|
|
if routingRuleIDs := string(ctx.QueryArgs().Peek("routing_rule_ids")); routingRuleIDs != "" {
|
|
filters.RoutingRuleIDs = parseCommaSeparated(routingRuleIDs)
|
|
}
|
|
if teamIDs := string(ctx.QueryArgs().Peek("team_ids")); teamIDs != "" {
|
|
filters.TeamIDs = parseCommaSeparated(teamIDs)
|
|
}
|
|
if customerIDs := string(ctx.QueryArgs().Peek("customer_ids")); customerIDs != "" {
|
|
filters.CustomerIDs = parseCommaSeparated(customerIDs)
|
|
}
|
|
if userIDs := string(ctx.QueryArgs().Peek("user_ids")); userIDs != "" {
|
|
filters.UserIDs = parseCommaSeparated(userIDs)
|
|
}
|
|
if businessUnitIDs := string(ctx.QueryArgs().Peek("business_unit_ids")); businessUnitIDs != "" {
|
|
filters.BusinessUnitIDs = parseCommaSeparated(businessUnitIDs)
|
|
}
|
|
if routingEngines := string(ctx.QueryArgs().Peek("routing_engine_used")); routingEngines != "" {
|
|
filters.RoutingEngineUsed = parseCommaSeparated(routingEngines)
|
|
}
|
|
if startTime := string(ctx.QueryArgs().Peek("start_time")); startTime != "" {
|
|
if t, err := time.Parse(time.RFC3339, startTime); err == nil {
|
|
filters.StartTime = &t
|
|
}
|
|
}
|
|
if endTime := string(ctx.QueryArgs().Peek("end_time")); endTime != "" {
|
|
if t, err := time.Parse(time.RFC3339, endTime); err == nil {
|
|
filters.EndTime = &t
|
|
}
|
|
}
|
|
if minLatency := string(ctx.QueryArgs().Peek("min_latency")); minLatency != "" {
|
|
if f, err := strconv.ParseFloat(minLatency, 64); err == nil {
|
|
filters.MinLatency = &f
|
|
}
|
|
}
|
|
if maxLatency := string(ctx.QueryArgs().Peek("max_latency")); maxLatency != "" {
|
|
if val, err := strconv.ParseFloat(maxLatency, 64); err == nil {
|
|
filters.MaxLatency = &val
|
|
}
|
|
}
|
|
if minTokens := string(ctx.QueryArgs().Peek("min_tokens")); minTokens != "" {
|
|
if val, err := strconv.Atoi(minTokens); err == nil {
|
|
filters.MinTokens = &val
|
|
}
|
|
}
|
|
if maxTokens := string(ctx.QueryArgs().Peek("max_tokens")); maxTokens != "" {
|
|
if val, err := strconv.Atoi(maxTokens); err == nil {
|
|
filters.MaxTokens = &val
|
|
}
|
|
}
|
|
if cost := string(ctx.QueryArgs().Peek("min_cost")); cost != "" {
|
|
if val, err := strconv.ParseFloat(cost, 64); err == nil {
|
|
filters.MinCost = &val
|
|
}
|
|
}
|
|
if maxCost := string(ctx.QueryArgs().Peek("max_cost")); maxCost != "" {
|
|
if val, err := strconv.ParseFloat(maxCost, 64); err == nil {
|
|
filters.MaxCost = &val
|
|
}
|
|
}
|
|
if missingCost := string(ctx.QueryArgs().Peek("missing_cost_only")); missingCost != "" {
|
|
if val, err := strconv.ParseBool(missingCost); err == nil {
|
|
filters.MissingCostOnly = val
|
|
}
|
|
}
|
|
if contentSearch := string(ctx.QueryArgs().Peek("content_search")); contentSearch != "" {
|
|
filters.ContentSearch = contentSearch
|
|
}
|
|
parseMetadataFilters(ctx, filters)
|
|
|
|
stats, err := h.logManager.GetStats(ctx, filters)
|
|
if err != nil {
|
|
logger.Error("failed to get log stats: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("Stats calculation failed: %v", err))
|
|
return
|
|
}
|
|
|
|
SendJSON(ctx, stats)
|
|
}
|
|
|
|
// getLogsHistogram handles GET /api/logs/histogram - Get time-bucketed request counts
|
|
func (h *LoggingHandler) getLogsHistogram(ctx *fasthttp.RequestCtx) {
|
|
filters := parseHistogramFilters(ctx)
|
|
bucketSizeSeconds := calculateBucketSize(filters.StartTime, filters.EndTime)
|
|
|
|
result, err := h.logManager.GetHistogram(ctx, filters, bucketSizeSeconds)
|
|
if err != nil {
|
|
logger.Error("failed to get log histogram: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("Histogram calculation failed: %v", err))
|
|
return
|
|
}
|
|
|
|
SendJSON(ctx, result)
|
|
}
|
|
|
|
// calculateBucketSize determines appropriate bucket size based on time range
|
|
func calculateBucketSize(start, end *time.Time) int64 {
|
|
if start == nil || end == nil {
|
|
return 3600 // Default 1 hour
|
|
}
|
|
duration := end.Sub(*start)
|
|
switch {
|
|
case duration >= 365*24*time.Hour: // >= 12 months
|
|
return 30 * 24 * 3600 // Monthly (30 days)
|
|
case duration >= 90*24*time.Hour: // >= 3 months
|
|
return 7 * 24 * 3600 // Weekly (7 days)
|
|
case duration >= 30*24*time.Hour: // >= 1 month
|
|
return 3 * 24 * 3600 // 3 days
|
|
case duration >= 7*24*time.Hour: // >= 7 days
|
|
return 24 * 3600 // Daily
|
|
case duration >= 3*24*time.Hour: // >= 3 days
|
|
return 8 * 3600 // 8 hours
|
|
case duration >= 24*time.Hour: // >= 24 hours
|
|
return 3600 // Hourly
|
|
case duration >= 2*time.Hour: // >= 2 hours
|
|
return 600 // 10 minutes
|
|
default:
|
|
return 60 // 1 minute buckets for < 2 hours
|
|
}
|
|
}
|
|
|
|
// parseHistogramFilters extracts common filter parameters from query args
|
|
func parseHistogramFilters(ctx *fasthttp.RequestCtx) *logstore.SearchFilters {
|
|
filters := &logstore.SearchFilters{}
|
|
|
|
if providers := string(ctx.QueryArgs().Peek("providers")); providers != "" {
|
|
filters.Providers = parseCommaSeparated(providers)
|
|
}
|
|
if models := string(ctx.QueryArgs().Peek("models")); models != "" {
|
|
filters.Models = parseCommaSeparated(models)
|
|
}
|
|
if aliases := string(ctx.QueryArgs().Peek("aliases")); aliases != "" {
|
|
filters.Aliases = parseCommaSeparated(aliases)
|
|
}
|
|
if statuses := string(ctx.QueryArgs().Peek("status")); statuses != "" {
|
|
filters.Status = parseCommaSeparated(statuses)
|
|
}
|
|
if objects := string(ctx.QueryArgs().Peek("objects")); objects != "" {
|
|
filters.Objects = parseCommaSeparated(objects)
|
|
}
|
|
if parentRequestID := parseParentRequestIDFilter(ctx); parentRequestID != "" {
|
|
filters.ParentRequestID = parentRequestID
|
|
}
|
|
if selectedKeyIDs := string(ctx.QueryArgs().Peek("selected_key_ids")); selectedKeyIDs != "" {
|
|
filters.SelectedKeyIDs = parseCommaSeparated(selectedKeyIDs)
|
|
}
|
|
if virtualKeyIDs := string(ctx.QueryArgs().Peek("virtual_key_ids")); virtualKeyIDs != "" {
|
|
filters.VirtualKeyIDs = parseCommaSeparated(virtualKeyIDs)
|
|
}
|
|
if routingRuleIDs := string(ctx.QueryArgs().Peek("routing_rule_ids")); routingRuleIDs != "" {
|
|
filters.RoutingRuleIDs = parseCommaSeparated(routingRuleIDs)
|
|
}
|
|
if teamIDs := string(ctx.QueryArgs().Peek("team_ids")); teamIDs != "" {
|
|
filters.TeamIDs = parseCommaSeparated(teamIDs)
|
|
}
|
|
if customerIDs := string(ctx.QueryArgs().Peek("customer_ids")); customerIDs != "" {
|
|
filters.CustomerIDs = parseCommaSeparated(customerIDs)
|
|
}
|
|
if userIDs := string(ctx.QueryArgs().Peek("user_ids")); userIDs != "" {
|
|
filters.UserIDs = parseCommaSeparated(userIDs)
|
|
}
|
|
if businessUnitIDs := string(ctx.QueryArgs().Peek("business_unit_ids")); businessUnitIDs != "" {
|
|
filters.BusinessUnitIDs = parseCommaSeparated(businessUnitIDs)
|
|
}
|
|
if routingEngines := string(ctx.QueryArgs().Peek("routing_engine_used")); routingEngines != "" {
|
|
filters.RoutingEngineUsed = parseCommaSeparated(routingEngines)
|
|
}
|
|
if startTime := string(ctx.QueryArgs().Peek("start_time")); startTime != "" {
|
|
if t, err := time.Parse(time.RFC3339, startTime); err == nil {
|
|
filters.StartTime = &t
|
|
}
|
|
}
|
|
if endTime := string(ctx.QueryArgs().Peek("end_time")); endTime != "" {
|
|
if t, err := time.Parse(time.RFC3339, endTime); err == nil {
|
|
filters.EndTime = &t
|
|
}
|
|
}
|
|
if minLatency := string(ctx.QueryArgs().Peek("min_latency")); minLatency != "" {
|
|
if f, err := strconv.ParseFloat(minLatency, 64); err == nil {
|
|
filters.MinLatency = &f
|
|
}
|
|
}
|
|
if maxLatency := string(ctx.QueryArgs().Peek("max_latency")); maxLatency != "" {
|
|
if val, err := strconv.ParseFloat(maxLatency, 64); err == nil {
|
|
filters.MaxLatency = &val
|
|
}
|
|
}
|
|
if minTokens := string(ctx.QueryArgs().Peek("min_tokens")); minTokens != "" {
|
|
if val, err := strconv.Atoi(minTokens); err == nil {
|
|
filters.MinTokens = &val
|
|
}
|
|
}
|
|
if maxTokens := string(ctx.QueryArgs().Peek("max_tokens")); maxTokens != "" {
|
|
if val, err := strconv.Atoi(maxTokens); err == nil {
|
|
filters.MaxTokens = &val
|
|
}
|
|
}
|
|
if cost := string(ctx.QueryArgs().Peek("min_cost")); cost != "" {
|
|
if val, err := strconv.ParseFloat(cost, 64); err == nil {
|
|
filters.MinCost = &val
|
|
}
|
|
}
|
|
if maxCost := string(ctx.QueryArgs().Peek("max_cost")); maxCost != "" {
|
|
if val, err := strconv.ParseFloat(maxCost, 64); err == nil {
|
|
filters.MaxCost = &val
|
|
}
|
|
}
|
|
if missingCost := string(ctx.QueryArgs().Peek("missing_cost_only")); missingCost != "" {
|
|
if val, err := strconv.ParseBool(missingCost); err == nil {
|
|
filters.MissingCostOnly = val
|
|
}
|
|
}
|
|
if contentSearch := string(ctx.QueryArgs().Peek("content_search")); contentSearch != "" {
|
|
filters.ContentSearch = contentSearch
|
|
}
|
|
parseMetadataFilters(ctx, filters)
|
|
|
|
return filters
|
|
}
|
|
|
|
// getLogsTokenHistogram handles GET /api/logs/histogram/tokens - Get time-bucketed token usage
|
|
func (h *LoggingHandler) getLogsTokenHistogram(ctx *fasthttp.RequestCtx) {
|
|
filters := parseHistogramFilters(ctx)
|
|
bucketSizeSeconds := calculateBucketSize(filters.StartTime, filters.EndTime)
|
|
|
|
result, err := h.logManager.GetTokenHistogram(ctx, filters, bucketSizeSeconds)
|
|
if err != nil {
|
|
logger.Error("failed to get token histogram: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("Token histogram calculation failed: %v", err))
|
|
return
|
|
}
|
|
|
|
SendJSON(ctx, result)
|
|
}
|
|
|
|
// getLogsCostHistogram handles GET /api/logs/histogram/cost - Get time-bucketed cost data with model breakdown
|
|
func (h *LoggingHandler) getLogsCostHistogram(ctx *fasthttp.RequestCtx) {
|
|
filters := parseHistogramFilters(ctx)
|
|
bucketSizeSeconds := calculateBucketSize(filters.StartTime, filters.EndTime)
|
|
|
|
result, err := h.logManager.GetCostHistogram(ctx, filters, bucketSizeSeconds)
|
|
if err != nil {
|
|
logger.Error("failed to get cost histogram: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("Cost histogram calculation failed: %v", err))
|
|
return
|
|
}
|
|
|
|
SendJSON(ctx, result)
|
|
}
|
|
|
|
// getLogsModelHistogram handles GET /api/logs/histogram/models - Get time-bucketed model usage with success/error breakdown
|
|
func (h *LoggingHandler) getLogsModelHistogram(ctx *fasthttp.RequestCtx) {
|
|
filters := parseHistogramFilters(ctx)
|
|
bucketSizeSeconds := calculateBucketSize(filters.StartTime, filters.EndTime)
|
|
|
|
result, err := h.logManager.GetModelHistogram(ctx, filters, bucketSizeSeconds)
|
|
if err != nil {
|
|
logger.Error("failed to get model histogram: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("Model histogram calculation failed: %v", err))
|
|
return
|
|
}
|
|
|
|
SendJSON(ctx, result)
|
|
}
|
|
|
|
// getLogsLatencyHistogram handles GET /api/logs/histogram/latency - Get time-bucketed latency percentiles
|
|
func (h *LoggingHandler) getLogsLatencyHistogram(ctx *fasthttp.RequestCtx) {
|
|
filters := parseHistogramFilters(ctx)
|
|
bucketSizeSeconds := calculateBucketSize(filters.StartTime, filters.EndTime)
|
|
|
|
result, err := h.logManager.GetLatencyHistogram(ctx, filters, bucketSizeSeconds)
|
|
if err != nil {
|
|
logger.Error("failed to get latency histogram: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("Latency histogram calculation failed: %v", err))
|
|
return
|
|
}
|
|
|
|
SendJSON(ctx, result)
|
|
}
|
|
|
|
// getLogsProviderCostHistogram handles GET /api/logs/histogram/cost/by-provider - Get time-bucketed cost data with provider breakdown
|
|
func (h *LoggingHandler) getLogsProviderCostHistogram(ctx *fasthttp.RequestCtx) {
|
|
filters := parseHistogramFilters(ctx)
|
|
bucketSizeSeconds := calculateBucketSize(filters.StartTime, filters.EndTime)
|
|
|
|
result, err := h.logManager.GetProviderCostHistogram(ctx, filters, bucketSizeSeconds)
|
|
if err != nil {
|
|
logger.Error("failed to get provider cost histogram: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("Provider cost histogram calculation failed: %v", err))
|
|
return
|
|
}
|
|
|
|
SendJSON(ctx, result)
|
|
}
|
|
|
|
// getLogsProviderTokenHistogram handles GET /api/logs/histogram/tokens/by-provider - Get time-bucketed token usage with provider breakdown
|
|
func (h *LoggingHandler) getLogsProviderTokenHistogram(ctx *fasthttp.RequestCtx) {
|
|
filters := parseHistogramFilters(ctx)
|
|
bucketSizeSeconds := calculateBucketSize(filters.StartTime, filters.EndTime)
|
|
|
|
result, err := h.logManager.GetProviderTokenHistogram(ctx, filters, bucketSizeSeconds)
|
|
if err != nil {
|
|
logger.Error("failed to get provider token histogram: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("Provider token histogram calculation failed: %v", err))
|
|
return
|
|
}
|
|
|
|
SendJSON(ctx, result)
|
|
}
|
|
|
|
// getLogsProviderLatencyHistogram handles GET /api/logs/histogram/latency/by-provider - Get time-bucketed latency percentiles with provider breakdown
|
|
func (h *LoggingHandler) getLogsProviderLatencyHistogram(ctx *fasthttp.RequestCtx) {
|
|
filters := parseHistogramFilters(ctx)
|
|
bucketSizeSeconds := calculateBucketSize(filters.StartTime, filters.EndTime)
|
|
|
|
result, err := h.logManager.GetProviderLatencyHistogram(ctx, filters, bucketSizeSeconds)
|
|
if err != nil {
|
|
logger.Error("failed to get provider latency histogram: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("Provider latency histogram calculation failed: %v", err))
|
|
return
|
|
}
|
|
|
|
SendJSON(ctx, result)
|
|
}
|
|
|
|
// parseDimension extracts and validates the "dimension" query parameter.
|
|
// Returns the validated HistogramDimension and true on success, or sends an error response and returns false.
|
|
func parseDimension(ctx *fasthttp.RequestCtx) (logstore.HistogramDimension, bool) {
|
|
dim := logstore.HistogramDimension(string(ctx.QueryArgs().Peek("dimension")))
|
|
if dim == "" {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "Missing required query parameter: dimension. Valid values: provider, team_id, customer_id, user_id, business_unit_id")
|
|
return "", false
|
|
}
|
|
if !logstore.ValidHistogramDimensions[dim] {
|
|
SendError(ctx, fasthttp.StatusBadRequest, fmt.Sprintf("Invalid dimension: %s. Valid values: provider, team_id, customer_id, user_id, business_unit_id", dim))
|
|
return "", false
|
|
}
|
|
return dim, true
|
|
}
|
|
|
|
// getLogsDimensionCostHistogram handles GET /api/logs/histogram/cost/by-dimension
|
|
// Returns time-bucketed cost data grouped by the dimension specified in the "dimension" query param.
|
|
func (h *LoggingHandler) getLogsDimensionCostHistogram(ctx *fasthttp.RequestCtx) {
|
|
dimension, ok := parseDimension(ctx)
|
|
if !ok {
|
|
return
|
|
}
|
|
filters := parseHistogramFilters(ctx)
|
|
bucketSizeSeconds := calculateBucketSize(filters.StartTime, filters.EndTime)
|
|
|
|
result, err := h.logManager.GetDimensionCostHistogram(ctx, filters, bucketSizeSeconds, dimension)
|
|
if err != nil {
|
|
logger.Error("failed to get dimension cost histogram: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("Dimension cost histogram calculation failed: %v", err))
|
|
return
|
|
}
|
|
SendJSON(ctx, result)
|
|
}
|
|
|
|
// getLogsDimensionTokenHistogram handles GET /api/logs/histogram/tokens/by-dimension
|
|
// Returns time-bucketed token usage grouped by the dimension specified in the "dimension" query param.
|
|
func (h *LoggingHandler) getLogsDimensionTokenHistogram(ctx *fasthttp.RequestCtx) {
|
|
dimension, ok := parseDimension(ctx)
|
|
if !ok {
|
|
return
|
|
}
|
|
filters := parseHistogramFilters(ctx)
|
|
bucketSizeSeconds := calculateBucketSize(filters.StartTime, filters.EndTime)
|
|
|
|
result, err := h.logManager.GetDimensionTokenHistogram(ctx, filters, bucketSizeSeconds, dimension)
|
|
if err != nil {
|
|
logger.Error("failed to get dimension token histogram: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("Dimension token histogram calculation failed: %v", err))
|
|
return
|
|
}
|
|
SendJSON(ctx, result)
|
|
}
|
|
|
|
// getLogsDimensionLatencyHistogram handles GET /api/logs/histogram/latency/by-dimension
|
|
// Returns time-bucketed latency percentiles grouped by the dimension specified in the "dimension" query param.
|
|
func (h *LoggingHandler) getLogsDimensionLatencyHistogram(ctx *fasthttp.RequestCtx) {
|
|
dimension, ok := parseDimension(ctx)
|
|
if !ok {
|
|
return
|
|
}
|
|
filters := parseHistogramFilters(ctx)
|
|
bucketSizeSeconds := calculateBucketSize(filters.StartTime, filters.EndTime)
|
|
|
|
result, err := h.logManager.GetDimensionLatencyHistogram(ctx, filters, bucketSizeSeconds, dimension)
|
|
if err != nil {
|
|
logger.Error("failed to get dimension latency histogram: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("Dimension latency histogram calculation failed: %v", err))
|
|
return
|
|
}
|
|
SendJSON(ctx, result)
|
|
}
|
|
|
|
// getDroppedRequests handles GET /api/logs/dropped - Get the number of dropped requests
|
|
func (h *LoggingHandler) getDroppedRequests(ctx *fasthttp.RequestCtx) {
|
|
droppedRequests := h.logManager.GetDroppedRequests(ctx)
|
|
SendJSON(ctx, map[string]int64{"dropped_requests": droppedRequests})
|
|
}
|
|
|
|
// getModelRankings handles GET /api/logs/rankings - Get models ranked by usage with trends
|
|
func (h *LoggingHandler) getModelRankings(ctx *fasthttp.RequestCtx) {
|
|
filters := parseHistogramFilters(ctx)
|
|
|
|
result, err := h.logManager.GetModelRankings(ctx, filters)
|
|
if err != nil {
|
|
logger.Error("failed to get model rankings: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("Model rankings calculation failed: %v", err))
|
|
return
|
|
}
|
|
|
|
SendJSON(ctx, result)
|
|
}
|
|
|
|
// getAvailableFilterData handles GET /api/logs/filterdata - Get all unique filter data from logs
|
|
func (h *LoggingHandler) getAvailableFilterData(ctx *fasthttp.RequestCtx) {
|
|
hideDeletedVirtualKeys := h.shouldHideDeletedVirtualKeysInFilters()
|
|
|
|
var (
|
|
models []string
|
|
aliases []string
|
|
selectedKeys []logging.KeyPair
|
|
virtualKeys []logging.KeyPair
|
|
routingRules []logging.KeyPair
|
|
routingEngines []string
|
|
teams []logging.KeyPair
|
|
customers []logging.KeyPair
|
|
users []logging.KeyPair
|
|
businessUnits []logging.KeyPair
|
|
metadataKeys map[string][]string
|
|
mu sync.Mutex
|
|
)
|
|
|
|
g, gCtx := errgroup.WithContext(ctx)
|
|
|
|
g.Go(func() error {
|
|
result := h.logManager.GetAvailableModels(gCtx)
|
|
mu.Lock()
|
|
models = result
|
|
mu.Unlock()
|
|
return nil
|
|
})
|
|
g.Go(func() error {
|
|
result := h.logManager.GetAvailableAliases(gCtx)
|
|
mu.Lock()
|
|
aliases = result
|
|
mu.Unlock()
|
|
return nil
|
|
})
|
|
g.Go(func() error {
|
|
result := h.logManager.GetAvailableSelectedKeys(gCtx)
|
|
mu.Lock()
|
|
selectedKeys = result
|
|
mu.Unlock()
|
|
return nil
|
|
})
|
|
g.Go(func() error {
|
|
result := h.logManager.GetAvailableVirtualKeys(gCtx)
|
|
mu.Lock()
|
|
virtualKeys = result
|
|
mu.Unlock()
|
|
return nil
|
|
})
|
|
g.Go(func() error {
|
|
result := h.logManager.GetAvailableRoutingRules(gCtx)
|
|
mu.Lock()
|
|
routingRules = result
|
|
mu.Unlock()
|
|
return nil
|
|
})
|
|
g.Go(func() error {
|
|
result := h.logManager.GetAvailableRoutingEngines(gCtx)
|
|
mu.Lock()
|
|
routingEngines = result
|
|
mu.Unlock()
|
|
return nil
|
|
})
|
|
g.Go(func() error {
|
|
result := h.logManager.GetAvailableTeams(gCtx)
|
|
mu.Lock()
|
|
teams = result
|
|
mu.Unlock()
|
|
return nil
|
|
})
|
|
g.Go(func() error {
|
|
result := h.logManager.GetAvailableCustomers(gCtx)
|
|
mu.Lock()
|
|
customers = result
|
|
mu.Unlock()
|
|
return nil
|
|
})
|
|
g.Go(func() error {
|
|
result := h.logManager.GetAvailableUsers(gCtx)
|
|
mu.Lock()
|
|
users = result
|
|
mu.Unlock()
|
|
return nil
|
|
})
|
|
g.Go(func() error {
|
|
result := h.logManager.GetAvailableBusinessUnits(gCtx)
|
|
mu.Lock()
|
|
businessUnits = result
|
|
mu.Unlock()
|
|
return nil
|
|
})
|
|
g.Go(func() error {
|
|
result, err := h.logManager.GetAvailableMetadataKeys(gCtx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
mu.Lock()
|
|
metadataKeys = result
|
|
mu.Unlock()
|
|
return nil
|
|
})
|
|
|
|
if err := g.Wait(); err != nil {
|
|
logger.Error("failed to get filter data: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("Failed to get filter data: %v", err))
|
|
return
|
|
}
|
|
|
|
// Extract IDs for redaction lookup
|
|
selectedKeyIDs := make([]string, len(selectedKeys))
|
|
for i, key := range selectedKeys {
|
|
selectedKeyIDs[i] = key.ID
|
|
}
|
|
virtualKeyIDs := make([]string, len(virtualKeys))
|
|
for i, key := range virtualKeys {
|
|
virtualKeyIDs[i] = key.ID
|
|
}
|
|
routingRuleIDs := make([]string, len(routingRules))
|
|
for i, rule := range routingRules {
|
|
routingRuleIDs[i] = rule.ID
|
|
}
|
|
|
|
redactedSelectedKeys := make(map[string]schemas.Key)
|
|
for _, selectedKey := range h.redactedKeysManager.GetAllRedactedKeys(ctx, selectedKeyIDs) {
|
|
redactedSelectedKeys[selectedKey.ID] = selectedKey
|
|
}
|
|
redactedVirtualKeys := make(map[string]tables.TableVirtualKey)
|
|
for _, virtualKey := range h.redactedKeysManager.GetAllRedactedVirtualKeys(ctx, virtualKeyIDs) {
|
|
redactedVirtualKeys[virtualKey.ID] = virtualKey
|
|
}
|
|
redactedRoutingRules := make(map[string]tables.TableRoutingRule)
|
|
for _, routingRule := range h.redactedKeysManager.GetAllRedactedRoutingRules(ctx, routingRuleIDs) {
|
|
redactedRoutingRules[routingRule.ID] = routingRule
|
|
}
|
|
|
|
// Check if all selected key ids are present in the redacted selected keys (will not be present in case a key is deleted, but we still need to show its filter)
|
|
for _, selectedKey := range selectedKeys {
|
|
if _, ok := redactedSelectedKeys[selectedKey.ID]; !ok {
|
|
// Create a new key struct directly since we know it doesn't exist
|
|
redactedSelectedKeys[selectedKey.ID] = schemas.Key{
|
|
ID: selectedKey.ID,
|
|
Name: selectedKey.Name + " (deleted)",
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if all virtual key ids are present in the redacted virtual keys (will not be present in case a virtual key is deleted, but we still need to show its filter)
|
|
for _, virtualKey := range virtualKeys {
|
|
if _, ok := redactedVirtualKeys[virtualKey.ID]; !ok {
|
|
if hideDeletedVirtualKeys {
|
|
continue
|
|
}
|
|
// Create a new virtual key struct directly since we know it doesn't exist
|
|
redactedVirtualKeys[virtualKey.ID] = tables.TableVirtualKey{
|
|
ID: virtualKey.ID,
|
|
Name: virtualKey.Name + " (deleted)",
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if all routing rule ids are present in the redacted routing rules (will not be present in case a routing rule is deleted, but we still need to show its filter)
|
|
for _, routingRule := range routingRules {
|
|
if _, ok := redactedRoutingRules[routingRule.ID]; !ok {
|
|
// Create a new routing rule struct directly since we know it doesn't exist
|
|
redactedRoutingRules[routingRule.ID] = tables.TableRoutingRule{
|
|
ID: routingRule.ID,
|
|
Name: routingRule.Name + " (deleted)",
|
|
}
|
|
}
|
|
}
|
|
|
|
// Convert maps to arrays for frontend consumption
|
|
selectedKeysArray := make([]schemas.Key, 0, len(redactedSelectedKeys))
|
|
for _, key := range redactedSelectedKeys {
|
|
selectedKeysArray = append(selectedKeysArray, key)
|
|
}
|
|
|
|
virtualKeysArray := make([]tables.TableVirtualKey, 0, len(redactedVirtualKeys))
|
|
for _, key := range redactedVirtualKeys {
|
|
virtualKeysArray = append(virtualKeysArray, key)
|
|
}
|
|
|
|
routingRulesArray := make([]tables.TableRoutingRule, 0, len(redactedRoutingRules))
|
|
for _, rule := range redactedRoutingRules {
|
|
routingRulesArray = append(routingRulesArray, rule)
|
|
}
|
|
|
|
if metadataKeys == nil {
|
|
metadataKeys = make(map[string][]string)
|
|
}
|
|
SendJSON(ctx, map[string]interface{}{"models": models, "aliases": aliases, "selected_keys": selectedKeysArray, "virtual_keys": virtualKeysArray, "routing_rules": routingRulesArray, "routing_engines": routingEngines, "teams": teams, "customers": customers, "users": users, "business_units": businessUnits, "metadata_keys": metadataKeys})
|
|
}
|
|
|
|
// deleteLogs handles DELETE /api/logs - Delete logs by their IDs
|
|
func (h *LoggingHandler) deleteLogs(ctx *fasthttp.RequestCtx) {
|
|
var req struct {
|
|
IDs []string `json:"ids"`
|
|
}
|
|
if err := sonic.Unmarshal(ctx.PostBody(), &req); err != nil {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "Invalid JSON")
|
|
return
|
|
}
|
|
|
|
if len(req.IDs) == 0 {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "No log IDs provided")
|
|
return
|
|
}
|
|
|
|
if err := h.logManager.DeleteLogs(ctx, req.IDs); err != nil {
|
|
logger.Error("failed to delete logs: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, "Failed to delete logs")
|
|
return
|
|
}
|
|
|
|
SendJSON(ctx, map[string]interface{}{
|
|
"message": "Logs deleted successfully",
|
|
})
|
|
}
|
|
|
|
// recalculateLogCosts handles POST /api/logs/recalculate-cost - recompute missing costs in batches
|
|
func (h *LoggingHandler) recalculateLogCosts(ctx *fasthttp.RequestCtx) {
|
|
var payload recalculateCostRequest
|
|
body := ctx.PostBody()
|
|
if len(body) > 0 {
|
|
if err := sonic.Unmarshal(body, &payload); err != nil {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "Invalid JSON")
|
|
return
|
|
}
|
|
}
|
|
|
|
limit := 200
|
|
if payload.Limit != nil {
|
|
limit = *payload.Limit
|
|
}
|
|
if limit <= 0 {
|
|
limit = 200
|
|
}
|
|
if limit > 1000 {
|
|
limit = 1000
|
|
}
|
|
|
|
filters := payload.Filters
|
|
filters.MissingCostOnly = true
|
|
|
|
result, err := h.logManager.RecalculateCosts(ctx, &filters, limit)
|
|
if err != nil {
|
|
logger.Error("failed to recalculate log costs: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("Failed to recalculate costs: %v", err))
|
|
return
|
|
}
|
|
|
|
SendJSON(ctx, result)
|
|
}
|
|
|
|
// Helper functions
|
|
|
|
func findRedactedKey(redactedKeys []schemas.Key, id string, name string) *schemas.Key {
|
|
if len(redactedKeys) == 0 {
|
|
return &schemas.Key{
|
|
ID: id,
|
|
Name: func() string {
|
|
if name != "" {
|
|
return name + " (deleted)"
|
|
} else {
|
|
return ""
|
|
}
|
|
}(),
|
|
}
|
|
}
|
|
for _, key := range redactedKeys {
|
|
if key.ID == id {
|
|
return &key
|
|
}
|
|
}
|
|
return &schemas.Key{
|
|
ID: id,
|
|
Name: func() string {
|
|
if name != "" {
|
|
return name + " (deleted)"
|
|
} else {
|
|
return ""
|
|
}
|
|
}(),
|
|
}
|
|
}
|
|
|
|
func findRedactedVirtualKey(redactedVirtualKeys []tables.TableVirtualKey, id string, name string) *tables.TableVirtualKey {
|
|
if len(redactedVirtualKeys) == 0 {
|
|
return &tables.TableVirtualKey{
|
|
ID: id,
|
|
Name: func() string {
|
|
if name != "" {
|
|
return name + " (deleted)"
|
|
} else {
|
|
return ""
|
|
}
|
|
}(),
|
|
}
|
|
}
|
|
for _, virtualKey := range redactedVirtualKeys {
|
|
if virtualKey.ID == id {
|
|
return &virtualKey
|
|
}
|
|
}
|
|
return &tables.TableVirtualKey{
|
|
ID: id,
|
|
Name: func() string {
|
|
if name != "" {
|
|
return name + " (deleted)"
|
|
} else {
|
|
return ""
|
|
}
|
|
}(),
|
|
}
|
|
}
|
|
|
|
func findRedactedRoutingRule(redactedRoutingRules []tables.TableRoutingRule, id string, name string) *tables.TableRoutingRule {
|
|
if len(redactedRoutingRules) == 0 {
|
|
return &tables.TableRoutingRule{
|
|
ID: id,
|
|
Name: func() string {
|
|
if name != "" {
|
|
return name + " (deleted)"
|
|
} else {
|
|
return ""
|
|
}
|
|
}(),
|
|
}
|
|
}
|
|
for _, routingRule := range redactedRoutingRules {
|
|
if routingRule.ID == id {
|
|
return &routingRule
|
|
}
|
|
}
|
|
return &tables.TableRoutingRule{
|
|
ID: id,
|
|
Name: func() string {
|
|
if name != "" {
|
|
return name + " (deleted)"
|
|
} else {
|
|
return ""
|
|
}
|
|
}(),
|
|
}
|
|
}
|
|
|
|
// parseCommaSeparated splits a comma-separated string into a slice
|
|
func parseCommaSeparated(s string) []string {
|
|
if s == "" {
|
|
return nil
|
|
}
|
|
|
|
var result []string
|
|
for _, item := range strings.Split(s, ",") {
|
|
if trimmed := strings.TrimSpace(item); trimmed != "" {
|
|
result = append(result, trimmed)
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// parseMetadataFilters extracts metadata_* query params and sets them on the filters.
|
|
func parseMetadataFilters(ctx *fasthttp.RequestCtx, filters *logstore.SearchFilters) {
|
|
var metadataFilters map[string]string
|
|
ctx.QueryArgs().VisitAll(func(key, value []byte) { //nolint:staticcheck
|
|
keyStr := string(key)
|
|
if strings.HasPrefix(keyStr, "metadata_") {
|
|
metadataKey := strings.TrimPrefix(keyStr, "metadata_")
|
|
if metadataKey != "" {
|
|
if metadataFilters == nil {
|
|
metadataFilters = make(map[string]string)
|
|
}
|
|
metadataFilters[metadataKey] = string(value)
|
|
}
|
|
}
|
|
})
|
|
if len(metadataFilters) > 0 {
|
|
filters.MetadataFilters = metadataFilters
|
|
}
|
|
}
|
|
|
|
type recalculateCostRequest struct {
|
|
Filters logstore.SearchFilters `json:"filters"`
|
|
Limit *int `json:"limit,omitempty"`
|
|
}
|
|
|
|
// parseMCPFiltersAndPagination parses MCP tool log filters and pagination from query parameters.
|
|
// Returns an error if any required parsing fails (e.g., invalid time format, invalid number format).
|
|
func parseMCPFiltersAndPagination(ctx *fasthttp.RequestCtx) (*logstore.MCPToolLogSearchFilters, *logstore.PaginationOptions, error) {
|
|
filters := &logstore.MCPToolLogSearchFilters{}
|
|
pagination := &logstore.PaginationOptions{}
|
|
|
|
// Extract filters from query parameters
|
|
if toolNames := string(ctx.QueryArgs().Peek("tool_names")); toolNames != "" {
|
|
filters.ToolNames = parseCommaSeparated(toolNames)
|
|
}
|
|
if serverLabels := string(ctx.QueryArgs().Peek("server_labels")); serverLabels != "" {
|
|
filters.ServerLabels = parseCommaSeparated(serverLabels)
|
|
}
|
|
if statuses := string(ctx.QueryArgs().Peek("status")); statuses != "" {
|
|
filters.Status = parseCommaSeparated(statuses)
|
|
}
|
|
if virtualKeyIDs := string(ctx.QueryArgs().Peek("virtual_key_ids")); virtualKeyIDs != "" {
|
|
filters.VirtualKeyIDs = parseCommaSeparated(virtualKeyIDs)
|
|
}
|
|
if llmRequestIDs := string(ctx.QueryArgs().Peek("llm_request_ids")); llmRequestIDs != "" {
|
|
filters.LLMRequestIDs = parseCommaSeparated(llmRequestIDs)
|
|
}
|
|
if startTime := string(ctx.QueryArgs().Peek("start_time")); startTime != "" {
|
|
t, err := time.Parse(time.RFC3339, startTime)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("invalid start_time format: %w", err)
|
|
}
|
|
filters.StartTime = &t
|
|
}
|
|
if endTime := string(ctx.QueryArgs().Peek("end_time")); endTime != "" {
|
|
t, err := time.Parse(time.RFC3339, endTime)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("invalid end_time format: %w", err)
|
|
}
|
|
filters.EndTime = &t
|
|
}
|
|
if minLatency := string(ctx.QueryArgs().Peek("min_latency")); minLatency != "" {
|
|
f, err := strconv.ParseFloat(minLatency, 64)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("invalid min_latency format: %w", err)
|
|
}
|
|
filters.MinLatency = &f
|
|
}
|
|
if maxLatency := string(ctx.QueryArgs().Peek("max_latency")); maxLatency != "" {
|
|
val, err := strconv.ParseFloat(maxLatency, 64)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("invalid max_latency format: %w", err)
|
|
}
|
|
filters.MaxLatency = &val
|
|
}
|
|
if contentSearch := string(ctx.QueryArgs().Peek("content_search")); contentSearch != "" {
|
|
filters.ContentSearch = contentSearch
|
|
}
|
|
|
|
// Extract pagination parameters
|
|
pagination.Limit = 50 // Default limit
|
|
if limit := string(ctx.QueryArgs().Peek("limit")); limit != "" {
|
|
i, err := strconv.Atoi(limit)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("invalid limit format: %w", err)
|
|
}
|
|
if i <= 0 {
|
|
return nil, nil, fmt.Errorf("limit must be greater than 0")
|
|
}
|
|
if i > 1000 {
|
|
return nil, nil, fmt.Errorf("limit cannot exceed 1000")
|
|
}
|
|
pagination.Limit = i
|
|
}
|
|
|
|
pagination.Offset = 0 // Default offset
|
|
if offset := string(ctx.QueryArgs().Peek("offset")); offset != "" {
|
|
i, err := strconv.Atoi(offset)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("invalid offset format: %w", err)
|
|
}
|
|
if i < 0 {
|
|
return nil, nil, fmt.Errorf("offset cannot be negative")
|
|
}
|
|
pagination.Offset = i
|
|
}
|
|
|
|
// Sort parameters
|
|
pagination.SortBy = "timestamp" // Default sort field
|
|
if sortBy := string(ctx.QueryArgs().Peek("sort_by")); sortBy != "" {
|
|
if sortBy == "timestamp" || sortBy == "latency" || sortBy == "cost" {
|
|
pagination.SortBy = sortBy
|
|
} else {
|
|
return nil, nil, fmt.Errorf("invalid sort_by: must be 'timestamp', 'latency' or 'cost'")
|
|
}
|
|
}
|
|
|
|
pagination.Order = "desc" // Default sort order
|
|
if order := string(ctx.QueryArgs().Peek("order")); order != "" {
|
|
if order == "asc" || order == "desc" {
|
|
pagination.Order = order
|
|
} else {
|
|
return nil, nil, fmt.Errorf("invalid order: must be 'asc' or 'desc'")
|
|
}
|
|
}
|
|
|
|
return filters, pagination, nil
|
|
}
|
|
|
|
// parseMCPFilters parses MCP tool log filters from query parameters (without pagination).
|
|
// Returns an error if any required parsing fails.
|
|
func parseMCPFilters(ctx *fasthttp.RequestCtx) (*logstore.MCPToolLogSearchFilters, error) {
|
|
filters := &logstore.MCPToolLogSearchFilters{}
|
|
|
|
// Extract filters from query parameters
|
|
if toolNames := string(ctx.QueryArgs().Peek("tool_names")); toolNames != "" {
|
|
filters.ToolNames = parseCommaSeparated(toolNames)
|
|
}
|
|
if serverLabels := string(ctx.QueryArgs().Peek("server_labels")); serverLabels != "" {
|
|
filters.ServerLabels = parseCommaSeparated(serverLabels)
|
|
}
|
|
if statuses := string(ctx.QueryArgs().Peek("status")); statuses != "" {
|
|
filters.Status = parseCommaSeparated(statuses)
|
|
}
|
|
if virtualKeyIDs := string(ctx.QueryArgs().Peek("virtual_key_ids")); virtualKeyIDs != "" {
|
|
filters.VirtualKeyIDs = parseCommaSeparated(virtualKeyIDs)
|
|
}
|
|
if llmRequestIDs := string(ctx.QueryArgs().Peek("llm_request_ids")); llmRequestIDs != "" {
|
|
filters.LLMRequestIDs = parseCommaSeparated(llmRequestIDs)
|
|
}
|
|
if startTime := string(ctx.QueryArgs().Peek("start_time")); startTime != "" {
|
|
t, err := time.Parse(time.RFC3339, startTime)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid start_time format: %w", err)
|
|
}
|
|
filters.StartTime = &t
|
|
}
|
|
if endTime := string(ctx.QueryArgs().Peek("end_time")); endTime != "" {
|
|
t, err := time.Parse(time.RFC3339, endTime)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid end_time format: %w", err)
|
|
}
|
|
filters.EndTime = &t
|
|
}
|
|
if minLatency := string(ctx.QueryArgs().Peek("min_latency")); minLatency != "" {
|
|
f, err := strconv.ParseFloat(minLatency, 64)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid min_latency format: %w", err)
|
|
}
|
|
filters.MinLatency = &f
|
|
}
|
|
if maxLatency := string(ctx.QueryArgs().Peek("max_latency")); maxLatency != "" {
|
|
val, err := strconv.ParseFloat(maxLatency, 64)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid max_latency format: %w", err)
|
|
}
|
|
filters.MaxLatency = &val
|
|
}
|
|
if contentSearch := string(ctx.QueryArgs().Peek("content_search")); contentSearch != "" {
|
|
filters.ContentSearch = contentSearch
|
|
}
|
|
|
|
return filters, nil
|
|
}
|
|
|
|
// ==================== MCP TOOL LOGGING HANDLERS ====================
|
|
|
|
// getMCPLogs handles GET /api/mcp-logs - Get MCP tool logs with filtering, search, and pagination via query parameters
|
|
func (h *LoggingHandler) getMCPLogs(ctx *fasthttp.RequestCtx) {
|
|
filters, pagination, err := parseMCPFiltersAndPagination(ctx)
|
|
if err != nil {
|
|
SendError(ctx, fasthttp.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
|
|
result, err := h.logManager.SearchMCPToolLogs(ctx, filters, pagination)
|
|
if err != nil {
|
|
logger.Error("failed to search MCP tool logs: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("Search failed: %v", err))
|
|
return
|
|
}
|
|
|
|
// Collect unique virtual key IDs from the logs
|
|
virtualKeyIDs := make(map[string]struct{})
|
|
for _, log := range result.Logs {
|
|
if log.VirtualKeyID != nil && *log.VirtualKeyID != "" {
|
|
virtualKeyIDs[*log.VirtualKeyID] = struct{}{}
|
|
}
|
|
}
|
|
|
|
toSlice := func(m map[string]struct{}) []string {
|
|
if len(m) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]string, 0, len(m))
|
|
for id := range m {
|
|
out = append(out, id)
|
|
}
|
|
return out
|
|
}
|
|
|
|
redactedVirtualKeys := h.redactedKeysManager.GetAllRedactedVirtualKeys(ctx, toSlice(virtualKeyIDs))
|
|
|
|
// Add virtual key to the result
|
|
for i, log := range result.Logs {
|
|
if log.VirtualKeyID != nil && log.VirtualKeyName != nil && *log.VirtualKeyID != "" && *log.VirtualKeyName != "" {
|
|
result.Logs[i].VirtualKey = findRedactedVirtualKey(redactedVirtualKeys, *log.VirtualKeyID, *log.VirtualKeyName)
|
|
}
|
|
}
|
|
|
|
SendJSON(ctx, result)
|
|
}
|
|
|
|
// getMCPLogsStats handles GET /api/mcp-logs/stats - Get statistics for MCP tool logs with filtering
|
|
func (h *LoggingHandler) getMCPLogsStats(ctx *fasthttp.RequestCtx) {
|
|
filters, err := parseMCPFilters(ctx)
|
|
if err != nil {
|
|
SendError(ctx, fasthttp.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
|
|
stats, err := h.logManager.GetMCPToolLogStats(ctx, filters)
|
|
if err != nil {
|
|
logger.Error("failed to get MCP tool log stats: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("Stats calculation failed: %v", err))
|
|
return
|
|
}
|
|
|
|
SendJSON(ctx, stats)
|
|
}
|
|
|
|
// getMCPLogsFilterData handles GET /api/mcp-logs/filterdata - Get all unique filter data from MCP tool logs
|
|
func (h *LoggingHandler) getMCPLogsFilterData(ctx *fasthttp.RequestCtx) {
|
|
hideDeletedVirtualKeys := h.shouldHideDeletedVirtualKeysInFilters()
|
|
|
|
toolNames, err := h.logManager.GetAvailableToolNames(ctx)
|
|
if err != nil {
|
|
logger.Error("failed to get available tool names: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("Failed to get available tool names: %v", err))
|
|
return
|
|
}
|
|
|
|
serverLabels, err := h.logManager.GetAvailableServerLabels(ctx)
|
|
if err != nil {
|
|
logger.Error("failed to get available server labels: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("Failed to get available server labels: %v", err))
|
|
return
|
|
}
|
|
|
|
virtualKeys := h.logManager.GetAvailableMCPVirtualKeys(ctx)
|
|
|
|
// Extract IDs for redaction lookup
|
|
virtualKeyIDs := make([]string, len(virtualKeys))
|
|
for i, key := range virtualKeys {
|
|
virtualKeyIDs[i] = key.ID
|
|
}
|
|
|
|
redactedVirtualKeys := make(map[string]tables.TableVirtualKey)
|
|
for _, virtualKey := range h.redactedKeysManager.GetAllRedactedVirtualKeys(ctx, virtualKeyIDs) {
|
|
redactedVirtualKeys[virtualKey.ID] = virtualKey
|
|
}
|
|
|
|
// Check if all virtual key ids are present in the redacted virtual keys (will not be present in case a virtual key is deleted, but we still need to show its filter)
|
|
for _, virtualKey := range virtualKeys {
|
|
if _, ok := redactedVirtualKeys[virtualKey.ID]; !ok {
|
|
if hideDeletedVirtualKeys {
|
|
continue
|
|
}
|
|
// Create a new virtual key struct directly since we know it doesn't exist
|
|
redactedVirtualKeys[virtualKey.ID] = tables.TableVirtualKey{
|
|
ID: virtualKey.ID,
|
|
Name: virtualKey.Name + " (deleted)",
|
|
}
|
|
}
|
|
}
|
|
|
|
// Convert maps to arrays for frontend consumption
|
|
virtualKeysArray := make([]tables.TableVirtualKey, 0, len(redactedVirtualKeys))
|
|
for _, key := range redactedVirtualKeys {
|
|
virtualKeysArray = append(virtualKeysArray, key)
|
|
}
|
|
|
|
SendJSON(ctx, map[string]interface{}{
|
|
"tool_names": toolNames,
|
|
"server_labels": serverLabels,
|
|
"virtual_keys": virtualKeysArray,
|
|
})
|
|
}
|
|
|
|
// deleteMCPLogs handles DELETE /api/mcp-logs - Delete MCP tool logs by their IDs
|
|
func (h *LoggingHandler) deleteMCPLogs(ctx *fasthttp.RequestCtx) {
|
|
var req struct {
|
|
IDs []string `json:"ids"`
|
|
}
|
|
if err := sonic.Unmarshal(ctx.PostBody(), &req); err != nil {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "Invalid JSON")
|
|
return
|
|
}
|
|
|
|
if len(req.IDs) == 0 {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "No log IDs provided")
|
|
return
|
|
}
|
|
|
|
if err := h.logManager.DeleteMCPToolLogs(ctx, req.IDs); err != nil {
|
|
logger.Error("failed to delete MCP tool logs: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, "Failed to delete MCP tool logs")
|
|
return
|
|
}
|
|
|
|
SendJSON(ctx, map[string]interface{}{
|
|
"message": "MCP tool logs deleted successfully",
|
|
})
|
|
}
|
|
|
|
// parseMCPHistogramFilters extracts time range and MCP-specific filters for histogram queries.
|
|
func parseMCPHistogramFilters(ctx *fasthttp.RequestCtx) (*logstore.MCPToolLogSearchFilters, error) {
|
|
return parseMCPFilters(ctx)
|
|
}
|
|
|
|
// getMCPHistogram handles GET /api/mcp-logs/histogram - Get time-bucketed MCP tool call volume
|
|
func (h *LoggingHandler) getMCPHistogram(ctx *fasthttp.RequestCtx) {
|
|
filters, err := parseMCPHistogramFilters(ctx)
|
|
if err != nil {
|
|
SendError(ctx, fasthttp.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
bucketSizeSeconds := calculateBucketSize(filters.StartTime, filters.EndTime)
|
|
|
|
result, err := h.logManager.GetMCPHistogram(ctx, *filters, bucketSizeSeconds)
|
|
if err != nil {
|
|
logger.Error("failed to get MCP histogram: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("MCP histogram calculation failed: %v", err))
|
|
return
|
|
}
|
|
|
|
SendJSON(ctx, result)
|
|
}
|
|
|
|
// getMCPCostHistogram handles GET /api/mcp-logs/histogram/cost - Get time-bucketed MCP cost data
|
|
func (h *LoggingHandler) getMCPCostHistogram(ctx *fasthttp.RequestCtx) {
|
|
filters, err := parseMCPHistogramFilters(ctx)
|
|
if err != nil {
|
|
SendError(ctx, fasthttp.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
bucketSizeSeconds := calculateBucketSize(filters.StartTime, filters.EndTime)
|
|
|
|
result, err := h.logManager.GetMCPCostHistogram(ctx, *filters, bucketSizeSeconds)
|
|
if err != nil {
|
|
logger.Error("failed to get MCP cost histogram: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("MCP cost histogram calculation failed: %v", err))
|
|
return
|
|
}
|
|
|
|
SendJSON(ctx, result)
|
|
}
|
|
|
|
// getMCPTopTools handles GET /api/mcp-logs/histogram/top-tools - Get top 10 MCP tools by call count
|
|
func (h *LoggingHandler) getMCPTopTools(ctx *fasthttp.RequestCtx) {
|
|
filters, err := parseMCPHistogramFilters(ctx)
|
|
if err != nil {
|
|
SendError(ctx, fasthttp.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
|
|
result, err := h.logManager.GetMCPTopTools(ctx, *filters, 10)
|
|
if err != nil {
|
|
logger.Error("failed to get MCP top tools: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("MCP top tools calculation failed: %v", err))
|
|
return
|
|
}
|
|
|
|
SendJSON(ctx, result)
|
|
}
|