mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-07-25 02:44:26 +08:00
465 lines
15 KiB
Go
465 lines
15 KiB
Go
package anomaly
|
|
|
|
import (
|
|
"context"
|
|
"math"
|
|
"time"
|
|
|
|
"go.signoz.io/signoz/pkg/query-service/cache"
|
|
"go.signoz.io/signoz/pkg/query-service/interfaces"
|
|
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
|
"go.signoz.io/signoz/pkg/query-service/postprocess"
|
|
"go.signoz.io/signoz/pkg/query-service/utils/labels"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
var (
|
|
// TODO(srikanthccv): make this configurable?
|
|
movingAvgWindowSize = 7
|
|
)
|
|
|
|
// BaseProvider is an interface that includes common methods for all provider types
|
|
type BaseProvider interface {
|
|
GetBaseSeasonalProvider() *BaseSeasonalProvider
|
|
}
|
|
|
|
// GenericProviderOption is a generic type for provider options
|
|
type GenericProviderOption[T BaseProvider] func(T)
|
|
|
|
func WithCache[T BaseProvider](cache cache.Cache) GenericProviderOption[T] {
|
|
return func(p T) {
|
|
p.GetBaseSeasonalProvider().cache = cache
|
|
}
|
|
}
|
|
|
|
func WithKeyGenerator[T BaseProvider](keyGenerator cache.KeyGenerator) GenericProviderOption[T] {
|
|
return func(p T) {
|
|
p.GetBaseSeasonalProvider().keyGenerator = keyGenerator
|
|
}
|
|
}
|
|
|
|
func WithFeatureLookup[T BaseProvider](ff interfaces.FeatureLookup) GenericProviderOption[T] {
|
|
return func(p T) {
|
|
p.GetBaseSeasonalProvider().ff = ff
|
|
}
|
|
}
|
|
|
|
func WithReader[T BaseProvider](reader interfaces.Reader) GenericProviderOption[T] {
|
|
return func(p T) {
|
|
p.GetBaseSeasonalProvider().reader = reader
|
|
}
|
|
}
|
|
|
|
type BaseSeasonalProvider struct {
|
|
querierV2 interfaces.Querier
|
|
reader interfaces.Reader
|
|
fluxInterval time.Duration
|
|
cache cache.Cache
|
|
keyGenerator cache.KeyGenerator
|
|
ff interfaces.FeatureLookup
|
|
}
|
|
|
|
func (p *BaseSeasonalProvider) getQueryParams(req *GetAnomaliesRequest) *anomalyQueryParams {
|
|
if !req.Seasonality.IsValid() {
|
|
req.Seasonality = SeasonalityDaily
|
|
}
|
|
return prepareAnomalyQueryParams(req.Params, req.Seasonality)
|
|
}
|
|
|
|
func (p *BaseSeasonalProvider) getResults(ctx context.Context, params *anomalyQueryParams) (*anomalyQueryResults, error) {
|
|
currentPeriodResults, _, err := p.querierV2.QueryRange(ctx, params.CurrentPeriodQuery)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
currentPeriodResults, err = postprocess.PostProcessResult(currentPeriodResults, params.CurrentPeriodQuery)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
pastPeriodResults, _, err := p.querierV2.QueryRange(ctx, params.PastPeriodQuery)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
pastPeriodResults, err = postprocess.PostProcessResult(pastPeriodResults, params.PastPeriodQuery)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
currentSeasonResults, _, err := p.querierV2.QueryRange(ctx, params.CurrentSeasonQuery)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
currentSeasonResults, err = postprocess.PostProcessResult(currentSeasonResults, params.CurrentSeasonQuery)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
pastSeasonResults, _, err := p.querierV2.QueryRange(ctx, params.PastSeasonQuery)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
pastSeasonResults, err = postprocess.PostProcessResult(pastSeasonResults, params.PastSeasonQuery)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
past2SeasonResults, _, err := p.querierV2.QueryRange(ctx, params.Past2SeasonQuery)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
past2SeasonResults, err = postprocess.PostProcessResult(past2SeasonResults, params.Past2SeasonQuery)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
past3SeasonResults, _, err := p.querierV2.QueryRange(ctx, params.Past3SeasonQuery)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
past3SeasonResults, err = postprocess.PostProcessResult(past3SeasonResults, params.Past3SeasonQuery)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &anomalyQueryResults{
|
|
CurrentPeriodResults: currentPeriodResults,
|
|
PastPeriodResults: pastPeriodResults,
|
|
CurrentSeasonResults: currentSeasonResults,
|
|
PastSeasonResults: pastSeasonResults,
|
|
Past2SeasonResults: past2SeasonResults,
|
|
Past3SeasonResults: past3SeasonResults,
|
|
}, nil
|
|
}
|
|
|
|
// getMatchingSeries gets the matching series from the query result
|
|
// for the given series
|
|
func (p *BaseSeasonalProvider) getMatchingSeries(queryResult *v3.Result, series *v3.Series) *v3.Series {
|
|
if queryResult == nil || len(queryResult.Series) == 0 {
|
|
return nil
|
|
}
|
|
|
|
for _, curr := range queryResult.Series {
|
|
currLabels := labels.FromMap(curr.Labels)
|
|
seriesLabels := labels.FromMap(series.Labels)
|
|
if currLabels.Hash() == seriesLabels.Hash() {
|
|
return curr
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *BaseSeasonalProvider) getAvg(series *v3.Series) float64 {
|
|
if series == nil || len(series.Points) == 0 {
|
|
return 0
|
|
}
|
|
var sum float64
|
|
for _, smpl := range series.Points {
|
|
sum += smpl.Value
|
|
}
|
|
return sum / float64(len(series.Points))
|
|
}
|
|
|
|
func (p *BaseSeasonalProvider) getStdDev(series *v3.Series) float64 {
|
|
if series == nil || len(series.Points) == 0 {
|
|
return 0
|
|
}
|
|
avg := p.getAvg(series)
|
|
var sum float64
|
|
for _, smpl := range series.Points {
|
|
sum += math.Pow(smpl.Value-avg, 2)
|
|
}
|
|
return math.Sqrt(sum / float64(len(series.Points)))
|
|
}
|
|
|
|
// getMovingAvg gets the moving average for the given series
|
|
// for the given window size and start index
|
|
func (p *BaseSeasonalProvider) getMovingAvg(series *v3.Series, movingAvgWindowSize, startIdx int) float64 {
|
|
if series == nil || len(series.Points) == 0 {
|
|
return 0
|
|
}
|
|
if startIdx >= len(series.Points)-movingAvgWindowSize {
|
|
startIdx = len(series.Points) - movingAvgWindowSize
|
|
}
|
|
var sum float64
|
|
points := series.Points[startIdx:]
|
|
for i := 0; i < movingAvgWindowSize && i < len(points); i++ {
|
|
sum += points[i].Value
|
|
}
|
|
avg := sum / float64(movingAvgWindowSize)
|
|
return avg
|
|
}
|
|
|
|
func (p *BaseSeasonalProvider) getMean(floats ...float64) float64 {
|
|
if len(floats) == 0 {
|
|
return 0
|
|
}
|
|
var sum float64
|
|
for _, f := range floats {
|
|
sum += f
|
|
}
|
|
return sum / float64(len(floats))
|
|
}
|
|
|
|
func (p *BaseSeasonalProvider) getPredictedSeries(
|
|
series, prevSeries, currentSeasonSeries, pastSeasonSeries, past2SeasonSeries, past3SeasonSeries *v3.Series,
|
|
) *v3.Series {
|
|
predictedSeries := &v3.Series{
|
|
Labels: series.Labels,
|
|
LabelsArray: series.LabelsArray,
|
|
Points: []v3.Point{},
|
|
}
|
|
|
|
// for each point in the series, get the predicted value
|
|
// the predicted value is the moving average (with window size = 7) of the previous period series
|
|
// plus the average of the current season series
|
|
// minus the mean of the past season series, past2 season series and past3 season series
|
|
for idx, curr := range series.Points {
|
|
predictedValue :=
|
|
p.getMovingAvg(prevSeries, movingAvgWindowSize, idx) +
|
|
p.getAvg(currentSeasonSeries) -
|
|
p.getMean(p.getAvg(pastSeasonSeries), p.getAvg(past2SeasonSeries), p.getAvg(past3SeasonSeries))
|
|
|
|
if predictedValue < 0 {
|
|
predictedValue = p.getMovingAvg(prevSeries, movingAvgWindowSize, idx)
|
|
}
|
|
|
|
zap.L().Info("predictedSeries",
|
|
zap.Float64("movingAvg", p.getMovingAvg(prevSeries, movingAvgWindowSize, idx)),
|
|
zap.Float64("avg", p.getAvg(currentSeasonSeries)),
|
|
zap.Float64("mean", p.getMean(p.getAvg(pastSeasonSeries), p.getAvg(past2SeasonSeries), p.getAvg(past3SeasonSeries))),
|
|
zap.Any("labels", series.Labels),
|
|
zap.Float64("predictedValue", predictedValue),
|
|
)
|
|
predictedSeries.Points = append(predictedSeries.Points, v3.Point{
|
|
Timestamp: curr.Timestamp,
|
|
Value: predictedValue,
|
|
})
|
|
}
|
|
|
|
return predictedSeries
|
|
}
|
|
|
|
// getBounds gets the upper and lower bounds for the given series
|
|
// for the given z score threshold
|
|
// moving avg of the previous period series + z score threshold * std dev of the series
|
|
// moving avg of the previous period series - z score threshold * std dev of the series
|
|
func (p *BaseSeasonalProvider) getBounds(
|
|
series, prevSeries, _, _, _, _ *v3.Series,
|
|
zScoreThreshold float64,
|
|
) (*v3.Series, *v3.Series) {
|
|
upperBoundSeries := &v3.Series{
|
|
Labels: series.Labels,
|
|
LabelsArray: series.LabelsArray,
|
|
Points: []v3.Point{},
|
|
}
|
|
|
|
lowerBoundSeries := &v3.Series{
|
|
Labels: series.Labels,
|
|
LabelsArray: series.LabelsArray,
|
|
Points: []v3.Point{},
|
|
}
|
|
|
|
for idx, curr := range series.Points {
|
|
upperBound := p.getMovingAvg(prevSeries, movingAvgWindowSize, idx) + zScoreThreshold*p.getStdDev(series)
|
|
lowerBound := p.getMovingAvg(prevSeries, movingAvgWindowSize, idx) - zScoreThreshold*p.getStdDev(series)
|
|
upperBoundSeries.Points = append(upperBoundSeries.Points, v3.Point{
|
|
Timestamp: curr.Timestamp,
|
|
Value: upperBound,
|
|
})
|
|
lowerBoundSeries.Points = append(lowerBoundSeries.Points, v3.Point{
|
|
Timestamp: curr.Timestamp,
|
|
Value: math.Max(lowerBound, 0),
|
|
})
|
|
}
|
|
|
|
return upperBoundSeries, lowerBoundSeries
|
|
}
|
|
|
|
// getExpectedValue gets the expected value for the given series
|
|
// for the given index
|
|
// prevSeriesAvg + currentSeasonSeriesAvg - mean of past season series, past2 season series and past3 season series
|
|
func (p *BaseSeasonalProvider) getExpectedValue(
|
|
_, prevSeries, currentSeasonSeries, pastSeasonSeries, past2SeasonSeries, past3SeasonSeries *v3.Series, idx int,
|
|
) float64 {
|
|
prevSeriesAvg := p.getMovingAvg(prevSeries, movingAvgWindowSize, idx)
|
|
currentSeasonSeriesAvg := p.getAvg(currentSeasonSeries)
|
|
pastSeasonSeriesAvg := p.getAvg(pastSeasonSeries)
|
|
past2SeasonSeriesAvg := p.getAvg(past2SeasonSeries)
|
|
past3SeasonSeriesAvg := p.getAvg(past3SeasonSeries)
|
|
return prevSeriesAvg + currentSeasonSeriesAvg - p.getMean(pastSeasonSeriesAvg, past2SeasonSeriesAvg, past3SeasonSeriesAvg)
|
|
}
|
|
|
|
// getScore gets the anomaly score for the given series
|
|
// for the given index
|
|
// (value - expectedValue) / std dev of the series
|
|
func (p *BaseSeasonalProvider) getScore(
|
|
series, prevSeries, weekSeries, weekPrevSeries, past2SeasonSeries, past3SeasonSeries *v3.Series, value float64, idx int,
|
|
) float64 {
|
|
expectedValue := p.getExpectedValue(series, prevSeries, weekSeries, weekPrevSeries, past2SeasonSeries, past3SeasonSeries, idx)
|
|
return (value - expectedValue) / p.getStdDev(weekSeries)
|
|
}
|
|
|
|
// getAnomalyScores gets the anomaly scores for the given series
|
|
// for the given index
|
|
// (value - expectedValue) / std dev of the series
|
|
func (p *BaseSeasonalProvider) getAnomalyScores(
|
|
series, prevSeries, currentSeasonSeries, pastSeasonSeries, past2SeasonSeries, past3SeasonSeries *v3.Series,
|
|
) *v3.Series {
|
|
anomalyScoreSeries := &v3.Series{
|
|
Labels: series.Labels,
|
|
LabelsArray: series.LabelsArray,
|
|
Points: []v3.Point{},
|
|
}
|
|
|
|
for idx, curr := range series.Points {
|
|
anomalyScore := p.getScore(series, prevSeries, currentSeasonSeries, pastSeasonSeries, past2SeasonSeries, past3SeasonSeries, curr.Value, idx)
|
|
anomalyScoreSeries.Points = append(anomalyScoreSeries.Points, v3.Point{
|
|
Timestamp: curr.Timestamp,
|
|
Value: anomalyScore,
|
|
})
|
|
}
|
|
|
|
return anomalyScoreSeries
|
|
}
|
|
|
|
func (p *BaseSeasonalProvider) getAnomalies(ctx context.Context, req *GetAnomaliesRequest) (*GetAnomaliesResponse, error) {
|
|
anomalyParams := p.getQueryParams(req)
|
|
anomalyQueryResults, err := p.getResults(ctx, anomalyParams)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
currentPeriodResultsMap := make(map[string]*v3.Result)
|
|
for _, result := range anomalyQueryResults.CurrentPeriodResults {
|
|
currentPeriodResultsMap[result.QueryName] = result
|
|
}
|
|
|
|
pastPeriodResultsMap := make(map[string]*v3.Result)
|
|
for _, result := range anomalyQueryResults.PastPeriodResults {
|
|
pastPeriodResultsMap[result.QueryName] = result
|
|
}
|
|
|
|
currentSeasonResultsMap := make(map[string]*v3.Result)
|
|
for _, result := range anomalyQueryResults.CurrentSeasonResults {
|
|
currentSeasonResultsMap[result.QueryName] = result
|
|
}
|
|
|
|
pastSeasonResultsMap := make(map[string]*v3.Result)
|
|
for _, result := range anomalyQueryResults.PastSeasonResults {
|
|
pastSeasonResultsMap[result.QueryName] = result
|
|
}
|
|
|
|
past2SeasonResultsMap := make(map[string]*v3.Result)
|
|
for _, result := range anomalyQueryResults.Past2SeasonResults {
|
|
past2SeasonResultsMap[result.QueryName] = result
|
|
}
|
|
|
|
past3SeasonResultsMap := make(map[string]*v3.Result)
|
|
for _, result := range anomalyQueryResults.Past3SeasonResults {
|
|
past3SeasonResultsMap[result.QueryName] = result
|
|
}
|
|
|
|
for _, result := range currentPeriodResultsMap {
|
|
funcs := req.Params.CompositeQuery.BuilderQueries[result.QueryName].Functions
|
|
|
|
var zScoreThreshold float64
|
|
for _, f := range funcs {
|
|
if f.Name == v3.FunctionNameAnomaly {
|
|
value, ok := f.NamedArgs["z_score_threshold"]
|
|
if ok {
|
|
zScoreThreshold = value.(float64)
|
|
} else {
|
|
zScoreThreshold = 3
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
pastPeriodResult, ok := pastPeriodResultsMap[result.QueryName]
|
|
if !ok {
|
|
continue
|
|
}
|
|
currentSeasonResult, ok := currentSeasonResultsMap[result.QueryName]
|
|
if !ok {
|
|
continue
|
|
}
|
|
pastSeasonResult, ok := pastSeasonResultsMap[result.QueryName]
|
|
if !ok {
|
|
continue
|
|
}
|
|
past2SeasonResult, ok := past2SeasonResultsMap[result.QueryName]
|
|
if !ok {
|
|
continue
|
|
}
|
|
past3SeasonResult, ok := past3SeasonResultsMap[result.QueryName]
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
for _, series := range result.Series {
|
|
stdDev := p.getStdDev(series)
|
|
zap.L().Info("stdDev", zap.Float64("stdDev", stdDev), zap.Any("labels", series.Labels))
|
|
|
|
pastPeriodSeries := p.getMatchingSeries(pastPeriodResult, series)
|
|
currentSeasonSeries := p.getMatchingSeries(currentSeasonResult, series)
|
|
pastSeasonSeries := p.getMatchingSeries(pastSeasonResult, series)
|
|
past2SeasonSeries := p.getMatchingSeries(past2SeasonResult, series)
|
|
past3SeasonSeries := p.getMatchingSeries(past3SeasonResult, series)
|
|
|
|
prevSeriesAvg := p.getAvg(pastPeriodSeries)
|
|
currentSeasonSeriesAvg := p.getAvg(currentSeasonSeries)
|
|
pastSeasonSeriesAvg := p.getAvg(pastSeasonSeries)
|
|
past2SeasonSeriesAvg := p.getAvg(past2SeasonSeries)
|
|
past3SeasonSeriesAvg := p.getAvg(past3SeasonSeries)
|
|
zap.L().Info("getAvg", zap.Float64("prevSeriesAvg", prevSeriesAvg), zap.Float64("currentSeasonSeriesAvg", currentSeasonSeriesAvg), zap.Float64("pastSeasonSeriesAvg", pastSeasonSeriesAvg), zap.Float64("past2SeasonSeriesAvg", past2SeasonSeriesAvg), zap.Float64("past3SeasonSeriesAvg", past3SeasonSeriesAvg), zap.Any("labels", series.Labels))
|
|
|
|
predictedSeries := p.getPredictedSeries(
|
|
series,
|
|
pastPeriodSeries,
|
|
currentSeasonSeries,
|
|
pastSeasonSeries,
|
|
past2SeasonSeries,
|
|
past3SeasonSeries,
|
|
)
|
|
result.PredictedSeries = append(result.PredictedSeries, predictedSeries)
|
|
|
|
upperBoundSeries, lowerBoundSeries := p.getBounds(
|
|
series,
|
|
pastPeriodSeries,
|
|
currentSeasonSeries,
|
|
pastSeasonSeries,
|
|
past2SeasonSeries,
|
|
past3SeasonSeries,
|
|
zScoreThreshold,
|
|
)
|
|
result.UpperBoundSeries = append(result.UpperBoundSeries, upperBoundSeries)
|
|
result.LowerBoundSeries = append(result.LowerBoundSeries, lowerBoundSeries)
|
|
|
|
anomalyScoreSeries := p.getAnomalyScores(
|
|
series,
|
|
pastPeriodSeries,
|
|
currentSeasonSeries,
|
|
pastSeasonSeries,
|
|
past2SeasonSeries,
|
|
past3SeasonSeries,
|
|
)
|
|
result.AnomalyScores = append(result.AnomalyScores, anomalyScoreSeries)
|
|
}
|
|
}
|
|
|
|
results := make([]*v3.Result, 0, len(currentPeriodResultsMap))
|
|
for _, result := range currentPeriodResultsMap {
|
|
results = append(results, result)
|
|
}
|
|
|
|
return &GetAnomaliesResponse{
|
|
Results: results,
|
|
}, nil
|
|
}
|