mirror of
https://github.com/hanzoai/o11y.git
synced 2026-10-15 12:58:33 +00:00
listErrors and its siblings read a retired table. Claude-Session: https://claude.ai/code/session_011i2AG9nwpiKUCVxAdDzYuU
3996 lines
121 KiB
Go
3996 lines
121 KiB
Go
package app
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
|
|
"github.com/hanzoai/o11y/pkg/errors"
|
|
"github.com/hanzoai/o11y/pkg/flagger"
|
|
"github.com/hanzoai/o11y/pkg/modules/thirdpartyapi"
|
|
"github.com/hanzoai/o11y/pkg/queryparser"
|
|
|
|
"log/slog"
|
|
"math"
|
|
"net/http"
|
|
"regexp"
|
|
"slices"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"text/template"
|
|
"time"
|
|
|
|
"github.com/prometheus/prometheus/promql"
|
|
|
|
"github.com/hanzoai/o11y/pkg/http/render"
|
|
"github.com/hanzoai/o11y/pkg/licensing"
|
|
"github.com/hanzoai/o11y/pkg/o11y"
|
|
"github.com/hanzoai/o11y/pkg/query-service/app/integrations"
|
|
"github.com/hanzoai/o11y/pkg/types/retentiontypes"
|
|
"github.com/hanzoai/o11y/pkg/valuer"
|
|
|
|
"github.com/gorilla/websocket"
|
|
jsoniter "github.com/json-iterator/go"
|
|
|
|
"github.com/hanzoai/o11y/pkg/contextlinks"
|
|
traceFunnelsModule "github.com/hanzoai/o11y/pkg/modules/tracefunnel"
|
|
"github.com/hanzoai/o11y/pkg/query-service/agentConf"
|
|
"github.com/hanzoai/o11y/pkg/query-service/app/inframetrics"
|
|
queues2 "github.com/hanzoai/o11y/pkg/query-service/app/integrations/messagingQueues/queues"
|
|
"github.com/hanzoai/o11y/pkg/query-service/app/logs"
|
|
logsv3 "github.com/hanzoai/o11y/pkg/query-service/app/logs/v3"
|
|
logsv4 "github.com/hanzoai/o11y/pkg/query-service/app/logs/v4"
|
|
"github.com/hanzoai/o11y/pkg/query-service/app/metrics"
|
|
metricsv3 "github.com/hanzoai/o11y/pkg/query-service/app/metrics/v3"
|
|
"github.com/hanzoai/o11y/pkg/query-service/app/querier"
|
|
querierV2 "github.com/hanzoai/o11y/pkg/query-service/app/querier/v2"
|
|
"github.com/hanzoai/o11y/pkg/query-service/app/queryBuilder"
|
|
tracesV3 "github.com/hanzoai/o11y/pkg/query-service/app/traces/v3"
|
|
tracesV4 "github.com/hanzoai/o11y/pkg/query-service/app/traces/v4"
|
|
"github.com/hanzoai/o11y/pkg/query-service/constants"
|
|
v3 "github.com/hanzoai/o11y/pkg/query-service/model/v3"
|
|
"github.com/hanzoai/o11y/pkg/query-service/postprocess"
|
|
"github.com/hanzoai/o11y/pkg/types/authtypes"
|
|
"github.com/hanzoai/o11y/pkg/types/coretypes"
|
|
"github.com/hanzoai/o11y/pkg/types/ctxtypes"
|
|
"github.com/hanzoai/o11y/pkg/types/featuretypes"
|
|
"github.com/hanzoai/o11y/pkg/types/instrumentationtypes"
|
|
"github.com/hanzoai/o11y/pkg/types/opamptypes"
|
|
"github.com/hanzoai/o11y/pkg/types/pipelinetypes"
|
|
qbtypes "github.com/hanzoai/o11y/pkg/types/querybuildertypes/querybuildertypesv5"
|
|
"github.com/hanzoai/o11y/pkg/types/ruletypes"
|
|
traceFunnels "github.com/hanzoai/o11y/pkg/types/tracefunneltypes"
|
|
|
|
"github.com/hanzoai/o11y/pkg/query-service/app/integrations/messagingQueues/kafka"
|
|
"github.com/hanzoai/o11y/pkg/query-service/app/logparsingpipeline"
|
|
"github.com/hanzoai/o11y/pkg/query-service/interfaces"
|
|
"github.com/hanzoai/o11y/pkg/query-service/model"
|
|
"github.com/hanzoai/o11y/pkg/ruler"
|
|
"github.com/hanzoai/o11y/pkg/version"
|
|
)
|
|
|
|
type status string
|
|
|
|
const (
|
|
statusSuccess status = "success"
|
|
statusError status = "error"
|
|
)
|
|
|
|
// APIHandler implements the query service public API
|
|
type APIHandler struct {
|
|
logger *slog.Logger
|
|
reader interfaces.Reader
|
|
ruleManager ruler.Ruler
|
|
querier interfaces.Querier
|
|
querierV2 interfaces.Querier
|
|
queryBuilder *queryBuilder.QueryBuilder
|
|
|
|
// temporalityMap is a map of metric name to temporality to avoid fetching
|
|
// temporality for the same metric multiple times.
|
|
//
|
|
// Querying the v4 table on a low cardinal temporality column should be
|
|
// fast, but we can still avoid the query if we have the data in memory.
|
|
temporalityMap map[string]map[v3.Temporality]bool
|
|
temporalityMux sync.Mutex
|
|
|
|
IntegrationsController *integrations.Controller
|
|
|
|
LogsParsingPipelineController *logparsingpipeline.LogParsingPipelineController
|
|
|
|
// SetupCompleted indicates if O11y is ready for general use.
|
|
// at the moment, we mark the app ready when the first user
|
|
// is registers.
|
|
SetupCompleted bool
|
|
|
|
// Websocket connection upgrader
|
|
Upgrader *websocket.Upgrader
|
|
|
|
hostsRepo *inframetrics.HostsRepo
|
|
processesRepo *inframetrics.ProcessesRepo
|
|
podsRepo *inframetrics.PodsRepo
|
|
nodesRepo *inframetrics.NodesRepo
|
|
namespacesRepo *inframetrics.NamespacesRepo
|
|
clustersRepo *inframetrics.ClustersRepo
|
|
// workloads
|
|
deploymentsRepo *inframetrics.DeploymentsRepo
|
|
daemonsetsRepo *inframetrics.DaemonSetsRepo
|
|
statefulsetsRepo *inframetrics.StatefulSetsRepo
|
|
jobsRepo *inframetrics.JobsRepo
|
|
|
|
pvcsRepo *inframetrics.PvcsRepo
|
|
|
|
LicensingAPI licensing.API
|
|
|
|
QueryParserAPI *queryparser.API
|
|
|
|
O11y *o11y.O11y
|
|
}
|
|
|
|
type APIHandlerOpts struct {
|
|
// business data reader e.g. datastore
|
|
Reader interfaces.Reader
|
|
|
|
// Integrations
|
|
IntegrationsController *integrations.Controller
|
|
|
|
// Log parsing pipelines
|
|
LogsParsingPipelineController *logparsingpipeline.LogParsingPipelineController
|
|
|
|
// Flux Interval
|
|
FluxInterval time.Duration
|
|
|
|
LicensingAPI licensing.API
|
|
|
|
QueryParserAPI *queryparser.API
|
|
|
|
O11y *o11y.O11y
|
|
}
|
|
|
|
// NewAPIHandler returns an APIHandler
|
|
func NewAPIHandler(opts APIHandlerOpts, config o11y.Config) (*APIHandler, error) {
|
|
querierOpts := querier.QuerierOptions{
|
|
Reader: opts.Reader,
|
|
Cache: opts.O11y.Cache,
|
|
KeyGenerator: queryBuilder.NewKeyGenerator(),
|
|
FluxInterval: opts.FluxInterval,
|
|
}
|
|
|
|
querierOptsV2 := querierV2.QuerierOptions{
|
|
Reader: opts.Reader,
|
|
Cache: opts.O11y.Cache,
|
|
KeyGenerator: queryBuilder.NewKeyGenerator(),
|
|
FluxInterval: opts.FluxInterval,
|
|
}
|
|
|
|
querier := querier.NewQuerier(querierOpts)
|
|
querierv2 := querierV2.NewQuerier(querierOptsV2)
|
|
|
|
hostsRepo := inframetrics.NewHostsRepo(opts.Reader, querierv2)
|
|
processesRepo := inframetrics.NewProcessesRepo(opts.Reader, querierv2)
|
|
podsRepo := inframetrics.NewPodsRepo(opts.Reader, querierv2)
|
|
nodesRepo := inframetrics.NewNodesRepo(opts.Reader, querierv2)
|
|
namespacesRepo := inframetrics.NewNamespacesRepo(opts.Reader, querierv2)
|
|
clustersRepo := inframetrics.NewClustersRepo(opts.Reader, querierv2)
|
|
deploymentsRepo := inframetrics.NewDeploymentsRepo(opts.Reader, querierv2)
|
|
daemonsetsRepo := inframetrics.NewDaemonSetsRepo(opts.Reader, querierv2)
|
|
statefulsetsRepo := inframetrics.NewStatefulSetsRepo(opts.Reader, querierv2)
|
|
jobsRepo := inframetrics.NewJobsRepo(opts.Reader, querierv2)
|
|
pvcsRepo := inframetrics.NewPvcsRepo(opts.Reader, querierv2)
|
|
//quickFilterModule := quickfilter.NewAPI(opts.QuickFilterModule)
|
|
|
|
aH := &APIHandler{
|
|
logger: slog.Default(),
|
|
reader: opts.Reader,
|
|
temporalityMap: make(map[string]map[v3.Temporality]bool),
|
|
ruleManager: opts.O11y.Ruler,
|
|
IntegrationsController: opts.IntegrationsController,
|
|
LogsParsingPipelineController: opts.LogsParsingPipelineController,
|
|
querier: querier,
|
|
querierV2: querierv2,
|
|
hostsRepo: hostsRepo,
|
|
processesRepo: processesRepo,
|
|
podsRepo: podsRepo,
|
|
nodesRepo: nodesRepo,
|
|
namespacesRepo: namespacesRepo,
|
|
clustersRepo: clustersRepo,
|
|
deploymentsRepo: deploymentsRepo,
|
|
daemonsetsRepo: daemonsetsRepo,
|
|
statefulsetsRepo: statefulsetsRepo,
|
|
jobsRepo: jobsRepo,
|
|
pvcsRepo: pvcsRepo,
|
|
LicensingAPI: opts.LicensingAPI,
|
|
O11y: opts.O11y,
|
|
QueryParserAPI: opts.QueryParserAPI,
|
|
}
|
|
|
|
logsQueryBuilder := logsv4.PrepareLogsQuery
|
|
tracesQueryBuilder := tracesV4.PrepareTracesQuery
|
|
|
|
builderOpts := queryBuilder.QueryBuilderOptions{
|
|
BuildMetricQuery: metricsv3.PrepareMetricQuery,
|
|
BuildTraceQuery: tracesQueryBuilder,
|
|
BuildLogQuery: logsQueryBuilder,
|
|
}
|
|
aH.queryBuilder = queryBuilder.NewQueryBuilder(builderOpts)
|
|
|
|
// Zero-onboarding: o11y holds no native identity or first-run setup. Identity
|
|
// is asserted by the Hanzo IAM session (pkg/identn/iamidentn) and the tenant
|
|
// org is auto-provisioned on first request, so o11y is always "set up" — there
|
|
// is no first-user/registration gate and the data APIs are on from boot.
|
|
aH.SetupCompleted = true
|
|
|
|
aH.Upgrader = &websocket.Upgrader{
|
|
CheckOrigin: func(r *http.Request) bool {
|
|
return true
|
|
},
|
|
}
|
|
|
|
return aH, nil
|
|
}
|
|
|
|
// todo(remove): Implemented at render package (github.com/hanzoai/o11y/pkg/http/render) with the new error structure
|
|
type structuredResponse struct {
|
|
Data interface{} `json:"data"`
|
|
Total int `json:"total"`
|
|
Limit int `json:"limit"`
|
|
Offset int `json:"offset"`
|
|
Errors []structuredError `json:"errors"`
|
|
}
|
|
|
|
// todo(remove): Implemented at render package (github.com/hanzoai/o11y/pkg/http/render) with the new error structure
|
|
type structuredError struct {
|
|
Code int `json:"code,omitempty"`
|
|
Msg string `json:"msg"`
|
|
}
|
|
|
|
// todo(remove): Implemented at render package (github.com/hanzoai/o11y/pkg/http/render) with the new error structure
|
|
type ApiResponse struct {
|
|
Status status `json:"status"`
|
|
Data interface{} `json:"data,omitempty"`
|
|
ErrorType model.ErrorType `json:"errorType,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
// toApiError translates a pkg/errors typed error into the legacy
|
|
// model.ApiError to preserve the v1 JSON response shape.
|
|
func toApiError(err error) *model.ApiError {
|
|
t, _, _, _, _, _ := errors.Unwrapb(err)
|
|
|
|
var typ model.ErrorType
|
|
switch t {
|
|
case errors.TypeInvalidInput:
|
|
typ = model.ErrorBadData
|
|
case errors.TypeNotFound:
|
|
typ = model.ErrorNotFound
|
|
case errors.TypeAlreadyExists:
|
|
typ = model.ErrorConflict
|
|
case errors.TypeUnauthenticated:
|
|
typ = model.ErrorUnauthorized
|
|
case errors.TypeForbidden:
|
|
typ = model.ErrorForbidden
|
|
case errors.TypeUnsupported:
|
|
typ = model.ErrorNotImplemented
|
|
case errors.TypeTimeout:
|
|
typ = model.ErrorTimeout
|
|
case errors.TypeCanceled:
|
|
typ = model.ErrorCanceled
|
|
default:
|
|
typ = model.ErrorInternal
|
|
}
|
|
|
|
return &model.ApiError{Typ: typ, Err: err}
|
|
}
|
|
|
|
// todo(remove): Implemented at render package (github.com/hanzoai/o11y/pkg/http/render) with the new error structure
|
|
func RespondError(w http.ResponseWriter, apiErr model.BaseApiError, data interface{}) {
|
|
json := jsoniter.ConfigCompatibleWithStandardLibrary
|
|
b, err := json.Marshal(&ApiResponse{
|
|
Status: statusError,
|
|
ErrorType: apiErr.Type(),
|
|
Error: apiErr.Error(),
|
|
Data: data,
|
|
})
|
|
if err != nil {
|
|
slog.Error("error marshalling json response", errors.Attr(err))
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
var code int
|
|
switch apiErr.Type() {
|
|
case model.ErrorBadData:
|
|
code = http.StatusBadRequest
|
|
case model.ErrorExec:
|
|
code = 422
|
|
case model.ErrorCanceled, model.ErrorTimeout:
|
|
code = http.StatusServiceUnavailable
|
|
case model.ErrorInternal:
|
|
code = http.StatusInternalServerError
|
|
case model.ErrorNotFound:
|
|
code = http.StatusNotFound
|
|
case model.ErrorNotImplemented:
|
|
code = http.StatusNotImplemented
|
|
case model.ErrorUnauthorized:
|
|
code = http.StatusUnauthorized
|
|
case model.ErrorForbidden:
|
|
code = http.StatusForbidden
|
|
case model.ErrorConflict:
|
|
code = http.StatusConflict
|
|
default:
|
|
code = http.StatusInternalServerError
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(code)
|
|
if n, err := w.Write(b); err != nil {
|
|
slog.Error("error writing response", "bytes_written", n, errors.Attr(err))
|
|
}
|
|
}
|
|
|
|
// todo(remove): Implemented at render package (github.com/hanzoai/o11y/pkg/http/render) with the new error structure
|
|
func writeHttpResponse(w http.ResponseWriter, data interface{}) {
|
|
json := jsoniter.ConfigCompatibleWithStandardLibrary
|
|
b, err := json.Marshal(&ApiResponse{
|
|
Status: statusSuccess,
|
|
Data: data,
|
|
})
|
|
if err != nil {
|
|
slog.Error("error marshalling json response", errors.Attr(err))
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
if n, err := w.Write(b); err != nil {
|
|
slog.Error("error writing response", "bytes_written", n, errors.Attr(err))
|
|
}
|
|
}
|
|
|
|
// todo(remove): Implemented at render package (github.com/hanzoai/o11y/pkg/http/render) with the new error structure
|
|
func (aH *APIHandler) Respond(w http.ResponseWriter, data interface{}) {
|
|
writeHttpResponse(w, data)
|
|
}
|
|
|
|
func Intersection(a, b []int) (c []int) {
|
|
m := make(map[int]bool)
|
|
|
|
for _, item := range a {
|
|
m[item] = true
|
|
}
|
|
|
|
for _, item := range b {
|
|
if _, ok := m[item]; ok {
|
|
c = append(c, item)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// populateTemporality adds the temporality to the query if it is not present
|
|
func (aH *APIHandler) PopulateTemporality(ctx context.Context, orgID valuer.UUID, qp *v3.QueryRangeParamsV3) error {
|
|
|
|
aH.temporalityMux.Lock()
|
|
defer aH.temporalityMux.Unlock()
|
|
|
|
missingTemporality := make([]string, 0)
|
|
metricNameToTemporality := make(map[string]map[v3.Temporality]bool)
|
|
if qp.CompositeQuery != nil && len(qp.CompositeQuery.BuilderQueries) > 0 {
|
|
for _, query := range qp.CompositeQuery.BuilderQueries {
|
|
// if there is no temporality specified in the query but we have it in the map
|
|
// then use the value from the map
|
|
if query.Temporality == "" && aH.temporalityMap[query.AggregateAttribute.Key] != nil {
|
|
// We prefer delta if it is available
|
|
if aH.temporalityMap[query.AggregateAttribute.Key][v3.Delta] {
|
|
query.Temporality = v3.Delta
|
|
} else if aH.temporalityMap[query.AggregateAttribute.Key][v3.Cumulative] {
|
|
query.Temporality = v3.Cumulative
|
|
} else {
|
|
query.Temporality = v3.Unspecified
|
|
}
|
|
}
|
|
// we don't have temporality for this metric
|
|
if query.DataSource == v3.DataSourceMetrics && query.Temporality == "" {
|
|
missingTemporality = append(missingTemporality, query.AggregateAttribute.Key)
|
|
}
|
|
if _, ok := metricNameToTemporality[query.AggregateAttribute.Key]; !ok {
|
|
metricNameToTemporality[query.AggregateAttribute.Key] = make(map[v3.Temporality]bool)
|
|
}
|
|
}
|
|
}
|
|
|
|
nameToTemporality, err := aH.reader.FetchTemporality(ctx, orgID, missingTemporality)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if qp.CompositeQuery != nil && len(qp.CompositeQuery.BuilderQueries) > 0 {
|
|
for name := range qp.CompositeQuery.BuilderQueries {
|
|
query := qp.CompositeQuery.BuilderQueries[name]
|
|
if query.DataSource == v3.DataSourceMetrics && query.Temporality == "" {
|
|
if nameToTemporality[query.AggregateAttribute.Key][v3.Delta] {
|
|
query.Temporality = v3.Delta
|
|
} else if nameToTemporality[query.AggregateAttribute.Key][v3.Cumulative] {
|
|
query.Temporality = v3.Cumulative
|
|
} else {
|
|
query.Temporality = v3.Unspecified
|
|
}
|
|
aH.temporalityMap[query.AggregateAttribute.Key] = nameToTemporality[query.AggregateAttribute.Key]
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (aH *APIHandler) testRule(w http.ResponseWriter, r *http.Request) {
|
|
claims, err := authtypes.ClaimsFromContext(r.Context())
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
orgID, err := valuer.NewUUID(claims.OrgID)
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
defer r.Body.Close()
|
|
body, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
aH.logger.ErrorContext(r.Context(), "error reading request body for test rule", errors.Attr(err))
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
|
|
return
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
|
|
defer cancel()
|
|
|
|
alertCount, err := aH.ruleManager.TestNotification(ctx, orgID, string(body))
|
|
if err != nil {
|
|
RespondError(w, toApiError(err), nil)
|
|
return
|
|
}
|
|
|
|
response := map[string]interface{}{
|
|
"alertCount": alertCount,
|
|
"message": "notification sent",
|
|
}
|
|
aH.Respond(w, response)
|
|
}
|
|
|
|
func (aH *APIHandler) getRuleStats(w http.ResponseWriter, r *http.Request) {
|
|
ruleID := coretypes.Param(r, "id")
|
|
params := model.QueryRuleStateHistory{}
|
|
err := json.NewDecoder(r.Body).Decode(¶ms)
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
|
|
return
|
|
}
|
|
|
|
totalCurrentTriggers, err := aH.reader.GetTotalTriggers(r.Context(), ruleID, ¶ms)
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
|
|
return
|
|
}
|
|
currentTriggersSeries, err := aH.reader.GetTriggersByInterval(r.Context(), ruleID, ¶ms)
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
|
|
return
|
|
}
|
|
|
|
currentAvgResolutionTime, err := aH.reader.GetAvgResolutionTime(r.Context(), ruleID, ¶ms)
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
|
|
return
|
|
}
|
|
currentAvgResolutionTimeSeries, err := aH.reader.GetAvgResolutionTimeByInterval(r.Context(), ruleID, ¶ms)
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
|
|
return
|
|
}
|
|
|
|
if params.End-params.Start >= 86400000 {
|
|
days := int64(math.Ceil(float64(params.End-params.Start) / 86400000))
|
|
params.Start -= days * 86400000
|
|
params.End -= days * 86400000
|
|
} else {
|
|
params.Start -= 86400000
|
|
params.End -= 86400000
|
|
}
|
|
|
|
totalPastTriggers, err := aH.reader.GetTotalTriggers(r.Context(), ruleID, ¶ms)
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
|
|
return
|
|
}
|
|
pastTriggersSeries, err := aH.reader.GetTriggersByInterval(r.Context(), ruleID, ¶ms)
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
|
|
return
|
|
}
|
|
|
|
pastAvgResolutionTime, err := aH.reader.GetAvgResolutionTime(r.Context(), ruleID, ¶ms)
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
|
|
return
|
|
}
|
|
pastAvgResolutionTimeSeries, err := aH.reader.GetAvgResolutionTimeByInterval(r.Context(), ruleID, ¶ms)
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
|
|
return
|
|
}
|
|
if math.IsNaN(currentAvgResolutionTime) || math.IsInf(currentAvgResolutionTime, 0) {
|
|
currentAvgResolutionTime = 0
|
|
}
|
|
if math.IsNaN(pastAvgResolutionTime) || math.IsInf(pastAvgResolutionTime, 0) {
|
|
pastAvgResolutionTime = 0
|
|
}
|
|
|
|
stats := model.Stats{
|
|
TotalCurrentTriggers: totalCurrentTriggers,
|
|
TotalPastTriggers: totalPastTriggers,
|
|
CurrentTriggersSeries: currentTriggersSeries,
|
|
PastTriggersSeries: pastTriggersSeries,
|
|
CurrentAvgResolutionTime: strconv.FormatFloat(currentAvgResolutionTime, 'f', -1, 64),
|
|
PastAvgResolutionTime: strconv.FormatFloat(pastAvgResolutionTime, 'f', -1, 64),
|
|
CurrentAvgResolutionTimeSeries: currentAvgResolutionTimeSeries,
|
|
PastAvgResolutionTimeSeries: pastAvgResolutionTimeSeries,
|
|
}
|
|
|
|
aH.Respond(w, stats)
|
|
}
|
|
|
|
func (aH *APIHandler) getOverallStateTransitions(w http.ResponseWriter, r *http.Request) {
|
|
ruleID := coretypes.Param(r, "id")
|
|
params := model.QueryRuleStateHistory{}
|
|
err := json.NewDecoder(r.Body).Decode(¶ms)
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
|
|
return
|
|
}
|
|
|
|
stateItems, err := aH.reader.GetOverallStateTransitions(r.Context(), ruleID, ¶ms)
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
|
|
return
|
|
}
|
|
|
|
aH.Respond(w, stateItems)
|
|
}
|
|
|
|
func (aH *APIHandler) getRuleStateHistory(w http.ResponseWriter, r *http.Request) {
|
|
idStr := coretypes.Param(r, "id")
|
|
id, err := valuer.NewUUID(idStr)
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
|
|
return
|
|
}
|
|
|
|
params := model.QueryRuleStateHistory{}
|
|
err = json.NewDecoder(r.Body).Decode(¶ms)
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
|
|
return
|
|
}
|
|
if err := params.Validate(); err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
|
|
return
|
|
}
|
|
|
|
res, err := aH.reader.ReadRuleStateHistoryByRuleID(r.Context(), id.StringValue(), ¶ms)
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
|
|
return
|
|
}
|
|
|
|
rule, err := aH.ruleManager.GetRule(r.Context(), id)
|
|
if err == nil {
|
|
for idx := range res.Items {
|
|
lbls := make(map[string]string)
|
|
err := json.Unmarshal([]byte(res.Items[idx].Labels), &lbls)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
end := time.Unix(res.Items[idx].UnixMilli/1000, 0)
|
|
// why are we subtracting 3 minutes?
|
|
// the query range is calculated based on the rule's evalWindow and evalDelay
|
|
// alerts have 2 minutes delay built in, so we need to subtract that from the start time
|
|
// to get the correct query range
|
|
start := end.Add(-rule.EvalWindow.Duration() - 3*time.Minute)
|
|
if rule.AlertType == ruletypes.AlertTypeLogs {
|
|
// TODO(srikanthccv): re-visit this and support multiple queries
|
|
var q qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]
|
|
|
|
for _, query := range rule.RuleCondition.CompositeQuery.Queries {
|
|
if query.Type == qbtypes.QueryTypeBuilder {
|
|
switch spec := query.Spec.(type) {
|
|
case qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]:
|
|
q = spec
|
|
}
|
|
}
|
|
}
|
|
|
|
filterExpr := ""
|
|
if q.Filter != nil && q.Filter.Expression != "" {
|
|
filterExpr = q.Filter.Expression
|
|
}
|
|
|
|
whereClause := contextlinks.PrepareFilterExpression(lbls, filterExpr, q.GroupBy)
|
|
|
|
res.Items[idx].RelatedLogsLink = contextlinks.PrepareParamsForLogsV5(start, end, whereClause).Encode()
|
|
} else if rule.AlertType == ruletypes.AlertTypeTraces {
|
|
// TODO(srikanthccv): re-visit this and support multiple queries
|
|
var q qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]
|
|
|
|
for _, query := range rule.RuleCondition.CompositeQuery.Queries {
|
|
if query.Type == qbtypes.QueryTypeBuilder {
|
|
switch spec := query.Spec.(type) {
|
|
case qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]:
|
|
q = spec
|
|
}
|
|
}
|
|
}
|
|
|
|
filterExpr := ""
|
|
if q.Filter != nil && q.Filter.Expression != "" {
|
|
filterExpr = q.Filter.Expression
|
|
}
|
|
|
|
whereClause := contextlinks.PrepareFilterExpression(lbls, filterExpr, q.GroupBy)
|
|
res.Items[idx].RelatedTracesLink = contextlinks.PrepareParamsForTracesV5(start, end, whereClause).Encode()
|
|
}
|
|
}
|
|
}
|
|
|
|
aH.Respond(w, res)
|
|
}
|
|
|
|
func (aH *APIHandler) getRuleStateHistoryTopContributors(w http.ResponseWriter, r *http.Request) {
|
|
idStr := coretypes.Param(r, "id")
|
|
id, err := valuer.NewUUID(idStr)
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
|
|
return
|
|
}
|
|
|
|
params := model.QueryRuleStateHistory{}
|
|
err = json.NewDecoder(r.Body).Decode(¶ms)
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
|
|
return
|
|
}
|
|
|
|
res, err := aH.reader.ReadRuleStateHistoryTopContributorsByRuleID(r.Context(), id.StringValue(), ¶ms)
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
|
|
return
|
|
}
|
|
|
|
aH.Respond(w, res)
|
|
}
|
|
|
|
func prepareQuery(r *http.Request) (string, error) {
|
|
var postData *model.DashboardVars
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&postData); err != nil {
|
|
return "", fmt.Errorf("failed to decode request body: %v", err)
|
|
}
|
|
|
|
query := strings.TrimSpace(postData.Query)
|
|
|
|
if query == "" {
|
|
return "", fmt.Errorf("query is required")
|
|
}
|
|
|
|
notAllowedOps := []string{
|
|
"alter table",
|
|
"drop table",
|
|
"truncate table",
|
|
"drop database",
|
|
"drop view",
|
|
"drop function",
|
|
}
|
|
|
|
for _, op := range notAllowedOps {
|
|
if strings.Contains(strings.ToLower(query), op) {
|
|
return "", fmt.Errorf("operation %s is not allowed", op)
|
|
}
|
|
}
|
|
|
|
vars := make(map[string]string)
|
|
for k, v := range postData.Variables {
|
|
vars[k] = metrics.FormattedValue(v)
|
|
}
|
|
tmpl := template.New("dashboard-vars")
|
|
tmpl, tmplErr := tmpl.Parse(query)
|
|
if tmplErr != nil {
|
|
return "", tmplErr
|
|
}
|
|
var queryBuf bytes.Buffer
|
|
tmplErr = tmpl.Execute(&queryBuf, vars)
|
|
if tmplErr != nil {
|
|
return "", tmplErr
|
|
}
|
|
|
|
if !constants.IsDotMetricsEnabled {
|
|
return queryBuf.String(), nil
|
|
}
|
|
|
|
query = queryBuf.String()
|
|
|
|
// Now handle $var replacements (simple string replace)
|
|
keys := make([]string, 0, len(vars))
|
|
for k := range vars {
|
|
keys = append(keys, k)
|
|
}
|
|
|
|
sort.Slice(keys, func(i, j int) bool {
|
|
return len(keys[i]) > len(keys[j])
|
|
})
|
|
|
|
newQuery := query
|
|
for _, k := range keys {
|
|
placeholder := "$" + k
|
|
v := vars[k]
|
|
newQuery = strings.ReplaceAll(newQuery, placeholder, v)
|
|
}
|
|
|
|
return newQuery, nil
|
|
}
|
|
|
|
func (aH *APIHandler) queryDashboardVarsV2(w http.ResponseWriter, r *http.Request) {
|
|
query, err := prepareQuery(r)
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
|
|
return
|
|
}
|
|
|
|
dashboardVars, err := aH.reader.QueryDashboardVars(r.Context(), query)
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
|
|
return
|
|
}
|
|
aH.Respond(w, dashboardVars)
|
|
}
|
|
|
|
func (aH *APIHandler) queryRangeMetrics(w http.ResponseWriter, r *http.Request) {
|
|
|
|
query, apiErrorObj := parseQueryRangeRequest(r)
|
|
|
|
if apiErrorObj != nil {
|
|
RespondError(w, apiErrorObj, nil)
|
|
return
|
|
}
|
|
|
|
// TODO: add structured logging for query and apiError if needed
|
|
|
|
ctx := r.Context()
|
|
if to := r.FormValue("timeout"); to != "" {
|
|
var cancel context.CancelFunc
|
|
timeout, err := parseMetricsDuration(to)
|
|
if aH.HandleError(w, err, http.StatusBadRequest) {
|
|
return
|
|
}
|
|
|
|
ctx, cancel = context.WithTimeout(ctx, timeout)
|
|
defer cancel()
|
|
}
|
|
|
|
res, qs, apiError := aH.reader.GetQueryRangeResult(ctx, query)
|
|
|
|
if apiError != nil {
|
|
RespondError(w, apiError, nil)
|
|
return
|
|
}
|
|
|
|
if res.Err != nil {
|
|
aH.logger.ErrorContext(r.Context(), "error in query range metrics", errors.Attr(res.Err))
|
|
}
|
|
|
|
if res.Err != nil {
|
|
switch res.Err.(type) {
|
|
case promql.ErrQueryCanceled:
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorCanceled, Err: res.Err}, nil)
|
|
case promql.ErrQueryTimeout:
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorTimeout, Err: res.Err}, nil)
|
|
}
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorExec, Err: res.Err}, nil)
|
|
return
|
|
}
|
|
|
|
response_data := &model.QueryData{
|
|
ResultType: res.Value.Type(),
|
|
Result: res.Value,
|
|
Stats: qs,
|
|
}
|
|
|
|
aH.Respond(w, response_data)
|
|
|
|
}
|
|
|
|
func (aH *APIHandler) queryMetrics(w http.ResponseWriter, r *http.Request) {
|
|
|
|
queryParams, apiErrorObj := parseInstantQueryMetricsRequest(r)
|
|
|
|
if apiErrorObj != nil {
|
|
RespondError(w, apiErrorObj, nil)
|
|
return
|
|
}
|
|
|
|
// TODO: add structured logging for query and apiError if needed
|
|
|
|
ctx := r.Context()
|
|
if to := r.FormValue("timeout"); to != "" {
|
|
var cancel context.CancelFunc
|
|
timeout, err := parseMetricsDuration(to)
|
|
if aH.HandleError(w, err, http.StatusBadRequest) {
|
|
return
|
|
}
|
|
|
|
ctx, cancel = context.WithTimeout(ctx, timeout)
|
|
defer cancel()
|
|
}
|
|
|
|
res, qs, apiError := aH.reader.GetInstantQueryMetricsResult(ctx, queryParams)
|
|
|
|
if apiError != nil {
|
|
RespondError(w, apiError, nil)
|
|
return
|
|
}
|
|
|
|
if res.Err != nil {
|
|
aH.logger.ErrorContext(r.Context(), "error in query range metrics", errors.Attr(res.Err))
|
|
}
|
|
|
|
if res.Err != nil {
|
|
switch res.Err.(type) {
|
|
case promql.ErrQueryCanceled:
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorCanceled, Err: res.Err}, nil)
|
|
case promql.ErrQueryTimeout:
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorTimeout, Err: res.Err}, nil)
|
|
}
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorExec, Err: res.Err}, nil)
|
|
}
|
|
|
|
responseData := &model.QueryData{
|
|
ResultType: res.Value.Type(),
|
|
Result: res.Value,
|
|
Stats: qs,
|
|
}
|
|
|
|
aH.Respond(w, responseData)
|
|
|
|
}
|
|
|
|
func (aH *APIHandler) registerEvent(w http.ResponseWriter, r *http.Request) {
|
|
request, err := parseRegisterEventRequest(r)
|
|
if aH.HandleError(w, err, http.StatusBadRequest) {
|
|
return
|
|
}
|
|
claims, errv2 := authtypes.ClaimsFromContext(r.Context())
|
|
if errv2 == nil {
|
|
switch request.EventType {
|
|
case model.TrackEvent:
|
|
aH.O11y.Analytics.TrackUser(r.Context(), claims.OrgID, claims.IdentityID(), request.EventName, request.Attributes)
|
|
}
|
|
aH.WriteJSON(w, r, map[string]string{"data": "Event Processed Successfully"})
|
|
} else {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
|
|
}
|
|
}
|
|
|
|
func (aH *APIHandler) getUsage(w http.ResponseWriter, r *http.Request) {
|
|
|
|
query, err := parseGetUsageRequest(r)
|
|
if aH.HandleError(w, err, http.StatusBadRequest) {
|
|
return
|
|
}
|
|
|
|
result, err := aH.reader.GetUsage(r.Context(), query)
|
|
if aH.HandleError(w, err, http.StatusBadRequest) {
|
|
return
|
|
}
|
|
|
|
aH.WriteJSON(w, r, result)
|
|
|
|
}
|
|
|
|
func (aH *APIHandler) getServicesTopLevelOps(w http.ResponseWriter, r *http.Request) {
|
|
|
|
var start, end time.Time
|
|
var services []string
|
|
|
|
type topLevelOpsParams struct {
|
|
Service string `json:"service"`
|
|
Start string `json:"start"`
|
|
End string `json:"end"`
|
|
}
|
|
|
|
var params topLevelOpsParams
|
|
err := json.NewDecoder(r.Body).Decode(¶ms)
|
|
if err != nil {
|
|
aH.logger.ErrorContext(r.Context(), "error reading request body for get top operations", errors.Attr(err))
|
|
}
|
|
|
|
if params.Service != "" {
|
|
services = []string{params.Service}
|
|
}
|
|
|
|
startEpoch := params.Start
|
|
if startEpoch != "" {
|
|
startEpochInt, err := strconv.ParseInt(startEpoch, 10, 64)
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, "Error reading start time")
|
|
return
|
|
}
|
|
start = time.Unix(0, startEpochInt)
|
|
}
|
|
endEpoch := params.End
|
|
if endEpoch != "" {
|
|
endEpochInt, err := strconv.ParseInt(endEpoch, 10, 64)
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, "Error reading end time")
|
|
return
|
|
}
|
|
end = time.Unix(0, endEpochInt)
|
|
}
|
|
|
|
result, apiErr := aH.reader.GetTopLevelOperations(r.Context(), start, end, services)
|
|
if apiErr != nil {
|
|
RespondError(w, apiErr, nil)
|
|
return
|
|
}
|
|
|
|
aH.WriteJSON(w, r, result)
|
|
}
|
|
|
|
func (aH *APIHandler) dependencyGraph(w http.ResponseWriter, r *http.Request) {
|
|
|
|
query, err := parseGetServicesRequest(r)
|
|
if aH.HandleError(w, err, http.StatusBadRequest) {
|
|
return
|
|
}
|
|
|
|
result, err := aH.reader.GetDependencyGraph(r.Context(), query)
|
|
if aH.HandleError(w, err, http.StatusBadRequest) {
|
|
return
|
|
}
|
|
|
|
aH.WriteJSON(w, r, result)
|
|
}
|
|
|
|
func (aH *APIHandler) getServicesList(w http.ResponseWriter, r *http.Request) {
|
|
|
|
result, err := aH.reader.GetServicesList(r.Context())
|
|
if aH.HandleError(w, err, http.StatusBadRequest) {
|
|
return
|
|
}
|
|
|
|
aH.WriteJSON(w, r, result)
|
|
|
|
}
|
|
|
|
func (aH *APIHandler) SearchTraces(w http.ResponseWriter, r *http.Request) {
|
|
params, err := ParseSearchTracesParams(r)
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, "Error reading params")
|
|
return
|
|
}
|
|
|
|
result, err := aH.reader.SearchTraces(r.Context(), params)
|
|
if aH.HandleError(w, err, http.StatusBadRequest) {
|
|
return
|
|
}
|
|
|
|
aH.WriteJSON(w, r, result)
|
|
|
|
}
|
|
|
|
func (aH *APIHandler) listErrors(w http.ResponseWriter, r *http.Request) {
|
|
|
|
query, err := parseListErrorsRequest(r)
|
|
if aH.HandleError(w, err, http.StatusBadRequest) {
|
|
return
|
|
}
|
|
result, apiErr := aH.reader.ListErrors(r.Context(), query)
|
|
if apiErr != nil {
|
|
RespondError(w, apiErr, nil)
|
|
return
|
|
}
|
|
|
|
aH.WriteJSON(w, r, result)
|
|
}
|
|
|
|
func (aH *APIHandler) countErrors(w http.ResponseWriter, r *http.Request) {
|
|
|
|
query, err := parseCountErrorsRequest(r)
|
|
if aH.HandleError(w, err, http.StatusBadRequest) {
|
|
return
|
|
}
|
|
result, apiErr := aH.reader.CountErrors(r.Context(), query)
|
|
if apiErr != nil {
|
|
RespondError(w, apiErr, nil)
|
|
return
|
|
}
|
|
|
|
aH.WriteJSON(w, r, result)
|
|
}
|
|
|
|
func (aH *APIHandler) getErrorFromErrorID(w http.ResponseWriter, r *http.Request) {
|
|
|
|
query, err := parseGetErrorRequest(r)
|
|
if aH.HandleError(w, err, http.StatusBadRequest) {
|
|
return
|
|
}
|
|
result, apiErr := aH.reader.GetErrorFromErrorID(r.Context(), query)
|
|
if apiErr != nil {
|
|
RespondError(w, apiErr, nil)
|
|
return
|
|
}
|
|
|
|
aH.WriteJSON(w, r, result)
|
|
}
|
|
|
|
func (aH *APIHandler) getNextPrevErrorIDs(w http.ResponseWriter, r *http.Request) {
|
|
|
|
query, err := parseGetErrorRequest(r)
|
|
if aH.HandleError(w, err, http.StatusBadRequest) {
|
|
return
|
|
}
|
|
result, apiErr := aH.reader.GetNextPrevErrorIDs(r.Context(), query)
|
|
if apiErr != nil {
|
|
RespondError(w, apiErr, nil)
|
|
return
|
|
}
|
|
|
|
aH.WriteJSON(w, r, result)
|
|
}
|
|
|
|
func (aH *APIHandler) getErrorFromGroupID(w http.ResponseWriter, r *http.Request) {
|
|
|
|
query, err := parseGetErrorRequest(r)
|
|
if aH.HandleError(w, err, http.StatusBadRequest) {
|
|
return
|
|
}
|
|
result, apiErr := aH.reader.GetErrorFromGroupID(r.Context(), query)
|
|
if apiErr != nil {
|
|
RespondError(w, apiErr, nil)
|
|
return
|
|
}
|
|
|
|
aH.WriteJSON(w, r, result)
|
|
}
|
|
|
|
func (aH *APIHandler) setCustomRetentionTTL(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
claims, errv2 := authtypes.ClaimsFromContext(ctx)
|
|
if errv2 != nil {
|
|
render.Error(w, errors.Newf(errors.TypeInternal, errors.CodeInternal, "failed to get org id from context"))
|
|
return
|
|
}
|
|
|
|
var params retentiontypes.CustomRetentionTTLParams
|
|
if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
|
|
render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "Invalid data"))
|
|
return
|
|
}
|
|
|
|
// Context is not used here as TTL is long duration DB operation
|
|
result, apiErr := aH.reader.SetTTLV2(context.Background(), claims.OrgID, ¶ms)
|
|
if apiErr != nil {
|
|
render.Error(w, errors.New(errors.TypeInvalidInput, errors.CodeInternal, apiErr.Error()))
|
|
return
|
|
}
|
|
|
|
aH.WriteJSON(w, r, result)
|
|
}
|
|
|
|
func (aH *APIHandler) getCustomRetentionTTL(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
claims, errv2 := authtypes.ClaimsFromContext(ctx)
|
|
if errv2 != nil {
|
|
render.Error(w, errors.New(errors.TypeInternal, errors.CodeInternal, "failed to get org id from context"))
|
|
return
|
|
}
|
|
|
|
result, apiErr := aH.reader.GetCustomRetentionTTL(r.Context(), claims.OrgID)
|
|
if apiErr != nil {
|
|
render.Error(w, errors.New(errors.TypeInvalidInput, errors.CodeInternal, apiErr.Error()))
|
|
return
|
|
}
|
|
|
|
aH.WriteJSON(w, r, result)
|
|
}
|
|
|
|
func (aH *APIHandler) getDisks(w http.ResponseWriter, r *http.Request) {
|
|
result, apiErr := aH.reader.GetDisks(context.Background())
|
|
if apiErr != nil && aH.HandleError(w, apiErr.Err, http.StatusInternalServerError) {
|
|
return
|
|
}
|
|
|
|
aH.WriteJSON(w, r, result)
|
|
}
|
|
|
|
func (aH *APIHandler) getVersion(w http.ResponseWriter, r *http.Request) {
|
|
// EE is "N" and the constant is the point: this build has NO enterprise edition.
|
|
// It reported "Y" while the EE features were compiled out, so the frontend took
|
|
// the enterprise bootstrap path and called SigNoz's Zeus licensing service, which
|
|
// 404s here — a flag claiming a capability the binary does not have.
|
|
//
|
|
// The field STAYS rather than being deleted, and stays a single expression, so
|
|
// enabling an edition later is one edit in one place. When there is something to
|
|
// gate, this reads Hanzo's OWN entitlement (GET /v1/entitlements on api.hanzo.ai,
|
|
// cloud/apps/entitlements — live and auth-gated today, 403 not 404) — never Zeus,
|
|
// which is SigNoz's commercial licensing service and not ours to call.
|
|
versionResponse := model.GetVersionResponse{
|
|
Version: version.Info.Version(),
|
|
EE: "N",
|
|
SetupCompleted: aH.SetupCompleted,
|
|
}
|
|
|
|
aH.WriteJSON(w, r, versionResponse)
|
|
}
|
|
|
|
// getHealth is used to check the health of the service.
|
|
// 'live' query param can be used to check liveliness of
|
|
// the service by checking the database connection.
|
|
func (aH *APIHandler) getHealth(w http.ResponseWriter, r *http.Request) {
|
|
_, ok := r.URL.Query()["live"]
|
|
if ok {
|
|
err := aH.reader.CheckDatastore(r.Context())
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Err: err, Typ: model.ErrorStatusServiceUnavailable}, nil)
|
|
return
|
|
}
|
|
}
|
|
|
|
aH.WriteJSON(w, r, map[string]string{"status": "ok"})
|
|
}
|
|
|
|
func (aH *APIHandler) HandleError(w http.ResponseWriter, err error, statusCode int) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
if statusCode == http.StatusInternalServerError {
|
|
aH.logger.Error("internal server error in http handler", errors.Attr(err))
|
|
}
|
|
structuredResp := structuredResponse{
|
|
Errors: []structuredError{
|
|
{
|
|
Code: statusCode,
|
|
Msg: err.Error(),
|
|
},
|
|
},
|
|
}
|
|
resp, _ := json.Marshal(&structuredResp)
|
|
http.Error(w, string(resp), statusCode)
|
|
return true
|
|
}
|
|
|
|
func (aH *APIHandler) WriteJSON(w http.ResponseWriter, r *http.Request, response interface{}) {
|
|
marshall := json.Marshal
|
|
if prettyPrint := r.FormValue("pretty"); prettyPrint != "" && prettyPrint != "false" {
|
|
marshall = func(v interface{}) ([]byte, error) {
|
|
return json.MarshalIndent(v, "", " ")
|
|
}
|
|
}
|
|
resp, _ := marshall(response)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write(resp)
|
|
}
|
|
|
|
// not using md5 hashing as the plain string would work
|
|
func uniqueIdentifier(params []string, separator string) string {
|
|
return strings.Join(params, separator)
|
|
}
|
|
|
|
func (aH *APIHandler) onboardProducers(
|
|
|
|
w http.ResponseWriter, r *http.Request,
|
|
|
|
) {
|
|
messagingQueue, apiErr := ParseKafkaQueueBody(r)
|
|
if apiErr != nil {
|
|
aH.logger.ErrorContext(r.Context(), "failed to parse kafka queue body", errors.Attr(apiErr.Err))
|
|
RespondError(w, apiErr, nil)
|
|
return
|
|
}
|
|
|
|
chq, err := kafka.BuildDatastoreQuery(messagingQueue, kafka.KafkaQueue, "onboard_producers")
|
|
|
|
if err != nil {
|
|
aH.logger.ErrorContext(r.Context(), "failed to build datastore query for onboard producers", errors.Attr(err))
|
|
RespondError(w, apiErr, nil)
|
|
return
|
|
}
|
|
|
|
results, err := aH.reader.GetListResultV3(r.Context(), chq.Query)
|
|
|
|
if err != nil {
|
|
apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err}
|
|
RespondError(w, apiErrObj, err)
|
|
return
|
|
}
|
|
|
|
var entries []kafka.OnboardingResponse
|
|
|
|
for _, result := range results {
|
|
|
|
for key, value := range result.Data {
|
|
var message, attribute, status string
|
|
|
|
intValue := int(*value.(*uint8))
|
|
|
|
if key == "entries" {
|
|
attribute = "telemetry ingestion"
|
|
if intValue != 0 {
|
|
entries = nil
|
|
entry := kafka.OnboardingResponse{
|
|
Attribute: attribute,
|
|
Message: "No data available in the given time range",
|
|
Status: "0",
|
|
}
|
|
entries = append(entries, entry)
|
|
break
|
|
} else {
|
|
status = "1"
|
|
}
|
|
} else if key == "queue" {
|
|
attribute = "messaging.system"
|
|
if intValue != 0 {
|
|
status = "0"
|
|
message = "messaging.system attribute is not present or not equal to kafka in your spans"
|
|
} else {
|
|
status = "1"
|
|
}
|
|
} else if key == "kind" {
|
|
attribute = "kind"
|
|
if intValue != 0 {
|
|
status = "0"
|
|
message = "check if your producer spans has kind=4 as attribute"
|
|
} else {
|
|
status = "1"
|
|
}
|
|
} else if key == "destination" {
|
|
attribute = "messaging.destination.name"
|
|
if intValue != 0 {
|
|
status = "0"
|
|
message = "messaging.destination.name attribute is not present in your spans"
|
|
} else {
|
|
status = "1"
|
|
}
|
|
} else if key == "partition" {
|
|
attribute = "messaging.destination.partition.id"
|
|
if intValue != 0 {
|
|
status = "0"
|
|
message = "messaging.destination.partition.id attribute is not present in your spans"
|
|
} else {
|
|
status = "1"
|
|
}
|
|
}
|
|
|
|
entry := kafka.OnboardingResponse{
|
|
Attribute: attribute,
|
|
Message: message,
|
|
Status: status,
|
|
}
|
|
|
|
entries = append(entries, entry)
|
|
}
|
|
}
|
|
|
|
sort.Slice(entries, func(i, j int) bool {
|
|
return entries[i].Attribute < entries[j].Attribute
|
|
})
|
|
|
|
aH.Respond(w, entries)
|
|
}
|
|
|
|
func (aH *APIHandler) onboardConsumers(
|
|
|
|
w http.ResponseWriter, r *http.Request,
|
|
|
|
) {
|
|
messagingQueue, apiErr := ParseKafkaQueueBody(r)
|
|
if apiErr != nil {
|
|
aH.logger.ErrorContext(r.Context(), "failed to parse kafka queue body", errors.Attr(apiErr.Err))
|
|
RespondError(w, apiErr, nil)
|
|
return
|
|
}
|
|
|
|
chq, err := kafka.BuildDatastoreQuery(messagingQueue, kafka.KafkaQueue, "onboard_consumers")
|
|
|
|
if err != nil {
|
|
aH.logger.ErrorContext(r.Context(), "failed to build datastore query for onboard consumers", errors.Attr(err))
|
|
RespondError(w, apiErr, nil)
|
|
return
|
|
}
|
|
|
|
result, err := aH.reader.GetListResultV3(r.Context(), chq.Query)
|
|
|
|
if err != nil {
|
|
apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err}
|
|
RespondError(w, apiErrObj, err)
|
|
return
|
|
}
|
|
|
|
var entries []kafka.OnboardingResponse
|
|
|
|
for _, result := range result {
|
|
for key, value := range result.Data {
|
|
var message, attribute, status string
|
|
|
|
intValue := int(*value.(*uint8))
|
|
|
|
if key == "entries" {
|
|
attribute = "telemetry ingestion"
|
|
if intValue != 0 {
|
|
entries = nil
|
|
entry := kafka.OnboardingResponse{
|
|
Attribute: attribute,
|
|
Message: "No data available in the given time range",
|
|
Status: "0",
|
|
}
|
|
entries = append(entries, entry)
|
|
break
|
|
} else {
|
|
status = "1"
|
|
}
|
|
} else if key == "queue" {
|
|
attribute = "messaging.system"
|
|
if intValue != 0 {
|
|
status = "0"
|
|
message = "messaging.system attribute is not present or not equal to kafka in your spans"
|
|
} else {
|
|
status = "1"
|
|
}
|
|
} else if key == "kind" {
|
|
attribute = "kind"
|
|
if intValue != 0 {
|
|
status = "0"
|
|
message = "check if your consumer spans has kind=5 as attribute"
|
|
} else {
|
|
status = "1"
|
|
}
|
|
} else if key == "destination" {
|
|
attribute = "messaging.destination.name"
|
|
if intValue != 0 {
|
|
status = "0"
|
|
message = "messaging.destination.name attribute is not present in your spans"
|
|
} else {
|
|
status = "1"
|
|
}
|
|
} else if key == "partition" {
|
|
attribute = "messaging.destination.partition.id"
|
|
if intValue != 0 {
|
|
status = "0"
|
|
message = "messaging.destination.partition.id attribute is not present in your spans"
|
|
} else {
|
|
status = "1"
|
|
}
|
|
} else if key == "svc" {
|
|
attribute = "service_name"
|
|
if intValue != 0 {
|
|
status = "0"
|
|
message = "service_name attribute is not present in your spans"
|
|
} else {
|
|
status = "1"
|
|
}
|
|
} else if key == "cgroup" {
|
|
attribute = "messaging.kafka.consumer.group"
|
|
if intValue != 0 {
|
|
status = "0"
|
|
message = "messaging.kafka.consumer.group attribute is not present in your spans"
|
|
} else {
|
|
status = "1"
|
|
}
|
|
} else if key == "bodysize" {
|
|
attribute = "messaging.message.body.size"
|
|
if intValue != 0 {
|
|
status = "0"
|
|
message = "messaging.message.body.size attribute is not present in your spans"
|
|
} else {
|
|
status = "1"
|
|
}
|
|
} else if key == "clientid" {
|
|
attribute = "messaging.client_id"
|
|
if intValue != 0 {
|
|
status = "0"
|
|
message = "messaging.client_id attribute is not present in your spans"
|
|
} else {
|
|
status = "1"
|
|
}
|
|
} else if key == "instanceid" {
|
|
attribute = "service.instance.id"
|
|
if intValue != 0 {
|
|
status = "0"
|
|
message = "service.instance.id attribute is not present in your spans"
|
|
} else {
|
|
status = "1"
|
|
}
|
|
}
|
|
|
|
entry := kafka.OnboardingResponse{
|
|
Attribute: attribute,
|
|
Message: message,
|
|
Status: status,
|
|
}
|
|
entries = append(entries, entry)
|
|
}
|
|
}
|
|
|
|
sort.Slice(entries, func(i, j int) bool {
|
|
return entries[i].Attribute < entries[j].Attribute
|
|
})
|
|
|
|
aH.Respond(w, entries)
|
|
}
|
|
|
|
func (aH *APIHandler) onboardKafka(w http.ResponseWriter, r *http.Request) {
|
|
claims, err := authtypes.ClaimsFromContext(r.Context())
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
orgID, err := valuer.NewUUID(claims.OrgID)
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
messagingQueue, apiErr := ParseKafkaQueueBody(r)
|
|
if apiErr != nil {
|
|
aH.logger.ErrorContext(r.Context(), "failed to parse kafka queue body", errors.Attr(apiErr.Err))
|
|
RespondError(w, apiErr, nil)
|
|
return
|
|
}
|
|
|
|
queryRangeParams, err := kafka.BuildBuilderQueriesKafkaOnboarding(messagingQueue)
|
|
|
|
if err != nil {
|
|
aH.logger.ErrorContext(r.Context(), "failed to build kafka onboarding queries", errors.Attr(err))
|
|
RespondError(w, apiErr, nil)
|
|
return
|
|
}
|
|
|
|
ctx := ctxtypes.NewContextWithCommentVals(r.Context(), map[string]string{
|
|
instrumentationtypes.CodeNamespace: "app",
|
|
instrumentationtypes.CodeFunctionName: "onboardKafka",
|
|
})
|
|
|
|
results, errQueriesByName, err := aH.querierV2.QueryRange(ctx, orgID, queryRangeParams)
|
|
if err != nil {
|
|
apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err}
|
|
RespondError(w, apiErrObj, errQueriesByName)
|
|
return
|
|
}
|
|
|
|
var entries []kafka.OnboardingResponse
|
|
|
|
var fetchLatencyState, consumerLagState bool
|
|
|
|
for _, result := range results {
|
|
for _, series := range result.Series {
|
|
for _, point := range series.Points {
|
|
pointValue := point.Value
|
|
if pointValue > 0 {
|
|
if result.QueryName == "fetch_latency" {
|
|
fetchLatencyState = true
|
|
break
|
|
}
|
|
if result.QueryName == "consumer_lag" {
|
|
consumerLagState = true
|
|
break
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
var kafkaConsumerFetchLatencyAvg string = "kafka_consumer_fetch_latency_avg"
|
|
var kafkaConsumerLag string = "kafka_consumer_group_lag"
|
|
if constants.IsDotMetricsEnabled {
|
|
kafkaConsumerLag = "kafka.consumer_group.lag"
|
|
kafkaConsumerFetchLatencyAvg = "kafka.consumer.fetch_latency_avg"
|
|
}
|
|
|
|
if !fetchLatencyState && !consumerLagState {
|
|
entries = append(entries, kafka.OnboardingResponse{
|
|
Attribute: "telemetry ingestion",
|
|
Message: "No data available in the given time range",
|
|
Status: "0",
|
|
})
|
|
}
|
|
|
|
if !fetchLatencyState {
|
|
|
|
entries = append(entries, kafka.OnboardingResponse{
|
|
Attribute: kafkaConsumerFetchLatencyAvg,
|
|
Message: "Metric kafka_consumer_fetch_latency_avg is not present in the given time range.",
|
|
Status: "0",
|
|
})
|
|
} else {
|
|
entries = append(entries, kafka.OnboardingResponse{
|
|
Attribute: kafkaConsumerFetchLatencyAvg,
|
|
Status: "1",
|
|
})
|
|
}
|
|
|
|
if !consumerLagState {
|
|
entries = append(entries, kafka.OnboardingResponse{
|
|
Attribute: kafkaConsumerLag,
|
|
Message: "Metric kafka_consumer_group_lag is not present in the given time range.",
|
|
Status: "0",
|
|
})
|
|
} else {
|
|
entries = append(entries, kafka.OnboardingResponse{
|
|
Attribute: kafkaConsumerLag,
|
|
Status: "1",
|
|
})
|
|
}
|
|
|
|
aH.Respond(w, entries)
|
|
}
|
|
|
|
func (aH *APIHandler) getNetworkData(w http.ResponseWriter, r *http.Request) {
|
|
claims, err := authtypes.ClaimsFromContext(r.Context())
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
orgID, err := valuer.NewUUID(claims.OrgID)
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
attributeCache := &kafka.Clients{
|
|
Hash: make(map[string]struct{}),
|
|
}
|
|
messagingQueue, apiErr := ParseKafkaQueueBody(r)
|
|
|
|
if apiErr != nil {
|
|
aH.logger.ErrorContext(r.Context(), "failed to parse kafka queue body", errors.Attr(apiErr.Err))
|
|
RespondError(w, apiErr, nil)
|
|
return
|
|
}
|
|
|
|
queryRangeParams, err := kafka.BuildQRParamsWithCache(messagingQueue, "throughput", attributeCache)
|
|
if err != nil {
|
|
aH.logger.ErrorContext(r.Context(), "failed to build query range params for throughput", errors.Attr(err))
|
|
RespondError(w, apiErr, nil)
|
|
return
|
|
}
|
|
if err := validateQueryRangeParamsV3(queryRangeParams); err != nil {
|
|
aH.logger.ErrorContext(r.Context(), "failed to validate query range params", errors.Attr(err))
|
|
RespondError(w, apiErr, nil)
|
|
return
|
|
}
|
|
|
|
var result []*v3.Result
|
|
var errQueriesByName map[string]error
|
|
|
|
ctx := ctxtypes.NewContextWithCommentVals(r.Context(), map[string]string{
|
|
instrumentationtypes.CodeNamespace: "app",
|
|
instrumentationtypes.CodeFunctionName: "getNetworkData",
|
|
})
|
|
|
|
result, errQueriesByName, err = aH.querierV2.QueryRange(ctx, orgID, queryRangeParams)
|
|
if err != nil {
|
|
apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err}
|
|
RespondError(w, apiErrObj, errQueriesByName)
|
|
return
|
|
}
|
|
|
|
for _, res := range result {
|
|
for _, series := range res.Series {
|
|
clientID, clientIDOk := series.Labels["client_id"]
|
|
serviceInstanceID, serviceInstanceIDOk := series.Labels["service_instance_id"]
|
|
serviceName, serviceNameOk := series.Labels["service_name"]
|
|
params := []string{clientID, serviceInstanceID, serviceName}
|
|
hashKey := uniqueIdentifier(params, "#")
|
|
_, ok := attributeCache.Hash[hashKey]
|
|
if clientIDOk && serviceInstanceIDOk && serviceNameOk && !ok {
|
|
attributeCache.Hash[hashKey] = struct{}{}
|
|
attributeCache.ClientID = append(attributeCache.ClientID, clientID)
|
|
attributeCache.ServiceInstanceID = append(attributeCache.ServiceInstanceID, serviceInstanceID)
|
|
attributeCache.ServiceName = append(attributeCache.ServiceName, serviceName)
|
|
}
|
|
}
|
|
}
|
|
|
|
queryRangeParams, err = kafka.BuildQRParamsWithCache(messagingQueue, "fetch-latency", attributeCache)
|
|
if err != nil {
|
|
aH.logger.ErrorContext(r.Context(), "failed to build query range params for fetch latency", errors.Attr(err))
|
|
RespondError(w, apiErr, nil)
|
|
return
|
|
}
|
|
if err := validateQueryRangeParamsV3(queryRangeParams); err != nil {
|
|
aH.logger.ErrorContext(r.Context(), "failed to validate query range params for fetch latency", errors.Attr(err))
|
|
RespondError(w, apiErr, nil)
|
|
return
|
|
}
|
|
|
|
resultFetchLatency, errQueriesByNameFetchLatency, err := aH.querierV2.QueryRange(ctx, orgID, queryRangeParams)
|
|
if err != nil {
|
|
apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err}
|
|
RespondError(w, apiErrObj, errQueriesByNameFetchLatency)
|
|
return
|
|
}
|
|
|
|
latencyColumn := &v3.Result{QueryName: "latency"}
|
|
var latencySeries []*v3.Series
|
|
for _, res := range resultFetchLatency {
|
|
for _, series := range res.Series {
|
|
clientID, clientIDOk := series.Labels["client_id"]
|
|
serviceInstanceID, serviceInstanceIDOk := series.Labels["service_instance_id"]
|
|
serviceName, serviceNameOk := series.Labels["service_name"]
|
|
params := []string{clientID, serviceInstanceID, serviceName}
|
|
hashKey := uniqueIdentifier(params, "#")
|
|
_, ok := attributeCache.Hash[hashKey]
|
|
if clientIDOk && serviceInstanceIDOk && serviceNameOk && ok {
|
|
latencySeries = append(latencySeries, series)
|
|
}
|
|
}
|
|
}
|
|
|
|
latencyColumn.Series = latencySeries
|
|
result = append(result, latencyColumn)
|
|
|
|
resultFetchLatency = postprocess.TransformToTableForBuilderQueries(result, queryRangeParams)
|
|
|
|
resp := v3.QueryRangeResponse{
|
|
Result: resultFetchLatency,
|
|
}
|
|
aH.Respond(w, resp)
|
|
}
|
|
|
|
func (aH *APIHandler) getProducerData(w http.ResponseWriter, r *http.Request) {
|
|
claims, err := authtypes.ClaimsFromContext(r.Context())
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
orgID, err := valuer.NewUUID(claims.OrgID)
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
// parse the query params to retrieve the messaging queue struct
|
|
messagingQueue, apiErr := ParseKafkaQueueBody(r)
|
|
|
|
if apiErr != nil {
|
|
aH.logger.ErrorContext(r.Context(), "failed to parse kafka queue body", errors.Attr(apiErr.Err))
|
|
RespondError(w, apiErr, nil)
|
|
return
|
|
}
|
|
|
|
evalCtx := featuretypes.NewFlaggerEvaluationContext(orgID)
|
|
ctx := ctxtypes.NewContextWithCommentVals(r.Context(), map[string]string{
|
|
instrumentationtypes.CodeNamespace: "app",
|
|
instrumentationtypes.CodeFunctionName: "getProducerData",
|
|
})
|
|
kafkaSpanEval := aH.O11y.Flagger.BooleanOrEmpty(ctx, flagger.FeatureKafkaSpanEval, evalCtx)
|
|
|
|
queryRangeParams, err := kafka.BuildQueryRangeParams(messagingQueue, "producer", kafkaSpanEval)
|
|
if err != nil {
|
|
aH.logger.ErrorContext(r.Context(), "failed to build query range params for producer", errors.Attr(err))
|
|
RespondError(w, apiErr, nil)
|
|
return
|
|
}
|
|
|
|
if err := validateQueryRangeParamsV3(queryRangeParams); err != nil {
|
|
aH.logger.ErrorContext(r.Context(), "failed to validate query range params for producer", errors.Attr(err))
|
|
RespondError(w, apiErr, nil)
|
|
return
|
|
}
|
|
|
|
var result []*v3.Result
|
|
var errQueriesByName map[string]error
|
|
|
|
result, errQueriesByName, err = aH.querierV2.QueryRange(ctx, orgID, queryRangeParams)
|
|
if err != nil {
|
|
apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err}
|
|
RespondError(w, apiErrObj, errQueriesByName)
|
|
return
|
|
}
|
|
result = postprocess.TransformToTableForDatastoreQueries(result)
|
|
|
|
resp := v3.QueryRangeResponse{
|
|
Result: result,
|
|
}
|
|
aH.Respond(w, resp)
|
|
}
|
|
|
|
func (aH *APIHandler) getConsumerData(w http.ResponseWriter, r *http.Request) {
|
|
claims, err := authtypes.ClaimsFromContext(r.Context())
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
orgID, err := valuer.NewUUID(claims.OrgID)
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
messagingQueue, apiErr := ParseKafkaQueueBody(r)
|
|
|
|
if apiErr != nil {
|
|
aH.logger.ErrorContext(r.Context(), "failed to parse kafka queue body", errors.Attr(apiErr.Err))
|
|
RespondError(w, apiErr, nil)
|
|
return
|
|
}
|
|
|
|
evalCtx := featuretypes.NewFlaggerEvaluationContext(orgID)
|
|
kafkaSpanEval := aH.O11y.Flagger.BooleanOrEmpty(r.Context(), flagger.FeatureKafkaSpanEval, evalCtx)
|
|
|
|
queryRangeParams, err := kafka.BuildQueryRangeParams(messagingQueue, "consumer", kafkaSpanEval)
|
|
if err != nil {
|
|
aH.logger.ErrorContext(r.Context(), "failed to build query range params for consumer", errors.Attr(err))
|
|
RespondError(w, apiErr, nil)
|
|
return
|
|
}
|
|
|
|
if err := validateQueryRangeParamsV3(queryRangeParams); err != nil {
|
|
aH.logger.ErrorContext(r.Context(), "failed to validate query range params for consumer", errors.Attr(err))
|
|
RespondError(w, apiErr, nil)
|
|
return
|
|
}
|
|
|
|
var result []*v3.Result
|
|
var errQueriesByName map[string]error
|
|
|
|
ctx := ctxtypes.NewContextWithCommentVals(r.Context(), map[string]string{
|
|
instrumentationtypes.CodeNamespace: "app",
|
|
instrumentationtypes.CodeFunctionName: "getConsumerData",
|
|
})
|
|
result, errQueriesByName, err = aH.querierV2.QueryRange(ctx, orgID, queryRangeParams)
|
|
if err != nil {
|
|
apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err}
|
|
RespondError(w, apiErrObj, errQueriesByName)
|
|
return
|
|
}
|
|
result = postprocess.TransformToTableForDatastoreQueries(result)
|
|
|
|
resp := v3.QueryRangeResponse{
|
|
Result: result,
|
|
}
|
|
aH.Respond(w, resp)
|
|
}
|
|
|
|
// s1
|
|
func (aH *APIHandler) getPartitionOverviewLatencyData(w http.ResponseWriter, r *http.Request) {
|
|
claims, err := authtypes.ClaimsFromContext(r.Context())
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
orgID, err := valuer.NewUUID(claims.OrgID)
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
messagingQueue, apiErr := ParseKafkaQueueBody(r)
|
|
|
|
if apiErr != nil {
|
|
aH.logger.ErrorContext(r.Context(), "failed to parse kafka queue body", errors.Attr(apiErr.Err))
|
|
RespondError(w, apiErr, nil)
|
|
return
|
|
}
|
|
|
|
evalCtx := featuretypes.NewFlaggerEvaluationContext(orgID)
|
|
kafkaSpanEval := aH.O11y.Flagger.BooleanOrEmpty(r.Context(), flagger.FeatureKafkaSpanEval, evalCtx)
|
|
|
|
queryRangeParams, err := kafka.BuildQueryRangeParams(messagingQueue, "producer-topic-throughput", kafkaSpanEval)
|
|
if err != nil {
|
|
aH.logger.ErrorContext(r.Context(), "failed to build query range params for producer topic throughput", errors.Attr(err))
|
|
RespondError(w, apiErr, nil)
|
|
return
|
|
}
|
|
|
|
if err := validateQueryRangeParamsV3(queryRangeParams); err != nil {
|
|
aH.logger.ErrorContext(r.Context(), "failed to validate query range params for producer topic throughput", errors.Attr(err))
|
|
RespondError(w, apiErr, nil)
|
|
return
|
|
}
|
|
|
|
var result []*v3.Result
|
|
var errQueriesByName map[string]error
|
|
|
|
ctx := ctxtypes.NewContextWithCommentVals(r.Context(), map[string]string{
|
|
instrumentationtypes.CodeNamespace: "app",
|
|
instrumentationtypes.CodeFunctionName: "getPartitionOverviewLatencyData",
|
|
})
|
|
result, errQueriesByName, err = aH.querierV2.QueryRange(ctx, orgID, queryRangeParams)
|
|
if err != nil {
|
|
apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err}
|
|
RespondError(w, apiErrObj, errQueriesByName)
|
|
return
|
|
}
|
|
result = postprocess.TransformToTableForDatastoreQueries(result)
|
|
|
|
resp := v3.QueryRangeResponse{
|
|
Result: result,
|
|
}
|
|
aH.Respond(w, resp)
|
|
}
|
|
|
|
// s1
|
|
func (aH *APIHandler) getConsumerPartitionLatencyData(w http.ResponseWriter, r *http.Request) {
|
|
claims, err := authtypes.ClaimsFromContext(r.Context())
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
orgID, err := valuer.NewUUID(claims.OrgID)
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
messagingQueue, apiErr := ParseKafkaQueueBody(r)
|
|
|
|
if apiErr != nil {
|
|
aH.logger.ErrorContext(r.Context(), "failed to parse kafka queue body", errors.Attr(apiErr.Err))
|
|
RespondError(w, apiErr, nil)
|
|
return
|
|
}
|
|
|
|
evalCtx := featuretypes.NewFlaggerEvaluationContext(orgID)
|
|
kafkaSpanEval := aH.O11y.Flagger.BooleanOrEmpty(r.Context(), flagger.FeatureKafkaSpanEval, evalCtx)
|
|
|
|
queryRangeParams, err := kafka.BuildQueryRangeParams(messagingQueue, "consumer_partition_latency", kafkaSpanEval)
|
|
if err != nil {
|
|
aH.logger.ErrorContext(r.Context(), "failed to build query range params for consumer partition latency", errors.Attr(err))
|
|
RespondError(w, apiErr, nil)
|
|
return
|
|
}
|
|
|
|
if err := validateQueryRangeParamsV3(queryRangeParams); err != nil {
|
|
aH.logger.ErrorContext(r.Context(), "failed to validate query range params for consumer partition latency", errors.Attr(err))
|
|
RespondError(w, apiErr, nil)
|
|
return
|
|
}
|
|
|
|
var result []*v3.Result
|
|
var errQueriesByName map[string]error
|
|
|
|
ctx := ctxtypes.NewContextWithCommentVals(r.Context(), map[string]string{
|
|
instrumentationtypes.CodeNamespace: "app",
|
|
instrumentationtypes.CodeFunctionName: "getConsumerPartitionLatencyData",
|
|
})
|
|
result, errQueriesByName, err = aH.querierV2.QueryRange(ctx, orgID, queryRangeParams)
|
|
if err != nil {
|
|
apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err}
|
|
RespondError(w, apiErrObj, errQueriesByName)
|
|
return
|
|
}
|
|
result = postprocess.TransformToTableForDatastoreQueries(result)
|
|
|
|
resp := v3.QueryRangeResponse{
|
|
Result: result,
|
|
}
|
|
aH.Respond(w, resp)
|
|
}
|
|
|
|
// s3 p overview
|
|
// fetch traces
|
|
// cache attributes
|
|
// fetch byte rate metrics
|
|
func (aH *APIHandler) getProducerThroughputOverview(w http.ResponseWriter, r *http.Request) {
|
|
claims, err := authtypes.ClaimsFromContext(r.Context())
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
orgID, err := valuer.NewUUID(claims.OrgID)
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
messagingQueue, apiErr := ParseKafkaQueueBody(r)
|
|
if apiErr != nil {
|
|
aH.logger.ErrorContext(r.Context(), "failed to parse kafka queue body", errors.Attr(apiErr.Err))
|
|
RespondError(w, apiErr, nil)
|
|
return
|
|
}
|
|
|
|
attributeCache := &kafka.Clients{
|
|
Hash: make(map[string]struct{}),
|
|
}
|
|
|
|
producerQueryRangeParams, err := kafka.BuildQRParamsWithCache(messagingQueue, "producer-throughput-overview", attributeCache)
|
|
if err != nil {
|
|
aH.logger.ErrorContext(r.Context(), "failed to build query range params for producer throughput overview", errors.Attr(err))
|
|
RespondError(w, apiErr, nil)
|
|
return
|
|
}
|
|
|
|
if err := validateQueryRangeParamsV3(producerQueryRangeParams); err != nil {
|
|
aH.logger.ErrorContext(r.Context(), "failed to validate query range params for producer throughput overview", errors.Attr(err))
|
|
RespondError(w, apiErr, nil)
|
|
return
|
|
}
|
|
|
|
var result []*v3.Result
|
|
var errQueriesByName map[string]error
|
|
|
|
ctx := ctxtypes.NewContextWithCommentVals(r.Context(), map[string]string{
|
|
instrumentationtypes.CodeNamespace: "app",
|
|
instrumentationtypes.CodeFunctionName: "getProducerThroughputOverview",
|
|
})
|
|
result, errQueriesByName, err = aH.querierV2.QueryRange(ctx, orgID, producerQueryRangeParams)
|
|
if err != nil {
|
|
apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err}
|
|
RespondError(w, apiErrObj, errQueriesByName)
|
|
return
|
|
}
|
|
|
|
for _, res := range result {
|
|
for _, series := range res.Series {
|
|
serviceName, serviceNameOk := series.Labels["service_name"]
|
|
topicName, topicNameOk := series.Labels["topic"]
|
|
params := []string{serviceName, topicName}
|
|
hashKey := uniqueIdentifier(params, "#")
|
|
_, ok := attributeCache.Hash[hashKey]
|
|
if topicNameOk && serviceNameOk && !ok {
|
|
attributeCache.Hash[hashKey] = struct{}{}
|
|
attributeCache.TopicName = append(attributeCache.TopicName, topicName)
|
|
attributeCache.ServiceName = append(attributeCache.ServiceName, serviceName)
|
|
}
|
|
}
|
|
}
|
|
|
|
queryRangeParams, err := kafka.BuildQRParamsWithCache(messagingQueue, "producer-throughput-overview-byte-rate", attributeCache)
|
|
if err != nil {
|
|
aH.logger.ErrorContext(r.Context(), "failed to build query range params for producer throughput byte rate", errors.Attr(err))
|
|
RespondError(w, apiErr, nil)
|
|
return
|
|
}
|
|
if err := validateQueryRangeParamsV3(queryRangeParams); err != nil {
|
|
aH.logger.ErrorContext(r.Context(), "failed to validate query range params for producer throughput byte rate", errors.Attr(err))
|
|
RespondError(w, apiErr, nil)
|
|
return
|
|
}
|
|
|
|
resultFetchLatency, errQueriesByNameFetchLatency, err := aH.querierV2.QueryRange(ctx, orgID, queryRangeParams)
|
|
if err != nil {
|
|
apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err}
|
|
RespondError(w, apiErrObj, errQueriesByNameFetchLatency)
|
|
return
|
|
}
|
|
|
|
byteRateColumn := &v3.Result{QueryName: "byte_rate"}
|
|
var byteRateSeries []*v3.Series
|
|
for _, res := range resultFetchLatency {
|
|
for _, series := range res.Series {
|
|
topic, topicOk := series.Labels["topic"]
|
|
serviceName, serviceNameOk := series.Labels["service_name"]
|
|
params := []string{serviceName, topic}
|
|
hashKey := uniqueIdentifier(params, "#")
|
|
_, ok := attributeCache.Hash[hashKey]
|
|
if topicOk && serviceNameOk && ok {
|
|
byteRateSeries = append(byteRateSeries, series)
|
|
}
|
|
}
|
|
}
|
|
|
|
byteRateColumn.Series = byteRateSeries
|
|
var latencyColumnResult []*v3.Result
|
|
latencyColumnResult = append(latencyColumnResult, byteRateColumn)
|
|
|
|
resultFetchLatency = postprocess.TransformToTableForBuilderQueries(latencyColumnResult, queryRangeParams)
|
|
|
|
result = postprocess.TransformToTableForDatastoreQueries(result)
|
|
|
|
result = append(result, resultFetchLatency[0])
|
|
resp := v3.QueryRangeResponse{
|
|
Result: result,
|
|
}
|
|
aH.Respond(w, resp)
|
|
}
|
|
|
|
// s3 p details
|
|
func (aH *APIHandler) getProducerThroughputDetails(w http.ResponseWriter, r *http.Request) {
|
|
claims, err := authtypes.ClaimsFromContext(r.Context())
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
orgID, err := valuer.NewUUID(claims.OrgID)
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
messagingQueue, apiErr := ParseKafkaQueueBody(r)
|
|
|
|
if apiErr != nil {
|
|
aH.logger.ErrorContext(r.Context(), "failed to parse kafka queue body", errors.Attr(apiErr.Err))
|
|
RespondError(w, apiErr, nil)
|
|
return
|
|
}
|
|
|
|
evalCtx := featuretypes.NewFlaggerEvaluationContext(orgID)
|
|
kafkaSpanEval := aH.O11y.Flagger.BooleanOrEmpty(r.Context(), flagger.FeatureKafkaSpanEval, evalCtx)
|
|
|
|
queryRangeParams, err := kafka.BuildQueryRangeParams(messagingQueue, "producer-throughput-details", kafkaSpanEval)
|
|
if err != nil {
|
|
aH.logger.ErrorContext(r.Context(), "failed to build query range params for producer throughput details", errors.Attr(err))
|
|
RespondError(w, apiErr, nil)
|
|
return
|
|
}
|
|
|
|
if err := validateQueryRangeParamsV3(queryRangeParams); err != nil {
|
|
aH.logger.ErrorContext(r.Context(), "failed to validate query range params for producer throughput details", errors.Attr(err))
|
|
RespondError(w, apiErr, nil)
|
|
return
|
|
}
|
|
|
|
var result []*v3.Result
|
|
var errQueriesByName map[string]error
|
|
|
|
ctx := ctxtypes.NewContextWithCommentVals(r.Context(), map[string]string{
|
|
instrumentationtypes.CodeNamespace: "app",
|
|
instrumentationtypes.CodeFunctionName: "getProducerThroughputDetails",
|
|
})
|
|
result, errQueriesByName, err = aH.querierV2.QueryRange(ctx, orgID, queryRangeParams)
|
|
if err != nil {
|
|
apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err}
|
|
RespondError(w, apiErrObj, errQueriesByName)
|
|
return
|
|
}
|
|
result = postprocess.TransformToTableForDatastoreQueries(result)
|
|
|
|
resp := v3.QueryRangeResponse{
|
|
Result: result,
|
|
}
|
|
aH.Respond(w, resp)
|
|
}
|
|
|
|
// s3 c overview
|
|
func (aH *APIHandler) getConsumerThroughputOverview(w http.ResponseWriter, r *http.Request) {
|
|
claims, err := authtypes.ClaimsFromContext(r.Context())
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
orgID, err := valuer.NewUUID(claims.OrgID)
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
messagingQueue, apiErr := ParseKafkaQueueBody(r)
|
|
|
|
if apiErr != nil {
|
|
aH.logger.ErrorContext(r.Context(), "failed to parse kafka queue body", errors.Attr(apiErr.Err))
|
|
RespondError(w, apiErr, nil)
|
|
return
|
|
}
|
|
|
|
evalCtx := featuretypes.NewFlaggerEvaluationContext(orgID)
|
|
kafkaSpanEval := aH.O11y.Flagger.BooleanOrEmpty(r.Context(), flagger.FeatureKafkaSpanEval, evalCtx)
|
|
|
|
queryRangeParams, err := kafka.BuildQueryRangeParams(messagingQueue, "consumer-throughput-overview", kafkaSpanEval)
|
|
if err != nil {
|
|
aH.logger.ErrorContext(r.Context(), "failed to build query range params for consumer throughput overview", errors.Attr(err))
|
|
RespondError(w, apiErr, nil)
|
|
return
|
|
}
|
|
|
|
if err := validateQueryRangeParamsV3(queryRangeParams); err != nil {
|
|
aH.logger.ErrorContext(r.Context(), "failed to validate query range params for consumer throughput overview", errors.Attr(err))
|
|
RespondError(w, apiErr, nil)
|
|
return
|
|
}
|
|
|
|
var result []*v3.Result
|
|
var errQueriesByName map[string]error
|
|
|
|
ctx := ctxtypes.NewContextWithCommentVals(r.Context(), map[string]string{
|
|
instrumentationtypes.CodeNamespace: "app",
|
|
instrumentationtypes.CodeFunctionName: "getConsumerThroughputOverview",
|
|
})
|
|
result, errQueriesByName, err = aH.querierV2.QueryRange(ctx, orgID, queryRangeParams)
|
|
if err != nil {
|
|
apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err}
|
|
RespondError(w, apiErrObj, errQueriesByName)
|
|
return
|
|
}
|
|
result = postprocess.TransformToTableForDatastoreQueries(result)
|
|
|
|
resp := v3.QueryRangeResponse{
|
|
Result: result,
|
|
}
|
|
aH.Respond(w, resp)
|
|
}
|
|
|
|
// s3 c details
|
|
func (aH *APIHandler) getConsumerThroughputDetails(w http.ResponseWriter, r *http.Request) {
|
|
claims, err := authtypes.ClaimsFromContext(r.Context())
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
orgID, err := valuer.NewUUID(claims.OrgID)
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
messagingQueue, apiErr := ParseKafkaQueueBody(r)
|
|
|
|
if apiErr != nil {
|
|
aH.logger.ErrorContext(r.Context(), "failed to parse kafka queue body", errors.Attr(apiErr.Err))
|
|
RespondError(w, apiErr, nil)
|
|
return
|
|
}
|
|
|
|
evalCtx := featuretypes.NewFlaggerEvaluationContext(orgID)
|
|
kafkaSpanEval := aH.O11y.Flagger.BooleanOrEmpty(r.Context(), flagger.FeatureKafkaSpanEval, evalCtx)
|
|
|
|
queryRangeParams, err := kafka.BuildQueryRangeParams(messagingQueue, "consumer-throughput-details", kafkaSpanEval)
|
|
if err != nil {
|
|
aH.logger.ErrorContext(r.Context(), "failed to build query range params for consumer throughput details", errors.Attr(err))
|
|
RespondError(w, apiErr, nil)
|
|
return
|
|
}
|
|
|
|
if err := validateQueryRangeParamsV3(queryRangeParams); err != nil {
|
|
aH.logger.ErrorContext(r.Context(), "failed to validate query range params for consumer throughput details", errors.Attr(err))
|
|
RespondError(w, apiErr, nil)
|
|
return
|
|
}
|
|
|
|
var result []*v3.Result
|
|
var errQueriesByName map[string]error
|
|
|
|
ctx := ctxtypes.NewContextWithCommentVals(r.Context(), map[string]string{
|
|
instrumentationtypes.CodeNamespace: "app",
|
|
instrumentationtypes.CodeFunctionName: "getConsumerThroughputDetails",
|
|
})
|
|
result, errQueriesByName, err = aH.querierV2.QueryRange(ctx, orgID, queryRangeParams)
|
|
if err != nil {
|
|
apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err}
|
|
RespondError(w, apiErrObj, errQueriesByName)
|
|
return
|
|
}
|
|
result = postprocess.TransformToTableForDatastoreQueries(result)
|
|
|
|
resp := v3.QueryRangeResponse{
|
|
Result: result,
|
|
}
|
|
aH.Respond(w, resp)
|
|
}
|
|
|
|
// s4
|
|
// needs logic to parse duration
|
|
// needs logic to get the percentage
|
|
// show 10 traces
|
|
func (aH *APIHandler) getProducerConsumerEval(w http.ResponseWriter, r *http.Request) {
|
|
claims, err := authtypes.ClaimsFromContext(r.Context())
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
orgID, err := valuer.NewUUID(claims.OrgID)
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
messagingQueue, apiErr := ParseKafkaQueueBody(r)
|
|
|
|
if apiErr != nil {
|
|
aH.logger.ErrorContext(r.Context(), "failed to parse kafka queue body", errors.Attr(apiErr.Err))
|
|
RespondError(w, apiErr, nil)
|
|
return
|
|
}
|
|
|
|
evalCtx := featuretypes.NewFlaggerEvaluationContext(orgID)
|
|
kafkaSpanEval := aH.O11y.Flagger.BooleanOrEmpty(r.Context(), flagger.FeatureKafkaSpanEval, evalCtx)
|
|
|
|
queryRangeParams, err := kafka.BuildQueryRangeParams(messagingQueue, "producer-consumer-eval", kafkaSpanEval)
|
|
if err != nil {
|
|
aH.logger.ErrorContext(r.Context(), "failed to build query range params for producer consumer eval", errors.Attr(err))
|
|
RespondError(w, &model.ApiError{
|
|
Typ: model.ErrorBadData,
|
|
Err: err,
|
|
}, nil)
|
|
return
|
|
}
|
|
|
|
if err := validateQueryRangeParamsV3(queryRangeParams); err != nil {
|
|
aH.logger.ErrorContext(r.Context(), "failed to validate query range params for producer consumer eval", errors.Attr(err))
|
|
RespondError(w, apiErr, nil)
|
|
return
|
|
}
|
|
|
|
var result []*v3.Result
|
|
var errQueriesByName map[string]error
|
|
|
|
ctx := ctxtypes.NewContextWithCommentVals(r.Context(), map[string]string{
|
|
instrumentationtypes.CodeNamespace: "app",
|
|
instrumentationtypes.CodeFunctionName: "getProducerConsumerEval",
|
|
})
|
|
result, errQueriesByName, err = aH.querierV2.QueryRange(ctx, orgID, queryRangeParams)
|
|
if err != nil {
|
|
apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err}
|
|
RespondError(w, apiErrObj, errQueriesByName)
|
|
return
|
|
}
|
|
|
|
resp := v3.QueryRangeResponse{
|
|
Result: result,
|
|
}
|
|
aH.Respond(w, resp)
|
|
}
|
|
|
|
func (aH *APIHandler) ListIntegrations(
|
|
w http.ResponseWriter, r *http.Request,
|
|
) {
|
|
params := map[string]string{}
|
|
for k, values := range r.URL.Query() {
|
|
params[k] = values[0]
|
|
}
|
|
claims, errv2 := authtypes.ClaimsFromContext(r.Context())
|
|
if errv2 != nil {
|
|
render.Error(w, errv2)
|
|
return
|
|
}
|
|
|
|
resp, apiErr := aH.IntegrationsController.ListIntegrations(
|
|
r.Context(), claims.OrgID, params,
|
|
)
|
|
if apiErr != nil {
|
|
RespondError(w, apiErr, "Failed to fetch integrations")
|
|
return
|
|
}
|
|
aH.Respond(w, resp)
|
|
}
|
|
|
|
func (aH *APIHandler) GetIntegration(
|
|
w http.ResponseWriter, r *http.Request,
|
|
) {
|
|
integrationId := coretypes.Param(r, "integrationId")
|
|
claims, errv2 := authtypes.ClaimsFromContext(r.Context())
|
|
if errv2 != nil {
|
|
render.Error(w, errv2)
|
|
return
|
|
}
|
|
integration, apiErr := aH.IntegrationsController.GetIntegration(
|
|
r.Context(), claims.OrgID, integrationId,
|
|
)
|
|
if apiErr != nil {
|
|
RespondError(w, apiErr, "Failed to fetch integration details")
|
|
return
|
|
}
|
|
|
|
aH.Respond(w, integration)
|
|
}
|
|
|
|
func (aH *APIHandler) GetIntegrationConnectionStatus(w http.ResponseWriter, r *http.Request) {
|
|
claims, err := authtypes.ClaimsFromContext(r.Context())
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
orgID, err := valuer.NewUUID(claims.OrgID)
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
integrationId := coretypes.Param(r, "integrationId")
|
|
claims, errv2 := authtypes.ClaimsFromContext(r.Context())
|
|
if errv2 != nil {
|
|
render.Error(w, errv2)
|
|
return
|
|
}
|
|
isInstalled, apiErr := aH.IntegrationsController.IsIntegrationInstalled(
|
|
r.Context(), claims.OrgID, integrationId,
|
|
)
|
|
if apiErr != nil {
|
|
RespondError(w, apiErr, "failed to check if integration is installed")
|
|
return
|
|
}
|
|
|
|
// Do not spend resources calculating connection status unless installed.
|
|
if !isInstalled {
|
|
aH.Respond(w, &integrations.IntegrationConnectionStatus{})
|
|
return
|
|
}
|
|
|
|
connectionTests, apiErr := aH.IntegrationsController.GetIntegrationConnectionTests(
|
|
r.Context(), claims.OrgID, integrationId,
|
|
)
|
|
if apiErr != nil {
|
|
RespondError(w, apiErr, "failed to fetch integration connection tests")
|
|
return
|
|
}
|
|
|
|
lookbackSecondsStr := r.URL.Query().Get("lookback_seconds")
|
|
lookbackSeconds, err := strconv.ParseInt(lookbackSecondsStr, 10, 64)
|
|
if err != nil {
|
|
lookbackSeconds = 15 * 60
|
|
}
|
|
|
|
connectionStatus, apiErr := aH.calculateConnectionStatus(
|
|
r.Context(), orgID, connectionTests, lookbackSeconds,
|
|
)
|
|
if apiErr != nil {
|
|
RespondError(w, apiErr, "Failed to calculate integration connection status")
|
|
return
|
|
}
|
|
|
|
aH.Respond(w, connectionStatus)
|
|
}
|
|
|
|
func (aH *APIHandler) calculateConnectionStatus(
|
|
ctx context.Context,
|
|
orgID valuer.UUID,
|
|
connectionTests *integrations.IntegrationConnectionTests,
|
|
lookbackSeconds int64,
|
|
) (*integrations.IntegrationConnectionStatus, *model.ApiError) {
|
|
// Calculate connection status for signals in parallel
|
|
|
|
result := &integrations.IntegrationConnectionStatus{}
|
|
errors := []*model.ApiError{}
|
|
var resultLock sync.Mutex
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
// Calculate logs connection status
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
|
|
logsConnStatus, apiErr := aH.calculateLogsConnectionStatus(ctx, orgID, connectionTests.Logs, lookbackSeconds)
|
|
|
|
resultLock.Lock()
|
|
defer resultLock.Unlock()
|
|
|
|
if apiErr != nil {
|
|
errors = append(errors, apiErr)
|
|
} else {
|
|
result.Logs = logsConnStatus
|
|
}
|
|
}()
|
|
|
|
// Calculate metrics connection status
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
|
|
if len(connectionTests.Metrics) < 1 {
|
|
return
|
|
}
|
|
|
|
statusForLastReceivedMetric, apiErr := aH.reader.GetLatestReceivedMetric(
|
|
ctx, orgID, connectionTests.Metrics, nil,
|
|
)
|
|
|
|
resultLock.Lock()
|
|
defer resultLock.Unlock()
|
|
|
|
if apiErr != nil {
|
|
errors = append(errors, apiErr)
|
|
|
|
} else if statusForLastReceivedMetric != nil {
|
|
resourceSummaryParts := []string{}
|
|
for k, v := range statusForLastReceivedMetric.LastReceivedLabels {
|
|
interestingLabels := []string{
|
|
"container_name", "host_name", "node_name",
|
|
"pod_name", "deployment_name", "cluster_name",
|
|
"namespace_name", "job_name", "service_name",
|
|
}
|
|
isInterestingKey := !strings.HasPrefix(k, "_") && slices.ContainsFunc(
|
|
interestingLabels, func(l string) bool { return strings.Contains(k, l) },
|
|
)
|
|
if isInterestingKey {
|
|
resourceSummaryParts = append(resourceSummaryParts, fmt.Sprintf(
|
|
"%s=%s", k, v,
|
|
))
|
|
}
|
|
}
|
|
|
|
result.Metrics = &integrations.SignalConnectionStatus{
|
|
LastReceivedFrom: strings.Join(resourceSummaryParts, ", "),
|
|
LastReceivedTsMillis: statusForLastReceivedMetric.LastReceivedTsMillis,
|
|
}
|
|
}
|
|
}()
|
|
|
|
wg.Wait()
|
|
|
|
if len(errors) > 0 {
|
|
return nil, errors[0]
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func (aH *APIHandler) calculateLogsConnectionStatus(ctx context.Context, orgID valuer.UUID, logsConnectionTest *integrations.LogsConnectionTest, lookbackSeconds int64) (*integrations.SignalConnectionStatus, *model.ApiError) {
|
|
if logsConnectionTest == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
logsConnTestFilter := &v3.FilterSet{
|
|
Operator: "AND",
|
|
Items: []v3.FilterItem{
|
|
{
|
|
Key: v3.AttributeKey{
|
|
Key: logsConnectionTest.AttributeKey,
|
|
DataType: v3.AttributeKeyDataTypeString,
|
|
Type: v3.AttributeKeyTypeTag,
|
|
},
|
|
Operator: "=",
|
|
Value: logsConnectionTest.AttributeValue,
|
|
},
|
|
},
|
|
}
|
|
|
|
qrParams := &v3.QueryRangeParamsV3{
|
|
Start: time.Now().UnixMilli() - (lookbackSeconds * 1000),
|
|
End: time.Now().UnixMilli(),
|
|
CompositeQuery: &v3.CompositeQuery{
|
|
PanelType: v3.PanelTypeList,
|
|
QueryType: v3.QueryTypeBuilder,
|
|
BuilderQueries: map[string]*v3.BuilderQuery{
|
|
"A": {
|
|
PageSize: 1,
|
|
Filters: logsConnTestFilter,
|
|
QueryName: "A",
|
|
DataSource: v3.DataSourceLogs,
|
|
Expression: "A",
|
|
AggregateOperator: v3.AggregateOperatorNoOp,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
ctx = ctxtypes.NewContextWithCommentVals(ctx, map[string]string{
|
|
instrumentationtypes.CodeNamespace: "app",
|
|
instrumentationtypes.CodeFunctionName: "calculateLogsConnectionStatus",
|
|
})
|
|
queryRes, _, err := aH.querier.QueryRange(ctx, orgID, qrParams)
|
|
if err != nil {
|
|
return nil, model.InternalError(fmt.Errorf(
|
|
"could not query for integration connection status: %w", err,
|
|
))
|
|
}
|
|
if len(queryRes) > 0 && queryRes[0].List != nil && len(queryRes[0].List) > 0 {
|
|
lastLog := queryRes[0].List[0]
|
|
|
|
resourceSummaryParts := []string{}
|
|
lastLogResourceAttribs := lastLog.Data["resources_string"]
|
|
if lastLogResourceAttribs != nil {
|
|
resourceAttribs, ok := lastLogResourceAttribs.(*map[string]string)
|
|
if !ok {
|
|
return nil, model.InternalError(fmt.Errorf(
|
|
"could not cast log resource attribs",
|
|
))
|
|
}
|
|
for k, v := range *resourceAttribs {
|
|
resourceSummaryParts = append(resourceSummaryParts, fmt.Sprintf(
|
|
"%s=%s", k, v,
|
|
))
|
|
}
|
|
}
|
|
lastLogResourceSummary := strings.Join(resourceSummaryParts, ", ")
|
|
|
|
return &integrations.SignalConnectionStatus{
|
|
LastReceivedTsMillis: lastLog.Timestamp.UnixMilli(),
|
|
LastReceivedFrom: lastLogResourceSummary,
|
|
}, nil
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
func (aH *APIHandler) InstallIntegration(w http.ResponseWriter, r *http.Request) {
|
|
req := integrations.InstallIntegrationRequest{}
|
|
|
|
err := json.NewDecoder(r.Body).Decode(&req)
|
|
if err != nil {
|
|
RespondError(w, model.BadRequest(err), nil)
|
|
return
|
|
}
|
|
claims, err := authtypes.ClaimsFromContext(r.Context())
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
integration, apiErr := aH.IntegrationsController.Install(
|
|
r.Context(), claims.OrgID, &req, claims.Email, valuer.MustNewUUID(claims.IdentityID()),
|
|
)
|
|
if apiErr != nil {
|
|
RespondError(w, apiErr, nil)
|
|
return
|
|
}
|
|
|
|
aH.Respond(w, integration)
|
|
}
|
|
|
|
func (aH *APIHandler) UninstallIntegration(w http.ResponseWriter, r *http.Request) {
|
|
req := integrations.UninstallIntegrationRequest{}
|
|
|
|
err := json.NewDecoder(r.Body).Decode(&req)
|
|
if err != nil {
|
|
RespondError(w, model.BadRequest(err), nil)
|
|
return
|
|
}
|
|
|
|
claims, errv2 := authtypes.ClaimsFromContext(r.Context())
|
|
if errv2 != nil {
|
|
render.Error(w, errv2)
|
|
return
|
|
}
|
|
|
|
apiErr := aH.IntegrationsController.Uninstall(r.Context(), claims.OrgID, &req)
|
|
if apiErr != nil {
|
|
RespondError(w, apiErr, nil)
|
|
return
|
|
}
|
|
|
|
aH.Respond(w, map[string]interface{}{})
|
|
}
|
|
|
|
func (aH *APIHandler) logFields(w http.ResponseWriter, r *http.Request) {
|
|
fields, apiErr := aH.reader.GetLogFields(r.Context())
|
|
if apiErr != nil {
|
|
RespondError(w, apiErr, "Failed to fetch fields from the DB")
|
|
return
|
|
}
|
|
aH.WriteJSON(w, r, fields)
|
|
}
|
|
|
|
func (aH *APIHandler) logFieldUpdate(w http.ResponseWriter, r *http.Request) {
|
|
field := model.UpdateField{}
|
|
if err := json.NewDecoder(r.Body).Decode(&field); err != nil {
|
|
apiErr := &model.ApiError{Typ: model.ErrorBadData, Err: err}
|
|
RespondError(w, apiErr, "Failed to decode payload")
|
|
return
|
|
}
|
|
|
|
err := logs.ValidateUpdateFieldPayload(&field)
|
|
if err != nil {
|
|
apiErr := &model.ApiError{Typ: model.ErrorBadData, Err: err}
|
|
RespondError(w, apiErr, "Incorrect payload")
|
|
return
|
|
}
|
|
|
|
apiErr := aH.reader.UpdateLogField(r.Context(), &field)
|
|
if apiErr != nil {
|
|
RespondError(w, apiErr, "Failed to update field in the DB")
|
|
return
|
|
}
|
|
aH.WriteJSON(w, r, field)
|
|
}
|
|
|
|
// getLogs serves GET /v1/o11y/logs — the most recent logs over the
|
|
// query window, newest first. Reads the configured logs table via the reader
|
|
// (real Datastore rows), replacing the former {"results":[]} stub. Params:
|
|
// limit (default 100, max 1000), timestampStart / timestampEnd (nanosecond
|
|
// epochs; default the last 15 minutes). The read is the caller's tenant only;
|
|
// a request that carries none is 403. A read error surfaces as an error, never
|
|
// a fabricated empty list.
|
|
func (aH *APIHandler) getLogs(w http.ResponseWriter, r *http.Request) {
|
|
q := r.URL.Query()
|
|
limit := 100
|
|
if v, err := strconv.Atoi(strings.TrimSpace(q.Get("limit"))); err == nil && v > 0 {
|
|
limit = v
|
|
}
|
|
end := time.Now().UnixNano()
|
|
if v, err := strconv.ParseInt(strings.TrimSpace(q.Get("timestampEnd")), 10, 64); err == nil && v > 0 {
|
|
end = v
|
|
}
|
|
start := end - int64(15*time.Minute)
|
|
if v, err := strconv.ParseInt(strings.TrimSpace(q.Get("timestampStart")), 10, 64); err == nil && v > 0 {
|
|
start = v
|
|
}
|
|
|
|
if _, err := authtypes.TenantFromContext(r.Context()); err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorForbidden, Err: err}, nil)
|
|
return
|
|
}
|
|
rows, err := aH.reader.GetRecentLogs(r.Context(), start, end, limit)
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, "Failed to fetch logs from the DB")
|
|
return
|
|
}
|
|
|
|
results := make([]map[string]interface{}, 0, len(rows))
|
|
for _, row := range rows {
|
|
item := map[string]interface{}{"timestamp": row.Timestamp.UnixNano()}
|
|
for k, v := range row.Data {
|
|
item[k] = v
|
|
}
|
|
results = append(results, item)
|
|
}
|
|
aH.WriteJSON(w, r, map[string]interface{}{"results": results})
|
|
}
|
|
|
|
func (aH *APIHandler) logAggregate(w http.ResponseWriter, r *http.Request) {
|
|
aH.WriteJSON(w, r, model.GetLogsAggregatesResponse{})
|
|
}
|
|
|
|
func parseAgentConfigVersion(r *http.Request) (int, error) {
|
|
versionString := coretypes.Param(r, "version")
|
|
|
|
if versionString == "latest" {
|
|
return -1, nil
|
|
}
|
|
|
|
version64, err := strconv.ParseInt(versionString, 0, 8)
|
|
|
|
if err != nil {
|
|
return 0, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "invalid version number")
|
|
}
|
|
|
|
if version64 <= 0 {
|
|
return 0, errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid version number")
|
|
}
|
|
|
|
return int(version64), nil
|
|
}
|
|
|
|
func (aH *APIHandler) PreviewLogsPipelinesHandler(w http.ResponseWriter, r *http.Request) {
|
|
req := logparsingpipeline.PipelinesPreviewRequest{}
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
render.Error(w, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "failed to decode request body"))
|
|
return
|
|
}
|
|
|
|
resultLogs, err := aH.LogsParsingPipelineController.PreviewLogsPipelines(r.Context(), &req)
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
aH.Respond(w, resultLogs)
|
|
}
|
|
|
|
func (aH *APIHandler) ListLogsPipelinesHandler(w http.ResponseWriter, r *http.Request) {
|
|
claims, err := authtypes.ClaimsFromContext(r.Context())
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
orgID, errv2 := valuer.NewUUID(claims.OrgID)
|
|
if errv2 != nil {
|
|
render.Error(w, errv2)
|
|
return
|
|
}
|
|
|
|
version, err := parseAgentConfigVersion(r)
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
var payload *logparsingpipeline.PipelinesResponse
|
|
if version != -1 {
|
|
payload, err = aH.listLogsPipelinesByVersion(r.Context(), orgID, version)
|
|
} else {
|
|
payload, err = aH.listLogsPipelines(r.Context(), orgID)
|
|
}
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
aH.Respond(w, payload)
|
|
}
|
|
|
|
// listLogsPipelines lists logs pipelines for latest version
|
|
func (aH *APIHandler) listLogsPipelines(ctx context.Context, orgID valuer.UUID) (
|
|
*logparsingpipeline.PipelinesResponse, error,
|
|
) {
|
|
// get latest agent config
|
|
latestVersion := -1
|
|
lastestConfig, err := agentConf.GetLatestVersion(ctx, orgID, opamptypes.ElementTypeLogPipelines)
|
|
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
|
return nil, err
|
|
}
|
|
|
|
if lastestConfig != nil {
|
|
latestVersion = lastestConfig.Version
|
|
}
|
|
|
|
payload, err := aH.LogsParsingPipelineController.GetPipelinesByVersion(ctx, orgID, latestVersion)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// todo(Nitya): make a new API for history pagination
|
|
limit := 10
|
|
history, err := agentConf.GetConfigHistory(ctx, orgID, opamptypes.ElementTypeLogPipelines, limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
payload.History = history
|
|
return payload, nil
|
|
}
|
|
|
|
// listLogsPipelinesByVersion lists pipelines along with config version history
|
|
func (aH *APIHandler) listLogsPipelinesByVersion(ctx context.Context, orgID valuer.UUID, version int) (
|
|
*logparsingpipeline.PipelinesResponse, error,
|
|
) {
|
|
payload, err := aH.LogsParsingPipelineController.GetPipelinesByVersion(ctx, orgID, version)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// todo(Nitya): make a new API for history pagination
|
|
limit := 10
|
|
history, err := agentConf.GetConfigHistory(ctx, orgID, opamptypes.ElementTypeLogPipelines, limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
payload.History = history
|
|
return payload, nil
|
|
}
|
|
|
|
func (aH *APIHandler) CreateLogsPipeline(w http.ResponseWriter, r *http.Request) {
|
|
claims, errv2 := authtypes.ClaimsFromContext(r.Context())
|
|
if errv2 != nil {
|
|
render.Error(w, errv2)
|
|
return
|
|
}
|
|
|
|
// prepare config by calling gen func
|
|
orgID, errv2 := valuer.NewUUID(claims.OrgID)
|
|
if errv2 != nil {
|
|
render.Error(w, errv2)
|
|
return
|
|
}
|
|
userID, errv2 := valuer.NewUUID(claims.UserID)
|
|
if errv2 != nil {
|
|
render.Error(w, errv2)
|
|
return
|
|
}
|
|
|
|
req := pipelinetypes.PostablePipelines{}
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
RespondError(w, model.BadRequest(err), nil)
|
|
return
|
|
}
|
|
|
|
createPipeline := func(
|
|
ctx context.Context,
|
|
postable []pipelinetypes.PostablePipeline,
|
|
) (*logparsingpipeline.PipelinesResponse, error) {
|
|
if len(postable) == 0 {
|
|
aH.logger.WarnContext(r.Context(), "found no pipelines in the http request, this will delete all the pipelines")
|
|
}
|
|
|
|
err := aH.LogsParsingPipelineController.ValidatePipelines(ctx, postable)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return aH.LogsParsingPipelineController.ApplyPipelines(ctx, orgID, userID, postable)
|
|
}
|
|
|
|
res, err := createPipeline(r.Context(), req.Pipelines)
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
aH.Respond(w, res)
|
|
}
|
|
|
|
func (aH *APIHandler) autocompleteAggregateAttributes(w http.ResponseWriter, r *http.Request) {
|
|
claims, err := authtypes.ClaimsFromContext(r.Context())
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
orgID, err := valuer.NewUUID(claims.OrgID)
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
var response *v3.AggregateAttributeResponse
|
|
req, err := parseAggregateAttributeRequest(r)
|
|
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
|
|
return
|
|
}
|
|
|
|
switch req.DataSource {
|
|
case v3.DataSourceMetrics:
|
|
response, err = aH.reader.GetMetricAggregateAttributes(r.Context(), orgID, req, false)
|
|
case v3.DataSourceLogs:
|
|
response, err = aH.reader.GetLogAggregateAttributes(r.Context(), req)
|
|
case v3.DataSourceTraces:
|
|
response, err = aH.reader.GetTraceAggregateAttributes(r.Context(), req)
|
|
case v3.DataSourceMeter:
|
|
response, err = aH.reader.GetMeterAggregateAttributes(r.Context(), orgID, req)
|
|
default:
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("invalid data source")}, nil)
|
|
return
|
|
}
|
|
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
|
|
return
|
|
}
|
|
|
|
aH.Respond(w, response)
|
|
}
|
|
|
|
func (aH *APIHandler) getQueryBuilderSuggestions(w http.ResponseWriter, r *http.Request) {
|
|
req, err := parseQBFilterSuggestionsRequest(r)
|
|
if err != nil {
|
|
RespondError(w, err, nil)
|
|
return
|
|
}
|
|
|
|
if req.DataSource != v3.DataSourceLogs {
|
|
// Support for traces and metrics might come later
|
|
RespondError(w, model.BadRequest(
|
|
fmt.Errorf("suggestions not supported for %s", req.DataSource),
|
|
), nil)
|
|
return
|
|
}
|
|
|
|
response, apiErr := aH.reader.GetQBFilterSuggestionsForLogs(r.Context(), req)
|
|
if apiErr != nil {
|
|
RespondError(w, apiErr, nil)
|
|
return
|
|
}
|
|
|
|
aH.Respond(w, response)
|
|
}
|
|
|
|
func (aH *APIHandler) autoCompleteAttributeKeys(w http.ResponseWriter, r *http.Request) {
|
|
var response *v3.FilterAttributeKeyResponse
|
|
req, err := parseFilterAttributeKeyRequest(r)
|
|
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
|
|
return
|
|
}
|
|
|
|
claims, err := authtypes.ClaimsFromContext(r.Context())
|
|
if err != nil {
|
|
aH.HandleError(w, err, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
orgID := valuer.MustNewUUID(claims.OrgID)
|
|
|
|
switch req.DataSource {
|
|
case v3.DataSourceMetrics:
|
|
response, err = aH.reader.GetMetricAttributeKeys(r.Context(), orgID, req)
|
|
case v3.DataSourceMeter:
|
|
response, err = aH.reader.GetMeterAttributeKeys(r.Context(), req)
|
|
case v3.DataSourceLogs:
|
|
response, err = aH.reader.GetLogAttributeKeys(r.Context(), req)
|
|
case v3.DataSourceTraces:
|
|
response, err = aH.reader.GetTraceAttributeKeys(r.Context(), req)
|
|
default:
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("invalid data source")}, nil)
|
|
return
|
|
}
|
|
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
|
|
return
|
|
}
|
|
|
|
aH.Respond(w, response)
|
|
}
|
|
|
|
func (aH *APIHandler) autoCompleteAttributeValues(w http.ResponseWriter, r *http.Request) {
|
|
var response *v3.FilterAttributeValueResponse
|
|
req, err := parseFilterAttributeValueRequest(r)
|
|
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
|
|
return
|
|
}
|
|
|
|
claims, err := authtypes.ClaimsFromContext(r.Context())
|
|
if err != nil {
|
|
aH.HandleError(w, err, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
orgID := valuer.MustNewUUID(claims.OrgID)
|
|
|
|
switch req.DataSource {
|
|
case v3.DataSourceMetrics:
|
|
response, err = aH.reader.GetMetricAttributeValues(r.Context(), orgID, req)
|
|
case v3.DataSourceLogs:
|
|
response, err = aH.reader.GetLogAttributeValues(r.Context(), req)
|
|
case v3.DataSourceTraces:
|
|
response, err = aH.reader.GetTraceAttributeValues(r.Context(), req)
|
|
default:
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("invalid data source")}, nil)
|
|
return
|
|
}
|
|
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
|
|
return
|
|
}
|
|
|
|
aH.Respond(w, response)
|
|
}
|
|
|
|
func (aH *APIHandler) autoCompleteAttributeValuesPost(w http.ResponseWriter, r *http.Request) {
|
|
var response *v3.FilterAttributeValueResponse
|
|
req, err := parseFilterAttributeValueRequestBody(r)
|
|
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
|
|
return
|
|
}
|
|
|
|
claims, err := authtypes.ClaimsFromContext(r.Context())
|
|
if err != nil {
|
|
aH.HandleError(w, err, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
orgID := valuer.MustNewUUID(claims.OrgID)
|
|
|
|
switch req.DataSource {
|
|
case v3.DataSourceMetrics:
|
|
response, err = aH.reader.GetMetricAttributeValues(r.Context(), orgID, req)
|
|
case v3.DataSourceLogs:
|
|
response, err = aH.reader.GetLogAttributeValues(r.Context(), req)
|
|
case v3.DataSourceTraces:
|
|
response, err = aH.reader.GetTraceAttributeValues(r.Context(), req)
|
|
default:
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("invalid data source")}, nil)
|
|
return
|
|
}
|
|
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
|
|
return
|
|
}
|
|
|
|
aH.Respond(w, response)
|
|
}
|
|
|
|
func (aH *APIHandler) getSpanKeysV3(ctx context.Context, queryRangeParams *v3.QueryRangeParamsV3) (map[string]v3.AttributeKey, error) {
|
|
data := map[string]v3.AttributeKey{}
|
|
for _, query := range queryRangeParams.CompositeQuery.BuilderQueries {
|
|
if query.DataSource == v3.DataSourceTraces {
|
|
spanKeys, err := aH.reader.GetSpanAttributeKeysByNames(ctx, logsv3.GetFieldNames(queryRangeParams.CompositeQuery))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Add timestamp as a span key to allow ordering by timestamp
|
|
spanKeys["timestamp"] = v3.AttributeKey{
|
|
Key: "timestamp",
|
|
IsColumn: true,
|
|
}
|
|
return spanKeys, nil
|
|
}
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
func (aH *APIHandler) QueryRangeV3Format(w http.ResponseWriter, r *http.Request) {
|
|
queryRangeParams, apiErrorObj := ParseQueryRangeParams(r)
|
|
|
|
if apiErrorObj != nil {
|
|
aH.logger.ErrorContext(r.Context(), "error parsing query range params", errors.Attr(apiErrorObj.Err))
|
|
RespondError(w, apiErrorObj, nil)
|
|
return
|
|
}
|
|
queryRangeParams.Version = "v3"
|
|
|
|
aH.Respond(w, queryRangeParams)
|
|
}
|
|
|
|
func (aH *APIHandler) queryRangeV3(ctx context.Context, queryRangeParams *v3.QueryRangeParamsV3, w http.ResponseWriter, r *http.Request) {
|
|
claims, err := authtypes.ClaimsFromContext(r.Context())
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
orgID, err := valuer.NewUUID(claims.OrgID)
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
var result []*v3.Result
|
|
var errQueriesByName map[string]error
|
|
var spanKeys map[string]v3.AttributeKey
|
|
if queryRangeParams.CompositeQuery.QueryType == v3.QueryTypeBuilder {
|
|
hasLogsQuery := false
|
|
hasTracesQuery := false
|
|
for _, query := range queryRangeParams.CompositeQuery.BuilderQueries {
|
|
if query.DataSource == v3.DataSourceLogs {
|
|
hasLogsQuery = true
|
|
}
|
|
if query.DataSource == v3.DataSourceTraces {
|
|
hasTracesQuery = true
|
|
}
|
|
}
|
|
// check if any enrichment is required for logs if yes then enrich them
|
|
if logsv3.EnrichmentRequired(queryRangeParams) && hasLogsQuery {
|
|
logsFields, apiErr := aH.reader.GetLogFieldsFromNames(ctx, logsv3.GetFieldNames(queryRangeParams.CompositeQuery))
|
|
if apiErr != nil {
|
|
RespondError(w, apiErr, errQueriesByName)
|
|
return
|
|
}
|
|
// get the fields if any logs query is present
|
|
fields := model.GetLogFieldsV3(ctx, queryRangeParams, logsFields)
|
|
logsv3.Enrich(queryRangeParams, fields)
|
|
}
|
|
if hasTracesQuery {
|
|
spanKeys, err = aH.getSpanKeysV3(ctx, queryRangeParams)
|
|
if err != nil {
|
|
apiErrObj := &model.ApiError{Typ: model.ErrorInternal, Err: err}
|
|
RespondError(w, apiErrObj, errQueriesByName)
|
|
return
|
|
}
|
|
tracesV4.Enrich(queryRangeParams, spanKeys)
|
|
}
|
|
}
|
|
|
|
// WARN: Only works for AND operator in traces query
|
|
if queryRangeParams.CompositeQuery.QueryType == v3.QueryTypeBuilder {
|
|
// check if traceID is used as filter (with equal/similar operator) in traces query if yes add timestamp filter to queryRange params
|
|
isUsed, traceIDs := tracesV3.TraceIdFilterUsedWithEqual(queryRangeParams)
|
|
if isUsed && len(traceIDs) > 0 {
|
|
aH.logger.DebugContext(ctx, "trace_id used as filter in traces query")
|
|
// query o11y_spans table with traceID to get min and max timestamp
|
|
min, max, err := aH.reader.GetMinAndMaxTimestampForTraceID(ctx, traceIDs)
|
|
if err == nil {
|
|
// add timestamp filter to queryRange params
|
|
tracesV3.AddTimestampFilters(min, max, queryRangeParams)
|
|
aH.logger.DebugContext(ctx, "post adding timestamp filter in traces query", "query_range_params", queryRangeParams)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Hook up query progress tracking if requested
|
|
queryIdHeader := r.Header.Get("X-O11Y-QUERY-ID")
|
|
if len(queryIdHeader) > 0 {
|
|
onQueryFinished, apiErr := aH.reader.ReportQueryStartForProgressTracking(queryIdHeader)
|
|
|
|
if apiErr != nil {
|
|
aH.logger.ErrorContext(ctx, "failed to report query start for progress tracking",
|
|
"query_id", queryIdHeader, errors.Attr(apiErr),
|
|
)
|
|
|
|
} else {
|
|
// Adding queryId to the context signals datastore queries to report progress
|
|
//lint:ignore SA1029 ignore for now
|
|
ctx = context.WithValue(ctx, "queryId", queryIdHeader)
|
|
|
|
defer func() {
|
|
go onQueryFinished()
|
|
}()
|
|
}
|
|
}
|
|
|
|
ctx = ctxtypes.NewContextWithCommentVals(ctx, map[string]string{
|
|
instrumentationtypes.CodeNamespace: "app",
|
|
instrumentationtypes.CodeFunctionName: "QueryRange",
|
|
})
|
|
result, errQueriesByName, err = aH.querier.QueryRange(ctx, orgID, queryRangeParams)
|
|
|
|
if err != nil {
|
|
queryErrors := map[string]string{}
|
|
for name, err := range errQueriesByName {
|
|
queryErrors[fmt.Sprintf("Query-%s", name)] = err.Error()
|
|
}
|
|
apiErrObj := &model.ApiError{Typ: model.ErrorInternal, Err: err}
|
|
RespondError(w, apiErrObj, queryErrors)
|
|
return
|
|
}
|
|
|
|
postprocess.ApplyHavingClause(result, queryRangeParams)
|
|
postprocess.ApplyMetricLimit(result, queryRangeParams)
|
|
|
|
aH.sendQueryResultEvents(r, result, queryRangeParams, "v3")
|
|
// only adding applyFunctions instead of postProcess since experssion are
|
|
// are executed in datastore directly and we wanted to add support for timeshift
|
|
if queryRangeParams.CompositeQuery.QueryType == v3.QueryTypeBuilder {
|
|
postprocess.ApplyFunctions(result, queryRangeParams)
|
|
}
|
|
|
|
if queryRangeParams.CompositeQuery.FillGaps {
|
|
postprocess.FillGaps(result, queryRangeParams)
|
|
}
|
|
|
|
if queryRangeParams.CompositeQuery.PanelType == v3.PanelTypeTable && queryRangeParams.FormatForWeb {
|
|
if queryRangeParams.CompositeQuery.QueryType == v3.QueryTypeDatastoreSQL {
|
|
result = postprocess.TransformToTableForDatastoreQueries(result)
|
|
} else if queryRangeParams.CompositeQuery.QueryType == v3.QueryTypeBuilder {
|
|
result = postprocess.TransformToTableForBuilderQueries(result, queryRangeParams)
|
|
}
|
|
}
|
|
|
|
resp := v3.QueryRangeResponse{
|
|
Result: result,
|
|
}
|
|
|
|
// This checks if the time for context to complete has exceeded.
|
|
// it adds flag to notify the user of incomplete response
|
|
select {
|
|
case <-ctx.Done():
|
|
resp.ContextTimeout = true
|
|
resp.ContextTimeoutMessage = "result might contain incomplete data due to context timeout, for custom timeout set the timeout header eg:- timeout:120"
|
|
default:
|
|
break
|
|
}
|
|
|
|
aH.Respond(w, resp)
|
|
}
|
|
|
|
func (aH *APIHandler) sendQueryResultEvents(r *http.Request, result []*v3.Result, queryRangeParams *v3.QueryRangeParamsV3, version string) {
|
|
claims, err := authtypes.ClaimsFromContext(r.Context())
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
queryInfoResult := NewQueryInfoResult(queryRangeParams, version)
|
|
|
|
if !(queryInfoResult.LogsUsed || queryInfoResult.MetricsUsed || queryInfoResult.TracesUsed) {
|
|
return
|
|
}
|
|
|
|
properties := queryInfoResult.ToMap()
|
|
referrer := r.Header.Get("Referer")
|
|
|
|
if referrer == "" {
|
|
return
|
|
}
|
|
|
|
properties["referrer"] = referrer
|
|
|
|
logsExplorerMatched, _ := regexp.MatchString(`/logs/logs-explorer(?:\?.*)?$`, referrer)
|
|
traceExplorerMatched, _ := regexp.MatchString(`/traces-explorer(?:\?.*)?$`, referrer)
|
|
metricsExplorerMatched, _ := regexp.MatchString(`/metrics-explorer/explorer(?:\?.*)?$`, referrer)
|
|
dashboardMatched, _ := regexp.MatchString(`/dashboard/[a-zA-Z0-9\-]+/(new|edit)(?:\?.*)?$`, referrer)
|
|
alertMatched, _ := regexp.MatchString(`/alerts/(new|edit)(?:\?.*)?$`, referrer)
|
|
|
|
switch {
|
|
case dashboardMatched:
|
|
properties["module_name"] = "dashboard"
|
|
case alertMatched:
|
|
properties["module_name"] = "rule"
|
|
case metricsExplorerMatched:
|
|
properties["module_name"] = "metrics-explorer"
|
|
case logsExplorerMatched:
|
|
properties["module_name"] = "logs-explorer"
|
|
case traceExplorerMatched:
|
|
properties["module_name"] = "traces-explorer"
|
|
default:
|
|
return
|
|
}
|
|
|
|
if dashboardMatched {
|
|
if dashboardIDRegex, err := regexp.Compile(`/dashboard/([a-f0-9\-]+)/`); err == nil {
|
|
if matches := dashboardIDRegex.FindStringSubmatch(referrer); len(matches) > 1 {
|
|
properties["dashboard_id"] = matches[1]
|
|
}
|
|
}
|
|
|
|
if widgetIDRegex, err := regexp.Compile(`widgetId=([a-f0-9\-]+)`); err == nil {
|
|
if matches := widgetIDRegex.FindStringSubmatch(referrer); len(matches) > 1 {
|
|
properties["widget_id"] = matches[1]
|
|
}
|
|
}
|
|
}
|
|
|
|
if alertMatched {
|
|
if alertIDRegex, err := regexp.Compile(`ruleId=(\d+)`); err == nil {
|
|
if matches := alertIDRegex.FindStringSubmatch(referrer); len(matches) > 1 {
|
|
properties["rule_id"] = matches[1]
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if result is empty or has no data
|
|
if len(result) == 0 {
|
|
aH.O11y.Analytics.TrackUser(r.Context(), claims.OrgID, claims.IdentityID(), "Telemetry Query Returned Empty", properties)
|
|
return
|
|
}
|
|
|
|
// Check if first result has no series data
|
|
if len(result[0].Series) == 0 {
|
|
// Check if first result has no list data
|
|
if len(result[0].List) == 0 {
|
|
// Check if first result has no table data
|
|
if result[0].Table == nil {
|
|
aH.O11y.Analytics.TrackUser(r.Context(), claims.OrgID, claims.IdentityID(), "Telemetry Query Returned Empty", properties)
|
|
return
|
|
}
|
|
|
|
if len(result[0].Table.Rows) == 0 {
|
|
aH.O11y.Analytics.TrackUser(r.Context(), claims.OrgID, claims.IdentityID(), "Telemetry Query Returned Empty", properties)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
aH.O11y.Analytics.TrackUser(r.Context(), claims.OrgID, claims.IdentityID(), "Telemetry Query Returned Results", properties)
|
|
|
|
}
|
|
|
|
func (aH *APIHandler) GetQueryProgressUpdates(w http.ResponseWriter, r *http.Request) {
|
|
// Upgrade connection to websocket, sending back the requested protocol
|
|
// value for sec-websocket-protocol
|
|
//
|
|
// Since js websocket API doesn't allow setting headers, this header is often
|
|
// used for passing auth tokens. As per websocket spec the connection will only
|
|
// succeed if the requested `Sec-Websocket-Protocol` is sent back as a header
|
|
// in the upgrade response (signifying that the protocol is supported by the server).
|
|
upgradeResponseHeaders := http.Header{}
|
|
requestedProtocol := r.Header.Get("Sec-WebSocket-Protocol")
|
|
if len(requestedProtocol) > 0 {
|
|
upgradeResponseHeaders.Add("Sec-WebSocket-Protocol", requestedProtocol)
|
|
}
|
|
|
|
c, err := aH.Upgrader.Upgrade(w, r, upgradeResponseHeaders)
|
|
if err != nil {
|
|
RespondError(w, model.InternalError(fmt.Errorf(
|
|
"couldn't upgrade connection: %w", err,
|
|
)), nil)
|
|
return
|
|
}
|
|
defer c.Close()
|
|
|
|
// Websocket upgrade complete. Subscribe to query progress and send updates to client
|
|
//
|
|
// Note: we handle any subscription problems (queryId query param missing or query already complete etc)
|
|
// after the websocket connection upgrade by closing the channel.
|
|
// The other option would be to handle the errors before websocket upgrade by sending an
|
|
// error response instead of the upgrade response, but that leads to a generic websocket
|
|
// connection failure on the client.
|
|
|
|
queryId := r.URL.Query().Get("q")
|
|
|
|
progressCh, unsubscribe, apiErr := aH.reader.SubscribeToQueryProgress(queryId)
|
|
if apiErr != nil {
|
|
// Shouldn't happen unless query progress requested after query finished
|
|
aH.logger.WarnContext(r.Context(), "failed to subscribe to query progress",
|
|
"query_id", queryId, errors.Attr(apiErr),
|
|
)
|
|
return
|
|
}
|
|
defer func() { go unsubscribe() }()
|
|
|
|
for queryProgress := range progressCh {
|
|
msg, err := json.Marshal(queryProgress)
|
|
if err != nil {
|
|
aH.logger.ErrorContext(r.Context(), "failed to serialize progress message",
|
|
"query_id", queryId, "progress", queryProgress, errors.Attr(err),
|
|
)
|
|
continue
|
|
}
|
|
|
|
err = c.WriteMessage(websocket.TextMessage, msg)
|
|
if err != nil {
|
|
aH.logger.ErrorContext(r.Context(), "failed to write progress message to websocket",
|
|
"query_id", queryId, "msg", string(msg), errors.Attr(err),
|
|
)
|
|
break
|
|
|
|
} else {
|
|
aH.logger.DebugContext(r.Context(), "wrote progress message to websocket",
|
|
"query_id", queryId, "msg", string(msg),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (aH *APIHandler) getMetricMetadata(w http.ResponseWriter, r *http.Request) {
|
|
claims, err := authtypes.ClaimsFromContext(r.Context())
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
orgID, err := valuer.NewUUID(claims.OrgID)
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
metricName := r.URL.Query().Get("metricName")
|
|
serviceName := r.URL.Query().Get("serviceName")
|
|
metricMetadata, err := aH.reader.GetMetricMetadata(r.Context(), orgID, metricName, serviceName)
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Err: err, Typ: model.ErrorInternal}, nil)
|
|
return
|
|
}
|
|
|
|
aH.WriteJSON(w, r, metricMetadata)
|
|
}
|
|
|
|
func (aH *APIHandler) queryRangeV4(ctx context.Context, queryRangeParams *v3.QueryRangeParamsV3, w http.ResponseWriter, r *http.Request) {
|
|
claims, err := authtypes.ClaimsFromContext(r.Context())
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
orgID, err := valuer.NewUUID(claims.OrgID)
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
var result []*v3.Result
|
|
var errQueriesByName map[string]error
|
|
var spanKeys map[string]v3.AttributeKey
|
|
if queryRangeParams.CompositeQuery.QueryType == v3.QueryTypeBuilder {
|
|
hasLogsQuery := false
|
|
hasTracesQuery := false
|
|
for _, query := range queryRangeParams.CompositeQuery.BuilderQueries {
|
|
if query.DataSource == v3.DataSourceLogs {
|
|
hasLogsQuery = true
|
|
}
|
|
if query.DataSource == v3.DataSourceTraces {
|
|
hasTracesQuery = true
|
|
}
|
|
}
|
|
|
|
// check if any enrichment is required for logs if yes then enrich them
|
|
if logsv3.EnrichmentRequired(queryRangeParams) && hasLogsQuery {
|
|
// get the fields if any logs query is present
|
|
logsFields, apiErr := aH.reader.GetLogFieldsFromNames(r.Context(), logsv3.GetFieldNames(queryRangeParams.CompositeQuery))
|
|
if apiErr != nil {
|
|
RespondError(w, apiErr, nil)
|
|
return
|
|
}
|
|
fields := model.GetLogFieldsV3(r.Context(), queryRangeParams, logsFields)
|
|
logsv3.Enrich(queryRangeParams, fields)
|
|
}
|
|
|
|
if hasTracesQuery {
|
|
spanKeys, err = aH.getSpanKeysV3(ctx, queryRangeParams)
|
|
if err != nil {
|
|
apiErrObj := &model.ApiError{Typ: model.ErrorInternal, Err: err}
|
|
RespondError(w, apiErrObj, errQueriesByName)
|
|
return
|
|
}
|
|
tracesV4.Enrich(queryRangeParams, spanKeys)
|
|
}
|
|
}
|
|
|
|
// WARN: Only works for AND operator in traces query
|
|
if queryRangeParams.CompositeQuery.QueryType == v3.QueryTypeBuilder {
|
|
// check if traceID is used as filter (with equal/similar operator) in traces query if yes add timestamp filter to queryRange params
|
|
isUsed, traceIDs := tracesV3.TraceIdFilterUsedWithEqual(queryRangeParams)
|
|
if isUsed && len(traceIDs) > 0 {
|
|
aH.logger.DebugContext(ctx, "trace_id used as filter in traces query")
|
|
// query o11y_spans table with traceID to get min and max timestamp
|
|
min, max, err := aH.reader.GetMinAndMaxTimestampForTraceID(ctx, traceIDs)
|
|
if err == nil {
|
|
// add timestamp filter to queryRange params
|
|
tracesV3.AddTimestampFilters(min, max, queryRangeParams)
|
|
aH.logger.DebugContext(ctx, "post adding timestamp filter in traces query", "query_range_params", queryRangeParams)
|
|
}
|
|
}
|
|
}
|
|
|
|
result, errQueriesByName, err = aH.querierV2.QueryRange(ctx, orgID, queryRangeParams)
|
|
|
|
if err != nil {
|
|
queryErrors := map[string]string{}
|
|
for name, err := range errQueriesByName {
|
|
queryErrors[fmt.Sprintf("Query-%s", name)] = err.Error()
|
|
}
|
|
apiErrObj := &model.ApiError{Typ: model.ErrorInternal, Err: err}
|
|
RespondError(w, apiErrObj, queryErrors)
|
|
return
|
|
}
|
|
|
|
if queryRangeParams.CompositeQuery.QueryType == v3.QueryTypeBuilder {
|
|
result, err = postprocess.PostProcessResult(result, queryRangeParams)
|
|
} else if queryRangeParams.CompositeQuery.QueryType == v3.QueryTypeDatastoreSQL &&
|
|
queryRangeParams.CompositeQuery.PanelType == v3.PanelTypeTable && queryRangeParams.FormatForWeb {
|
|
result = postprocess.TransformToTableForDatastoreQueries(result)
|
|
}
|
|
|
|
if err != nil {
|
|
apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err}
|
|
RespondError(w, apiErrObj, errQueriesByName)
|
|
return
|
|
}
|
|
aH.sendQueryResultEvents(r, result, queryRangeParams, "v4")
|
|
resp := v3.QueryRangeResponse{
|
|
Result: result,
|
|
}
|
|
|
|
aH.Respond(w, resp)
|
|
}
|
|
|
|
func (aH *APIHandler) traceFields(w http.ResponseWriter, r *http.Request) {
|
|
fields, apiErr := aH.reader.GetTraceFields(r.Context())
|
|
if apiErr != nil {
|
|
RespondError(w, apiErr, "failed to fetch fields from the db")
|
|
return
|
|
}
|
|
aH.WriteJSON(w, r, fields)
|
|
}
|
|
|
|
func (aH *APIHandler) updateTraceField(w http.ResponseWriter, r *http.Request) {
|
|
field := model.UpdateField{}
|
|
if err := json.NewDecoder(r.Body).Decode(&field); err != nil {
|
|
apiErr := &model.ApiError{Typ: model.ErrorBadData, Err: err}
|
|
RespondError(w, apiErr, "failed to decode payload")
|
|
return
|
|
}
|
|
|
|
err := logs.ValidateUpdateFieldPayloadV2(&field)
|
|
if err != nil {
|
|
apiErr := &model.ApiError{Typ: model.ErrorBadData, Err: err}
|
|
RespondError(w, apiErr, "incorrect payload")
|
|
return
|
|
}
|
|
|
|
apiErr := aH.reader.UpdateTraceField(r.Context(), &field)
|
|
if apiErr != nil {
|
|
RespondError(w, apiErr, "failed to update field in the db")
|
|
return
|
|
}
|
|
aH.WriteJSON(w, r, field)
|
|
}
|
|
|
|
func (aH *APIHandler) getQueueOverview(w http.ResponseWriter, r *http.Request) {
|
|
|
|
queueListRequest, apiErr := ParseQueueBody(r)
|
|
|
|
if apiErr != nil {
|
|
aH.logger.ErrorContext(r.Context(), "failed to parse queue body", errors.Attr(apiErr.Err))
|
|
RespondError(w, apiErr, nil)
|
|
return
|
|
}
|
|
|
|
chq, err := queues2.BuildOverviewQuery(queueListRequest)
|
|
|
|
if err != nil {
|
|
aH.logger.ErrorContext(r.Context(), "failed to build queue overview query", errors.Attr(err))
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: fmt.Errorf("error building datastore query: %v", err)}, nil)
|
|
return
|
|
}
|
|
|
|
results, err := aH.reader.GetListResultV3(r.Context(), chq.Query)
|
|
if err != nil {
|
|
RespondError(w, model.BadRequest(err), nil)
|
|
return
|
|
}
|
|
|
|
aH.Respond(w, results)
|
|
}
|
|
|
|
func (aH *APIHandler) getDomainList(w http.ResponseWriter, r *http.Request) {
|
|
// Extract claims from context for organization ID
|
|
claims, err := authtypes.ClaimsFromContext(r.Context())
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
orgID, err := valuer.NewUUID(claims.OrgID)
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
// Parse the request body to get third-party query parameters
|
|
thirdPartyQueryRequest, apiErr := ParseRequestBody(r)
|
|
if apiErr != nil {
|
|
aH.logger.ErrorContext(r.Context(), "failed to parse request body", errors.Attr(apiErr))
|
|
render.Error(w, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, apiErr.Error()))
|
|
return
|
|
}
|
|
|
|
// Build the v5 query range request for domain listing
|
|
queryRangeRequest, err := thirdpartyapi.BuildDomainList(thirdPartyQueryRequest)
|
|
if err != nil {
|
|
aH.logger.ErrorContext(r.Context(), "failed to build domain list query", errors.Attr(err))
|
|
apiErrObj := errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, err.Error())
|
|
render.Error(w, apiErrObj)
|
|
return
|
|
}
|
|
|
|
ctx := ctxtypes.NewContextWithCommentVals(r.Context(), map[string]string{
|
|
instrumentationtypes.CodeNamespace: "app",
|
|
instrumentationtypes.CodeFunctionName: "getDomainList",
|
|
})
|
|
// Execute the query using the v5 querier
|
|
result, err := aH.O11y.Querier.QueryRange(ctx, orgID, queryRangeRequest)
|
|
if err != nil {
|
|
aH.logger.ErrorContext(r.Context(), "query execution failed", errors.Attr(err))
|
|
apiErrObj := errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, err.Error())
|
|
render.Error(w, apiErrObj)
|
|
return
|
|
}
|
|
|
|
result = thirdpartyapi.FilterIntermediateColumns(result)
|
|
|
|
// Filter IP addresses if ShowIp is false
|
|
var finalResult = result
|
|
if !thirdPartyQueryRequest.ShowIp {
|
|
filteredResults := thirdpartyapi.FilterResponse([]*qbtypes.QueryRangeResponse{result})
|
|
if len(filteredResults) > 0 {
|
|
finalResult = filteredResults[0]
|
|
}
|
|
}
|
|
|
|
// Send the response
|
|
aH.Respond(w, finalResult)
|
|
}
|
|
|
|
// getDomainInfo handles requests for domain information using v5 query builder
|
|
func (aH *APIHandler) getDomainInfo(w http.ResponseWriter, r *http.Request) {
|
|
// Extract claims from context for organization ID
|
|
claims, err := authtypes.ClaimsFromContext(r.Context())
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
orgID, err := valuer.NewUUID(claims.OrgID)
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
// Parse the request body to get third-party query parameters
|
|
thirdPartyQueryRequest, apiErr := ParseRequestBody(r)
|
|
if apiErr != nil {
|
|
aH.logger.ErrorContext(r.Context(), "failed to parse request body", errors.Attr(apiErr))
|
|
render.Error(w, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, apiErr.Error()))
|
|
return
|
|
}
|
|
|
|
// Build the v5 query range request for domain info
|
|
queryRangeRequest, err := thirdpartyapi.BuildDomainInfo(thirdPartyQueryRequest)
|
|
if err != nil {
|
|
aH.logger.ErrorContext(r.Context(), "failed to build domain info query", errors.Attr(err))
|
|
apiErrObj := errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, err.Error())
|
|
render.Error(w, apiErrObj)
|
|
return
|
|
}
|
|
|
|
ctx := ctxtypes.NewContextWithCommentVals(r.Context(), map[string]string{
|
|
instrumentationtypes.CodeNamespace: "app",
|
|
instrumentationtypes.CodeFunctionName: "getDomainInfo",
|
|
})
|
|
// Execute the query using the v5 querier
|
|
result, err := aH.O11y.Querier.QueryRange(ctx, orgID, queryRangeRequest)
|
|
if err != nil {
|
|
aH.logger.ErrorContext(r.Context(), "query execution failed", errors.Attr(err))
|
|
apiErrObj := errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, err.Error())
|
|
render.Error(w, apiErrObj)
|
|
return
|
|
}
|
|
|
|
result = thirdpartyapi.FilterIntermediateColumns(result)
|
|
|
|
// Filter IP addresses if ShowIp is false
|
|
var finalResult *qbtypes.QueryRangeResponse = result
|
|
if !thirdPartyQueryRequest.ShowIp {
|
|
filteredResults := thirdpartyapi.FilterResponse([]*qbtypes.QueryRangeResponse{result})
|
|
if len(filteredResults) > 0 {
|
|
finalResult = filteredResults[0]
|
|
}
|
|
}
|
|
|
|
// Send the response
|
|
aH.Respond(w, finalResult)
|
|
}
|
|
|
|
func (aH *APIHandler) handleValidateTraces(w http.ResponseWriter, r *http.Request) {
|
|
funnelID := coretypes.Param(r, "funnel_id")
|
|
|
|
claims, err := authtypes.ClaimsFromContext(r.Context())
|
|
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
funnel, err := aH.O11y.Modules.TraceFunnel.Get(r.Context(), valuer.MustNewUUID(funnelID), valuer.MustNewUUID(claims.OrgID))
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("funnel not found: %v", err)}, nil)
|
|
return
|
|
}
|
|
|
|
var timeRange traceFunnels.TimeRange
|
|
if err := json.NewDecoder(r.Body).Decode(&timeRange); err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("error decoding time range: %v", err)}, nil)
|
|
return
|
|
}
|
|
|
|
if len(funnel.Steps) < 2 {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("funnel must have at least 2 steps")}, nil)
|
|
return
|
|
}
|
|
|
|
chq, err := traceFunnelsModule.ValidateTraces(funnel, timeRange)
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: fmt.Errorf("error building datastore query: %v", err)}, nil)
|
|
return
|
|
}
|
|
|
|
results, err := aH.reader.GetListResultV3(r.Context(), chq.Query)
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: fmt.Errorf("error converting datastore results to list: %v", err)}, nil)
|
|
return
|
|
}
|
|
aH.Respond(w, results)
|
|
}
|
|
|
|
func (aH *APIHandler) handleFunnelAnalytics(w http.ResponseWriter, r *http.Request) {
|
|
funnelID := coretypes.Param(r, "funnel_id")
|
|
|
|
claims, err := authtypes.ClaimsFromContext(r.Context())
|
|
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
funnel, err := aH.O11y.Modules.TraceFunnel.Get(r.Context(), valuer.MustNewUUID(funnelID), valuer.MustNewUUID(claims.OrgID))
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("funnel not found: %v", err)}, nil)
|
|
return
|
|
}
|
|
|
|
var stepTransition traceFunnels.StepTransitionRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&stepTransition); err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("error decoding time range: %v", err)}, nil)
|
|
return
|
|
}
|
|
|
|
chq, err := traceFunnelsModule.GetFunnelAnalytics(funnel, stepTransition.TimeRange)
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: fmt.Errorf("error building datastore query: %v", err)}, nil)
|
|
return
|
|
}
|
|
|
|
results, err := aH.reader.GetListResultV3(r.Context(), chq.Query)
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: fmt.Errorf("error converting datastore results to list: %v", err)}, nil)
|
|
return
|
|
}
|
|
aH.Respond(w, results)
|
|
}
|
|
|
|
func (aH *APIHandler) handleFunnelStepAnalytics(w http.ResponseWriter, r *http.Request) {
|
|
funnelID := coretypes.Param(r, "funnel_id")
|
|
|
|
claims, err := authtypes.ClaimsFromContext(r.Context())
|
|
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
funnel, err := aH.O11y.Modules.TraceFunnel.Get(r.Context(), valuer.MustNewUUID(funnelID), valuer.MustNewUUID(claims.OrgID))
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("funnel not found: %v", err)}, nil)
|
|
return
|
|
}
|
|
|
|
var stepTransition traceFunnels.StepTransitionRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&stepTransition); err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("error decoding time range: %v", err)}, nil)
|
|
return
|
|
}
|
|
|
|
chq, err := traceFunnelsModule.GetFunnelStepAnalytics(funnel, stepTransition.TimeRange, stepTransition.StepStart, stepTransition.StepEnd)
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: fmt.Errorf("error building datastore query: %v", err)}, nil)
|
|
return
|
|
}
|
|
|
|
results, err := aH.reader.GetListResultV3(r.Context(), chq.Query)
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: fmt.Errorf("error converting datastore results to list: %v", err)}, nil)
|
|
return
|
|
}
|
|
aH.Respond(w, results)
|
|
}
|
|
|
|
func (aH *APIHandler) handleStepAnalytics(w http.ResponseWriter, r *http.Request) {
|
|
funnelID := coretypes.Param(r, "funnel_id")
|
|
|
|
claims, err := authtypes.ClaimsFromContext(r.Context())
|
|
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
funnel, err := aH.O11y.Modules.TraceFunnel.Get(r.Context(), valuer.MustNewUUID(funnelID), valuer.MustNewUUID(claims.OrgID))
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("funnel not found: %v", err)}, nil)
|
|
return
|
|
}
|
|
|
|
var timeRange traceFunnels.TimeRange
|
|
if err := json.NewDecoder(r.Body).Decode(&timeRange); err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("error decoding time range: %v", err)}, nil)
|
|
return
|
|
}
|
|
|
|
chq, err := traceFunnelsModule.GetStepAnalytics(funnel, timeRange)
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: fmt.Errorf("error building datastore query: %v", err)}, nil)
|
|
return
|
|
}
|
|
|
|
results, err := aH.reader.GetListResultV3(r.Context(), chq.Query)
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: fmt.Errorf("error converting datastore results to list: %v", err)}, nil)
|
|
return
|
|
}
|
|
aH.Respond(w, results)
|
|
}
|
|
|
|
func (aH *APIHandler) handleFunnelSlowTraces(w http.ResponseWriter, r *http.Request) {
|
|
funnelID := coretypes.Param(r, "funnel_id")
|
|
|
|
claims, err := authtypes.ClaimsFromContext(r.Context())
|
|
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
funnel, err := aH.O11y.Modules.TraceFunnel.Get(r.Context(), valuer.MustNewUUID(funnelID), valuer.MustNewUUID(claims.OrgID))
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("funnel not found: %v", err)}, nil)
|
|
return
|
|
}
|
|
|
|
var req traceFunnels.StepTransitionRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("invalid request body: %v", err)}, nil)
|
|
return
|
|
}
|
|
|
|
chq, err := traceFunnelsModule.GetSlowestTraces(funnel, req.TimeRange, req.StepStart, req.StepEnd)
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: fmt.Errorf("error building datastore query: %v", err)}, nil)
|
|
return
|
|
}
|
|
|
|
results, err := aH.reader.GetListResultV3(r.Context(), chq.Query)
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: fmt.Errorf("error converting datastore results to list: %v", err)}, nil)
|
|
return
|
|
}
|
|
aH.Respond(w, results)
|
|
}
|
|
|
|
func (aH *APIHandler) handleFunnelErrorTraces(w http.ResponseWriter, r *http.Request) {
|
|
funnelID := coretypes.Param(r, "funnel_id")
|
|
|
|
claims, err := authtypes.ClaimsFromContext(r.Context())
|
|
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
funnel, err := aH.O11y.Modules.TraceFunnel.Get(r.Context(), valuer.MustNewUUID(funnelID), valuer.MustNewUUID(claims.OrgID))
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("funnel not found: %v", err)}, nil)
|
|
return
|
|
}
|
|
|
|
var req traceFunnels.StepTransitionRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("invalid request body: %v", err)}, nil)
|
|
return
|
|
}
|
|
|
|
chq, err := traceFunnelsModule.GetErroredTraces(funnel, req.TimeRange, req.StepStart, req.StepEnd)
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: fmt.Errorf("error building datastore query: %v", err)}, nil)
|
|
return
|
|
}
|
|
|
|
results, err := aH.reader.GetListResultV3(r.Context(), chq.Query)
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: fmt.Errorf("error converting datastore results to list: %v", err)}, nil)
|
|
return
|
|
}
|
|
aH.Respond(w, results)
|
|
}
|
|
|
|
func (aH *APIHandler) handleValidateTracesWithPayload(w http.ResponseWriter, r *http.Request) {
|
|
var req traceFunnels.PostableFunnel
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("error decoding request: %v", err)}, nil)
|
|
return
|
|
}
|
|
|
|
if len(req.Steps) < 2 {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("funnel must have at least 2 steps")}, nil)
|
|
return
|
|
}
|
|
|
|
// Create a StorableFunnel from the request
|
|
funnel := &traceFunnels.StorableFunnel{
|
|
Steps: req.Steps,
|
|
}
|
|
|
|
chq, err := traceFunnelsModule.ValidateTraces(funnel, traceFunnels.TimeRange{
|
|
StartTime: req.StartTime,
|
|
EndTime: req.EndTime,
|
|
})
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: fmt.Errorf("error building datastore query: %v", err)}, nil)
|
|
return
|
|
}
|
|
|
|
results, err := aH.reader.GetListResultV3(r.Context(), chq.Query)
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: fmt.Errorf("error converting datastore results to list: %v", err)}, nil)
|
|
return
|
|
}
|
|
aH.Respond(w, results)
|
|
}
|
|
|
|
func (aH *APIHandler) handleFunnelAnalyticsWithPayload(w http.ResponseWriter, r *http.Request) {
|
|
var req traceFunnels.PostableFunnel
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("error decoding request: %v", err)}, nil)
|
|
return
|
|
}
|
|
|
|
funnel := &traceFunnels.StorableFunnel{
|
|
Steps: req.Steps,
|
|
}
|
|
|
|
chq, err := traceFunnelsModule.GetFunnelAnalytics(funnel, traceFunnels.TimeRange{
|
|
StartTime: req.StartTime,
|
|
EndTime: req.EndTime,
|
|
})
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: fmt.Errorf("error building datastore query: %v", err)}, nil)
|
|
return
|
|
}
|
|
|
|
results, err := aH.reader.GetListResultV3(r.Context(), chq.Query)
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: fmt.Errorf("error converting datastore results to list: %v", err)}, nil)
|
|
return
|
|
}
|
|
aH.Respond(w, results)
|
|
}
|
|
|
|
func (aH *APIHandler) handleStepAnalyticsWithPayload(w http.ResponseWriter, r *http.Request) {
|
|
var req traceFunnels.PostableFunnel
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("error decoding request: %v", err)}, nil)
|
|
return
|
|
}
|
|
|
|
funnel := &traceFunnels.StorableFunnel{
|
|
Steps: req.Steps,
|
|
}
|
|
|
|
chq, err := traceFunnelsModule.GetStepAnalytics(funnel, traceFunnels.TimeRange{
|
|
StartTime: req.StartTime,
|
|
EndTime: req.EndTime,
|
|
})
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: fmt.Errorf("error building datastore query: %v", err)}, nil)
|
|
return
|
|
}
|
|
|
|
results, err := aH.reader.GetListResultV3(r.Context(), chq.Query)
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: fmt.Errorf("error converting datastore results to list: %v", err)}, nil)
|
|
return
|
|
}
|
|
aH.Respond(w, results)
|
|
}
|
|
|
|
func (aH *APIHandler) handleFunnelStepAnalyticsWithPayload(w http.ResponseWriter, r *http.Request) {
|
|
var req traceFunnels.PostableFunnel
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("error decoding request: %v", err)}, nil)
|
|
return
|
|
}
|
|
|
|
funnel := &traceFunnels.StorableFunnel{
|
|
Steps: req.Steps,
|
|
}
|
|
|
|
chq, err := traceFunnelsModule.GetFunnelStepAnalytics(funnel, traceFunnels.TimeRange{
|
|
StartTime: req.StartTime,
|
|
EndTime: req.EndTime,
|
|
}, req.StepStart, req.StepEnd)
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: fmt.Errorf("error building datastore query: %v", err)}, nil)
|
|
return
|
|
}
|
|
|
|
results, err := aH.reader.GetListResultV3(r.Context(), chq.Query)
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: fmt.Errorf("error converting datastore results to list: %v", err)}, nil)
|
|
return
|
|
}
|
|
aH.Respond(w, results)
|
|
}
|
|
|
|
func (aH *APIHandler) handleFunnelSlowTracesWithPayload(w http.ResponseWriter, r *http.Request) {
|
|
var req traceFunnels.PostableFunnel
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("error decoding request: %v", err)}, nil)
|
|
return
|
|
}
|
|
|
|
funnel := &traceFunnels.StorableFunnel{
|
|
Steps: req.Steps,
|
|
}
|
|
|
|
chq, err := traceFunnelsModule.GetSlowestTraces(funnel, traceFunnels.TimeRange{
|
|
StartTime: req.StartTime,
|
|
EndTime: req.EndTime,
|
|
}, req.StepStart, req.StepEnd)
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: fmt.Errorf("error building datastore query: %v", err)}, nil)
|
|
return
|
|
}
|
|
|
|
results, err := aH.reader.GetListResultV3(r.Context(), chq.Query)
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: fmt.Errorf("error converting datastore results to list: %v", err)}, nil)
|
|
return
|
|
}
|
|
aH.Respond(w, results)
|
|
}
|
|
|
|
func (aH *APIHandler) handleFunnelErrorTracesWithPayload(w http.ResponseWriter, r *http.Request) {
|
|
var req traceFunnels.PostableFunnel
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("error decoding request: %v", err)}, nil)
|
|
return
|
|
}
|
|
|
|
funnel := &traceFunnels.StorableFunnel{
|
|
Steps: req.Steps,
|
|
}
|
|
|
|
chq, err := traceFunnelsModule.GetErroredTraces(funnel, traceFunnels.TimeRange{
|
|
StartTime: req.StartTime,
|
|
EndTime: req.EndTime,
|
|
}, req.StepStart, req.StepEnd)
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: fmt.Errorf("error building datastore query: %v", err)}, nil)
|
|
return
|
|
}
|
|
|
|
results, err := aH.reader.GetListResultV3(r.Context(), chq.Query)
|
|
if err != nil {
|
|
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: fmt.Errorf("error converting datastore results to list: %v", err)}, nil)
|
|
return
|
|
}
|
|
aH.Respond(w, results)
|
|
}
|