mirror of
https://github.com/hanzoai/o11y.git
synced 2026-10-15 12:58:33 +00:00
o11y carried a second identity system: email/password sign-in, Google,
OIDC and SAML callbacks, self-registration, invites, reset-password
tokens, roles-on-users, SSO auth domains, and a pair of tokenizers that
minted and stored its own sessions. Hanzo IAM answers all of it, once,
at the edge. Two credential stores is one too many, and the one nobody
signs into is the one that rots.
What is left is Identity: WHO a request is, resolved from what the edge
asserted or from a service-account key, never issued here. The routes are
not merely unrouted — the handlers, stores, tables and templates behind
them are gone, and anonymous_test.go names each dead address so that
re-exempting one fails even though no route can come back.
The census moves with it: 366 addresses to 329, 353 typed ops to 319, ten
escape hatches to seven. zipdoc_gen.go loses the 34 descriptions whose
addresses left, because zip keys its doc registry by method and path, so
a description outliving its declaration attaches this module's prose to
whatever a host serves at that address. zipdoc_test.go pins that.
Cherry-picked from delete/o11y-identity 89854a68ae.
3990 lines
121 KiB
Go
3990 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 && aH.HandleError(w, apiErr.Err, http.StatusInternalServerError) {
|
|
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 the classic GET /api/v1/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). Honest: 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
|
|
}
|
|
|
|
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)
|
|
}
|