diff --git a/.github/ISSUE_TEMPLATE/request_dashboard.md b/.github/ISSUE_TEMPLATE/request_dashboard.md new file mode 100644 index 0000000000..ba68449103 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/request_dashboard.md @@ -0,0 +1,49 @@ +--- +name: Request Dashboard +about: Request a new dashboard for the SigNoz Dashboards repository +title: '[Dashboard Request] ' +labels: 'dashboard-template' +assignees: '' + +--- + + + +## Dashboard Name + + + +## Expected Dashboard Sections and Panels + +(Can be tweaked (add or remove panels/sections) according to available metrics) + +### Section Name + + + +### Panel Name + + + + + + + +## Expected Dashboard Variables + + + +## Additional Comments or Requirements + + + +## References or Screenshots + + + +## 📋 Notes + +Please review the [CONTRIBUTING.md](https://github.com/SigNoz/dashboards/blob/main/CONTRIBUTING.md) for guidelines on dashboard structure, naming conventions, and how to submit a pull request. diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index ba92f9f28f..8b3e35b53f 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -11,9 +11,9 @@ jobs: check-no-ee-references: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - name: Run check - run: make check-no-ee-references + - uses: actions/checkout@v4 + - name: Run check + run: make check-no-ee-references build-frontend: runs-on: ubuntu-latest @@ -43,7 +43,6 @@ jobs: run: | echo 'INTERCOM_APP_ID="${{ secrets.INTERCOM_APP_ID }}"' > frontend/.env echo 'SEGMENT_ID="${{ secrets.SEGMENT_ID }}"' >> frontend/.env - echo 'CLARITY_PROJECT_ID="${{ secrets.CLARITY_PROJECT_ID }}"' >> frontend/.env - name: Install dependencies run: cd frontend && yarn install - name: Run ESLint diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml index 7808f9d18e..a440d2a5c7 100644 --- a/.github/workflows/push.yaml +++ b/.github/workflows/push.yaml @@ -9,7 +9,6 @@ on: - v* jobs: - image-build-and-push-query-service: runs-on: ubuntu-latest steps: @@ -151,7 +150,6 @@ jobs: run: | echo 'INTERCOM_APP_ID="${{ secrets.INTERCOM_APP_ID }}"' > frontend/.env echo 'SEGMENT_ID="${{ secrets.SEGMENT_ID }}"' >> frontend/.env - echo 'CLARITY_PROJECT_ID="${{ secrets.CLARITY_PROJECT_ID }}"' >> frontend/.env echo 'SENTRY_AUTH_TOKEN="${{ secrets.SENTRY_AUTH_TOKEN }}"' >> frontend/.env echo 'SENTRY_ORG="${{ secrets.SENTRY_ORG }}"' >> frontend/.env echo 'SENTRY_PROJECT_ID="${{ secrets.SENTRY_PROJECT_ID }}"' >> frontend/.env diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cc1c4399d8..613b225353 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,6 +30,7 @@ Also, have a look at these [good first issues label](https://github.com/SigNoz/s - [To run ClickHouse setup](#41-to-run-clickhouse-setup-recommended-for-local-development) - [Contribute to SigNoz Helm Chart](#5-contribute-to-signoz-helm-chart-) - [To run helm chart for local development](#51-to-run-helm-chart-for-local-development) +- [Contribute to Dashboards](#6-contribute-to-dashboards-) - [Other Ways to Contribute](#other-ways-to-contribute) # 1. General Instructions 📝 @@ -37,7 +38,7 @@ Also, have a look at these [good first issues label](https://github.com/SigNoz/s ## 1.1 For Creating Issue(s) Before making any significant changes and before filing a new issue, please check [existing open](https://github.com/SigNoz/signoz/issues?q=is%3Aopen+is%3Aissue), or [recently closed](https://github.com/SigNoz/signoz/issues?q=is%3Aissue+is%3Aclosed) issues to make sure somebody else hasn't already reported the issue. Please try to include as much information as you can. -**Issue Types** - [Bug Report](https://github.com/SigNoz/signoz/issues/new?assignees=&labels=&template=bug_report.md&title=) | [Feature Request](https://github.com/SigNoz/signoz/issues/new?assignees=&labels=&template=feature_request.md&title=) | [Performance Issue Report](https://github.com/SigNoz/signoz/issues/new?assignees=&labels=&template=performance-issue-report.md&title=) | [Report a Security Vulnerability](https://github.com/SigNoz/signoz/security/policy) +**Issue Types** - [Bug Report](https://github.com/SigNoz/signoz/issues/new?assignees=&labels=&template=bug_report.md&title=) | [Feature Request](https://github.com/SigNoz/signoz/issues/new?assignees=&labels=&template=feature_request.md&title=) | [Performance Issue Report](https://github.com/SigNoz/signoz/issues/new?assignees=&labels=&template=performance-issue-report.md&title=) | [Request Dashboard](https://github.com/SigNoz/signoz/issues/new?assignees=&labels=dashboard-template&projects=&template=request_dashboard.md&title=%5BDashboard+Request%5D+) | [Report a Security Vulnerability](https://github.com/SigNoz/signoz/security/policy) #### Details like these are incredibly useful: @@ -56,7 +57,7 @@ Before making any significant changes and before filing a new issue, please chec Discussing your proposed changes ahead of time will make the contribution process smooth for everyone 🙌. - **[`^top^`](#)** + **[`^top^`](#contributing-guidelines)**
@@ -97,13 +98,14 @@ GitHub provides additional document on [forking a repository](https://help.githu stability and quality of the component. -You can always reach out to `ankit@signoz.io` to understand more about the repo and product. We are very responsive over email and [SLACK](https://signoz.io/slack). +You can always reach out to `ankit@signoz.io` to understand more about the repo and product. We are very responsive over email and [slack community](https://signoz.io/slack). ### Pointers: - If you find any **bugs** → please create an [**issue.**](https://github.com/SigNoz/signoz/issues/new?assignees=&labels=&template=bug_report.md&title=) - If you find anything **missing** in documentation → you can create an issue with the label **`documentation`**. - If you want to build any **new feature** → please create an [issue with the label **`enhancement`**.](https://github.com/SigNoz/signoz/issues/new?assignees=&labels=&template=feature_request.md&title=) - If you want to **discuss** something about the product, start a new [**discussion**.](https://github.com/SigNoz/signoz/discussions) +- If you want to request a new **dashboard template** → please create an issue [here](https://github.com/SigNoz/signoz/issues/new?assignees=&labels=dashboard-template&projects=&template=request_dashboard.md&title=%5BDashboard+Request%5D+).
@@ -117,7 +119,7 @@ e.g. If you are submitting a fix for an issue in frontend, the PR name should be - Feel free to ping us on [`#contributing`](https://signoz-community.slack.com/archives/C01LWQ8KS7M) or [`#contributing-frontend`](https://signoz-community.slack.com/archives/C027134DM8B) on our slack community if you need any help on this :) - **[`^top^`](#)** + **[`^top^`](#contributing-guidelines)**
@@ -127,14 +129,13 @@ e.g. If you are submitting a fix for an issue in frontend, the PR name should be - [**Frontend**](#3-develop-frontend-) (Written in Typescript, React) - [**Backend**](#4-contribute-to-backend-query-service-) (Query Service, written in Go) +- [**Dashboard Templates**](#6-contribute-to-dashboards-) (JSON dashboard templates built with SigNoz) Depending upon your area of expertise & interest, you can choose one or more to contribute. Below are detailed instructions to contribute in each area. -**Please note:** If you want to work on an issue, please ask the maintainers to assign the issue to you before starting work on it. This would help us understand who is working on an issue and prevent duplicate work. 🙏🏻 +**Please note:** If you want to work on an issue, please add a brief description of your solution on the issue before starting work on it. -⚠️ If you just raise a PR, without the corresponding issue being assigned to you - it may not be accepted. - - **[`^top^`](#)** + **[`^top^`](#contributing-guidelines)**
@@ -188,7 +189,7 @@ Also, have a look at [Frontend README.md](https://github.com/SigNoz/signoz/blob/ ### Important Notes: The Maintainers / Contributors who will change Line Numbers of `Frontend` & `Query-Section`, please update line numbers in [`/.scripts/commentLinesForSetup.sh`](https://github.com/SigNoz/signoz/blob/develop/.scripts/commentLinesForSetup.sh) - **[`^top^`](#)** + **[`^top^`](#contributing-guidelines)** ## 3.2 Contribute to Frontend without installing SigNoz backend @@ -209,7 +210,7 @@ Please ping us in the [`#contributing`](https://signoz-community.slack.com/archi **Frontend should now be accessible at** [`http://localhost:3301/services`](http://localhost:3301/services) - **[`^top^`](#)** + **[`^top^`](#contributing-guidelines)**
@@ -309,7 +310,7 @@ Click the button below. A workspace with all required environments will be creat > To use it on your forked repo, edit the 'Open in Gitpod' button URL to `https://gitpod.io/#https://github.com//signoz` --> - **[`^top^`](#)** + **[`^top^`](#contributing-guidelines)**
@@ -365,10 +366,21 @@ curl -sL https://github.com/SigNoz/signoz/raw/develop/sample-apps/hotrod/hotrod- | HOTROD_NAMESPACE=sample-application bash ``` - **[`^top^`](#)** + **[`^top^`](#contributing-guidelines)** --- +# 6. Contribute to Dashboards 📈 + +**Need to Update: [https://github.com/SigNoz/dashboards](https://github.com/SigNoz/dashboards)** + +To contribute a new dashboard template for any service, follow the contribution guidelines in the [Dashboard Contributing Guide](https://github.com/SigNoz/dashboards/blob/main/CONTRIBUTING.md). In brief: + +1. Create a dashboard JSON file. +2. Add a README file explaining the dashboard, the metrics ingested, and the configurations needed. +3. Include screenshots of the dashboard in the `assets/` directory. +4. Submit a pull request for review. + ## Other Ways to Contribute There are many other ways to get involved with the community and to participate in this project: @@ -379,7 +391,6 @@ There are many other ways to get involved with the community and to participate - Help answer questions on forums such as Stack Overflow and [SigNoz Community Slack Channel](https://signoz.io/slack). - Tell others about the project on Twitter, your blog, etc. - Again, Feel free to ping us on [`#contributing`](https://signoz-community.slack.com/archives/C01LWQ8KS7M) or [`#contributing-frontend`](https://signoz-community.slack.com/archives/C027134DM8B) on our slack community if you need any help on this :) Thank You! diff --git a/ee/query-service/anomaly/daily.go b/ee/query-service/anomaly/daily.go new file mode 100644 index 0000000000..bbafe1618e --- /dev/null +++ b/ee/query-service/anomaly/daily.go @@ -0,0 +1,44 @@ +package anomaly + +import ( + "context" + + querierV2 "go.signoz.io/signoz/pkg/query-service/app/querier/v2" + "go.signoz.io/signoz/pkg/query-service/app/queryBuilder" +) + +type DailyProvider struct { + BaseSeasonalProvider +} + +var _ BaseProvider = (*DailyProvider)(nil) + +func (dp *DailyProvider) GetBaseSeasonalProvider() *BaseSeasonalProvider { + return &dp.BaseSeasonalProvider +} + +// NewDailyProvider uses the same generic option type +func NewDailyProvider(opts ...GenericProviderOption[*DailyProvider]) *DailyProvider { + dp := &DailyProvider{ + BaseSeasonalProvider: BaseSeasonalProvider{}, + } + + for _, opt := range opts { + opt(dp) + } + + dp.querierV2 = querierV2.NewQuerier(querierV2.QuerierOptions{ + Reader: dp.reader, + Cache: dp.cache, + KeyGenerator: queryBuilder.NewKeyGenerator(), + FluxInterval: dp.fluxInterval, + FeatureLookup: dp.ff, + }) + + return dp +} + +func (p *DailyProvider) GetAnomalies(ctx context.Context, req *GetAnomaliesRequest) (*GetAnomaliesResponse, error) { + req.Seasonality = SeasonalityDaily + return p.getAnomalies(ctx, req) +} diff --git a/ee/query-service/anomaly/hourly.go b/ee/query-service/anomaly/hourly.go new file mode 100644 index 0000000000..1ee08655f0 --- /dev/null +++ b/ee/query-service/anomaly/hourly.go @@ -0,0 +1,44 @@ +package anomaly + +import ( + "context" + + querierV2 "go.signoz.io/signoz/pkg/query-service/app/querier/v2" + "go.signoz.io/signoz/pkg/query-service/app/queryBuilder" +) + +type HourlyProvider struct { + BaseSeasonalProvider +} + +var _ BaseProvider = (*HourlyProvider)(nil) + +func (hp *HourlyProvider) GetBaseSeasonalProvider() *BaseSeasonalProvider { + return &hp.BaseSeasonalProvider +} + +// NewHourlyProvider now uses the generic option type +func NewHourlyProvider(opts ...GenericProviderOption[*HourlyProvider]) *HourlyProvider { + hp := &HourlyProvider{ + BaseSeasonalProvider: BaseSeasonalProvider{}, + } + + for _, opt := range opts { + opt(hp) + } + + hp.querierV2 = querierV2.NewQuerier(querierV2.QuerierOptions{ + Reader: hp.reader, + Cache: hp.cache, + KeyGenerator: queryBuilder.NewKeyGenerator(), + FluxInterval: hp.fluxInterval, + FeatureLookup: hp.ff, + }) + + return hp +} + +func (p *HourlyProvider) GetAnomalies(ctx context.Context, req *GetAnomaliesRequest) (*GetAnomaliesResponse, error) { + req.Seasonality = SeasonalityHourly + return p.getAnomalies(ctx, req) +} diff --git a/ee/query-service/anomaly/params.go b/ee/query-service/anomaly/params.go new file mode 100644 index 0000000000..8340a2673a --- /dev/null +++ b/ee/query-service/anomaly/params.go @@ -0,0 +1,248 @@ +package anomaly + +import ( + "math" + "time" + + "go.signoz.io/signoz/pkg/query-service/common" + v3 "go.signoz.io/signoz/pkg/query-service/model/v3" +) + +type Seasonality string + +const ( + SeasonalityHourly Seasonality = "hourly" + SeasonalityDaily Seasonality = "daily" + SeasonalityWeekly Seasonality = "weekly" +) + +func (s Seasonality) String() string { + return string(s) +} + +var ( + oneWeekOffset = 24 * 7 * time.Hour.Milliseconds() + oneDayOffset = 24 * time.Hour.Milliseconds() + oneHourOffset = time.Hour.Milliseconds() + fiveMinOffset = 5 * time.Minute.Milliseconds() +) + +func (s Seasonality) IsValid() bool { + switch s { + case SeasonalityHourly, SeasonalityDaily, SeasonalityWeekly: + return true + default: + return false + } +} + +type GetAnomaliesRequest struct { + Params *v3.QueryRangeParamsV3 + Seasonality Seasonality +} + +type GetAnomaliesResponse struct { + Results []*v3.Result +} + +// anomalyParams is the params for anomaly detection +// prediction = avg(past_period_query) + avg(current_season_query) - mean(past_season_query, past2_season_query, past3_season_query) +// +// ^ ^ +// | | +// (rounded value for past peiod) + (seasonal growth) +// +// score = abs(value - prediction) / stddev (current_season_query) +type anomalyQueryParams struct { + // CurrentPeriodQuery is the query range params for period user is looking at or eval window + // Example: (now-5m, now), (now-30m, now), (now-1h, now) + // The results obtained from this query are used to compare with predicted values + // and to detect anomalies + CurrentPeriodQuery *v3.QueryRangeParamsV3 + // PastPeriodQuery is the query range params for past seasonal period + // Example: For weekly seasonality, (now-1w-5m, now-1w) + // : For daily seasonality, (now-1d-5m, now-1d) + // : For hourly seasonality, (now-1h-5m, now-1h) + PastPeriodQuery *v3.QueryRangeParamsV3 + // CurrentSeasonQuery is the query range params for current period (seasonal) + // Example: For weekly seasonality, this is the query range params for the (now-1w-5m, now) + // : For daily seasonality, this is the query range params for the (now-1d-5m, now) + // : For hourly seasonality, this is the query range params for the (now-1h-5m, now) + CurrentSeasonQuery *v3.QueryRangeParamsV3 + // PastSeasonQuery is the query range params for past seasonal period to the current season + // Example: For weekly seasonality, this is the query range params for the (now-2w-5m, now-1w) + // : For daily seasonality, this is the query range params for the (now-2d-5m, now-1d) + // : For hourly seasonality, this is the query range params for the (now-2h-5m, now-1h) + PastSeasonQuery *v3.QueryRangeParamsV3 + + // Past2SeasonQuery is the query range params for past 2 seasonal period to the current season + // Example: For weekly seasonality, this is the query range params for the (now-3w-5m, now-2w) + // : For daily seasonality, this is the query range params for the (now-3d-5m, now-2d) + // : For hourly seasonality, this is the query range params for the (now-3h-5m, now-2h) + Past2SeasonQuery *v3.QueryRangeParamsV3 + // Past3SeasonQuery is the query range params for past 3 seasonal period to the current season + // Example: For weekly seasonality, this is the query range params for the (now-4w-5m, now-3w) + // : For daily seasonality, this is the query range params for the (now-4d-5m, now-3d) + // : For hourly seasonality, this is the query range params for the (now-4h-5m, now-3h) + Past3SeasonQuery *v3.QueryRangeParamsV3 +} + +func updateStepInterval(req *v3.QueryRangeParamsV3) { + start := req.Start + end := req.End + + req.Step = int64(math.Max(float64(common.MinAllowedStepInterval(start, end)), 60)) + for _, q := range req.CompositeQuery.BuilderQueries { + // If the step interval is less than the minimum allowed step interval, set it to the minimum allowed step interval + if minStep := common.MinAllowedStepInterval(start, end); q.StepInterval < minStep { + q.StepInterval = minStep + } + } +} + +func prepareAnomalyQueryParams(req *v3.QueryRangeParamsV3, seasonality Seasonality) *anomalyQueryParams { + start := req.Start + end := req.End + + currentPeriodQuery := &v3.QueryRangeParamsV3{ + Start: start, + End: end, + CompositeQuery: req.CompositeQuery.Clone(), + Variables: make(map[string]interface{}, 0), + NoCache: false, + } + updateStepInterval(currentPeriodQuery) + + var pastPeriodStart, pastPeriodEnd int64 + + switch seasonality { + // for one week period, we fetch the data from the past week with 5 min offset + case SeasonalityWeekly: + pastPeriodStart = start - oneWeekOffset - fiveMinOffset + pastPeriodEnd = end - oneWeekOffset + // for one day period, we fetch the data from the past day with 5 min offset + case SeasonalityDaily: + pastPeriodStart = start - oneDayOffset - fiveMinOffset + pastPeriodEnd = end - oneDayOffset + // for one hour period, we fetch the data from the past hour with 5 min offset + case SeasonalityHourly: + pastPeriodStart = start - oneHourOffset - fiveMinOffset + pastPeriodEnd = end - oneHourOffset + } + + pastPeriodQuery := &v3.QueryRangeParamsV3{ + Start: pastPeriodStart, + End: pastPeriodEnd, + CompositeQuery: req.CompositeQuery.Clone(), + Variables: make(map[string]interface{}, 0), + NoCache: false, + } + updateStepInterval(pastPeriodQuery) + + // seasonality growth trend + var currentGrowthPeriodStart, currentGrowthPeriodEnd int64 + switch seasonality { + case SeasonalityWeekly: + currentGrowthPeriodStart = start - oneWeekOffset + currentGrowthPeriodEnd = end + case SeasonalityDaily: + currentGrowthPeriodStart = start - oneDayOffset + currentGrowthPeriodEnd = end + case SeasonalityHourly: + currentGrowthPeriodStart = start - oneHourOffset + currentGrowthPeriodEnd = end + } + + currentGrowthQuery := &v3.QueryRangeParamsV3{ + Start: currentGrowthPeriodStart, + End: currentGrowthPeriodEnd, + CompositeQuery: req.CompositeQuery.Clone(), + Variables: make(map[string]interface{}, 0), + NoCache: false, + } + updateStepInterval(currentGrowthQuery) + + var pastGrowthPeriodStart, pastGrowthPeriodEnd int64 + switch seasonality { + case SeasonalityWeekly: + pastGrowthPeriodStart = start - 2*oneWeekOffset + pastGrowthPeriodEnd = start - 1*oneWeekOffset + case SeasonalityDaily: + pastGrowthPeriodStart = start - 2*oneDayOffset + pastGrowthPeriodEnd = start - 1*oneDayOffset + case SeasonalityHourly: + pastGrowthPeriodStart = start - 2*oneHourOffset + pastGrowthPeriodEnd = start - 1*oneHourOffset + } + + pastGrowthQuery := &v3.QueryRangeParamsV3{ + Start: pastGrowthPeriodStart, + End: pastGrowthPeriodEnd, + CompositeQuery: req.CompositeQuery.Clone(), + Variables: make(map[string]interface{}, 0), + NoCache: false, + } + updateStepInterval(pastGrowthQuery) + + var past2GrowthPeriodStart, past2GrowthPeriodEnd int64 + switch seasonality { + case SeasonalityWeekly: + past2GrowthPeriodStart = start - 3*oneWeekOffset + past2GrowthPeriodEnd = start - 2*oneWeekOffset + case SeasonalityDaily: + past2GrowthPeriodStart = start - 3*oneDayOffset + past2GrowthPeriodEnd = start - 2*oneDayOffset + case SeasonalityHourly: + past2GrowthPeriodStart = start - 3*oneHourOffset + past2GrowthPeriodEnd = start - 2*oneHourOffset + } + + past2GrowthQuery := &v3.QueryRangeParamsV3{ + Start: past2GrowthPeriodStart, + End: past2GrowthPeriodEnd, + CompositeQuery: req.CompositeQuery.Clone(), + Variables: make(map[string]interface{}, 0), + NoCache: false, + } + updateStepInterval(past2GrowthQuery) + + var past3GrowthPeriodStart, past3GrowthPeriodEnd int64 + switch seasonality { + case SeasonalityWeekly: + past3GrowthPeriodStart = start - 4*oneWeekOffset + past3GrowthPeriodEnd = start - 3*oneWeekOffset + case SeasonalityDaily: + past3GrowthPeriodStart = start - 4*oneDayOffset + past3GrowthPeriodEnd = start - 3*oneDayOffset + case SeasonalityHourly: + past3GrowthPeriodStart = start - 4*oneHourOffset + past3GrowthPeriodEnd = start - 3*oneHourOffset + } + + past3GrowthQuery := &v3.QueryRangeParamsV3{ + Start: past3GrowthPeriodStart, + End: past3GrowthPeriodEnd, + CompositeQuery: req.CompositeQuery.Clone(), + Variables: make(map[string]interface{}, 0), + NoCache: false, + } + updateStepInterval(past3GrowthQuery) + + return &anomalyQueryParams{ + CurrentPeriodQuery: currentPeriodQuery, + PastPeriodQuery: pastPeriodQuery, + CurrentSeasonQuery: currentGrowthQuery, + PastSeasonQuery: pastGrowthQuery, + Past2SeasonQuery: past2GrowthQuery, + Past3SeasonQuery: past3GrowthQuery, + } +} + +type anomalyQueryResults struct { + CurrentPeriodResults []*v3.Result + PastPeriodResults []*v3.Result + CurrentSeasonResults []*v3.Result + PastSeasonResults []*v3.Result + Past2SeasonResults []*v3.Result + Past3SeasonResults []*v3.Result +} diff --git a/ee/query-service/anomaly/provider.go b/ee/query-service/anomaly/provider.go new file mode 100644 index 0000000000..6e0f7a8cd0 --- /dev/null +++ b/ee/query-service/anomaly/provider.go @@ -0,0 +1,9 @@ +package anomaly + +import ( + "context" +) + +type Provider interface { + GetAnomalies(ctx context.Context, req *GetAnomaliesRequest) (*GetAnomaliesResponse, error) +} diff --git a/ee/query-service/anomaly/seasonal.go b/ee/query-service/anomaly/seasonal.go new file mode 100644 index 0000000000..9b5f33d3df --- /dev/null +++ b/ee/query-service/anomaly/seasonal.go @@ -0,0 +1,466 @@ +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) { + zap.L().Info("fetching results for current period", zap.Any("currentPeriodQuery", params.CurrentPeriodQuery)) + 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 + } + + zap.L().Info("fetching results for past period", zap.Any("pastPeriodQuery", params.PastPeriodQuery)) + 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 + } + + zap.L().Info("fetching results for current season", zap.Any("currentSeasonQuery", params.CurrentSeasonQuery)) + 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 + } + + zap.L().Info("fetching results for past season", zap.Any("pastSeasonQuery", params.PastSeasonQuery)) + 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 + } + + zap.L().Info("fetching results for past 2 season", zap.Any("past2SeasonQuery", params.Past2SeasonQuery)) + 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 + } + + zap.L().Info("fetching results for past 3 season", zap.Any("past3SeasonQuery", params.Past3SeasonQuery)) + 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 = int(math.Max(0, float64(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, predictedSeries *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(predictedSeries, movingAvgWindowSize, idx) + zScoreThreshold*p.getStdDev(series) + lowerBound := p.getMovingAvg(predictedSeries, 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, + predictedSeries, + 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 +} diff --git a/ee/query-service/anomaly/weekly.go b/ee/query-service/anomaly/weekly.go new file mode 100644 index 0000000000..407e7e6440 --- /dev/null +++ b/ee/query-service/anomaly/weekly.go @@ -0,0 +1,43 @@ +package anomaly + +import ( + "context" + + querierV2 "go.signoz.io/signoz/pkg/query-service/app/querier/v2" + "go.signoz.io/signoz/pkg/query-service/app/queryBuilder" +) + +type WeeklyProvider struct { + BaseSeasonalProvider +} + +var _ BaseProvider = (*WeeklyProvider)(nil) + +func (wp *WeeklyProvider) GetBaseSeasonalProvider() *BaseSeasonalProvider { + return &wp.BaseSeasonalProvider +} + +func NewWeeklyProvider(opts ...GenericProviderOption[*WeeklyProvider]) *WeeklyProvider { + wp := &WeeklyProvider{ + BaseSeasonalProvider: BaseSeasonalProvider{}, + } + + for _, opt := range opts { + opt(wp) + } + + wp.querierV2 = querierV2.NewQuerier(querierV2.QuerierOptions{ + Reader: wp.reader, + Cache: wp.cache, + KeyGenerator: queryBuilder.NewKeyGenerator(), + FluxInterval: wp.fluxInterval, + FeatureLookup: wp.ff, + }) + + return wp +} + +func (p *WeeklyProvider) GetAnomalies(ctx context.Context, req *GetAnomaliesRequest) (*GetAnomaliesResponse, error) { + req.Seasonality = SeasonalityWeekly + return p.getAnomalies(ctx, req) +} diff --git a/ee/query-service/app/api/api.go b/ee/query-service/app/api/api.go index bb36fdf479..82557705fd 100644 --- a/ee/query-service/app/api/api.go +++ b/ee/query-service/app/api/api.go @@ -38,8 +38,7 @@ type APIHandlerOptions struct { Cache cache.Cache Gateway *httputil.ReverseProxy // Querier Influx Interval - FluxInterval time.Duration - + FluxInterval time.Duration UseLogsNewSchema bool } @@ -178,6 +177,8 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *baseapp.AuthMiddlew am.ViewAccess(ah.listLicensesV2)). Methods(http.MethodGet) + router.HandleFunc("/api/v4/query_range", am.ViewAccess(ah.queryRangeV4)).Methods(http.MethodPost) + // Gateway router.PathPrefix(gateway.RoutePrefix).HandlerFunc(am.AdminAccess(ah.ServeGatewayHTTP)) diff --git a/ee/query-service/app/api/queryrange.go b/ee/query-service/app/api/queryrange.go new file mode 100644 index 0000000000..d4f3eb975a --- /dev/null +++ b/ee/query-service/app/api/queryrange.go @@ -0,0 +1,119 @@ +package api + +import ( + "bytes" + "fmt" + "io" + "net/http" + + "go.signoz.io/signoz/ee/query-service/anomaly" + baseapp "go.signoz.io/signoz/pkg/query-service/app" + "go.signoz.io/signoz/pkg/query-service/app/queryBuilder" + "go.signoz.io/signoz/pkg/query-service/model" + v3 "go.signoz.io/signoz/pkg/query-service/model/v3" + "go.uber.org/zap" +) + +func (aH *APIHandler) queryRangeV4(w http.ResponseWriter, r *http.Request) { + + bodyBytes, _ := io.ReadAll(r.Body) + r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) + + queryRangeParams, apiErrorObj := baseapp.ParseQueryRangeParams(r) + + if apiErrorObj != nil { + zap.L().Error("error parsing metric query range params", zap.Error(apiErrorObj.Err)) + RespondError(w, apiErrorObj, nil) + return + } + queryRangeParams.Version = "v4" + + // add temporality for each metric + temporalityErr := aH.PopulateTemporality(r.Context(), queryRangeParams) + if temporalityErr != nil { + zap.L().Error("Error while adding temporality for metrics", zap.Error(temporalityErr)) + RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: temporalityErr}, nil) + return + } + + anomalyQueryExists := false + anomalyQuery := &v3.BuilderQuery{} + if queryRangeParams.CompositeQuery.QueryType == v3.QueryTypeBuilder { + for _, query := range queryRangeParams.CompositeQuery.BuilderQueries { + for _, fn := range query.Functions { + if fn.Name == v3.FunctionNameAnomaly { + anomalyQueryExists = true + anomalyQuery = query + break + } + } + } + } + + if anomalyQueryExists { + // ensure all queries have metric data source, and there should be only one anomaly query + for _, query := range queryRangeParams.CompositeQuery.BuilderQueries { + if query.DataSource != v3.DataSourceMetrics { + RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("all queries must have metric data source")}, nil) + return + } + } + + // get the threshold, and seasonality from the anomaly query + var seasonality anomaly.Seasonality + for _, fn := range anomalyQuery.Functions { + if fn.Name == v3.FunctionNameAnomaly { + seasonalityStr, ok := fn.NamedArgs["seasonality"].(string) + if !ok { + seasonalityStr = "daily" + } + if seasonalityStr == "weekly" { + seasonality = anomaly.SeasonalityWeekly + } else if seasonalityStr == "daily" { + seasonality = anomaly.SeasonalityDaily + } else { + seasonality = anomaly.SeasonalityHourly + } + break + } + } + var provider anomaly.Provider + switch seasonality { + case anomaly.SeasonalityWeekly: + provider = anomaly.NewWeeklyProvider( + anomaly.WithCache[*anomaly.WeeklyProvider](aH.opts.Cache), + anomaly.WithKeyGenerator[*anomaly.WeeklyProvider](queryBuilder.NewKeyGenerator()), + anomaly.WithReader[*anomaly.WeeklyProvider](aH.opts.DataConnector), + anomaly.WithFeatureLookup[*anomaly.WeeklyProvider](aH.opts.FeatureFlags), + ) + case anomaly.SeasonalityDaily: + provider = anomaly.NewDailyProvider( + anomaly.WithCache[*anomaly.DailyProvider](aH.opts.Cache), + anomaly.WithKeyGenerator[*anomaly.DailyProvider](queryBuilder.NewKeyGenerator()), + anomaly.WithReader[*anomaly.DailyProvider](aH.opts.DataConnector), + anomaly.WithFeatureLookup[*anomaly.DailyProvider](aH.opts.FeatureFlags), + ) + case anomaly.SeasonalityHourly: + provider = anomaly.NewHourlyProvider( + anomaly.WithCache[*anomaly.HourlyProvider](aH.opts.Cache), + anomaly.WithKeyGenerator[*anomaly.HourlyProvider](queryBuilder.NewKeyGenerator()), + anomaly.WithReader[*anomaly.HourlyProvider](aH.opts.DataConnector), + anomaly.WithFeatureLookup[*anomaly.HourlyProvider](aH.opts.FeatureFlags), + ) + } + anomalies, err := provider.GetAnomalies(r.Context(), &anomaly.GetAnomaliesRequest{Params: queryRangeParams}) + if err != nil { + RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil) + return + } + uniqueResults := make(map[string]*v3.Result) + for _, anomaly := range anomalies.Results { + uniqueResults[anomaly.QueryName] = anomaly + uniqueResults[anomaly.QueryName].IsAnomaly = true + } + aH.Respond(w, uniqueResults) + } else { + r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) + aH.QueryRangeV4(w, r) + } +} diff --git a/ee/query-service/app/db/metrics.go b/ee/query-service/app/db/metrics.go deleted file mode 100644 index 0cc8a55c32..0000000000 --- a/ee/query-service/app/db/metrics.go +++ /dev/null @@ -1,401 +0,0 @@ -package db - -import ( - "context" - "crypto/md5" - "encoding/json" - "fmt" - "reflect" - "regexp" - "sort" - "strings" - "time" - - "go.signoz.io/signoz/ee/query-service/model" - baseconst "go.signoz.io/signoz/pkg/query-service/constants" - basemodel "go.signoz.io/signoz/pkg/query-service/model" - "go.signoz.io/signoz/pkg/query-service/utils" - "go.uber.org/zap" -) - -// GetMetricResultEE runs the query and returns list of time series -func (r *ClickhouseReader) GetMetricResultEE(ctx context.Context, query string) ([]*basemodel.Series, string, error) { - - defer utils.Elapsed("GetMetricResult", nil)() - zap.L().Info("Executing metric result query: ", zap.String("query", query)) - - var hash string - // If getSubTreeSpans function is used in the clickhouse query - if strings.Contains(query, "getSubTreeSpans(") { - var err error - query, hash, err = r.getSubTreeSpansCustomFunction(ctx, query, hash) - if err == fmt.Errorf("no spans found for the given query") { - return nil, "", nil - } - if err != nil { - return nil, "", err - } - } - - rows, err := r.conn.Query(ctx, query) - if err != nil { - zap.L().Error("Error in processing query", zap.Error(err)) - return nil, "", fmt.Errorf("error in processing query") - } - - var ( - columnTypes = rows.ColumnTypes() - columnNames = rows.Columns() - vars = make([]interface{}, len(columnTypes)) - ) - for i := range columnTypes { - vars[i] = reflect.New(columnTypes[i].ScanType()).Interface() - } - // when group by is applied, each combination of cartesian product - // of attributes is separate series. each item in metricPointsMap - // represent a unique series. - metricPointsMap := make(map[string][]basemodel.MetricPoint) - // attribute key-value pairs for each group selection - attributesMap := make(map[string]map[string]string) - - defer rows.Close() - for rows.Next() { - if err := rows.Scan(vars...); err != nil { - return nil, "", err - } - var groupBy []string - var metricPoint basemodel.MetricPoint - groupAttributes := make(map[string]string) - // Assuming that the end result row contains a timestamp, value and option labels - // Label key and value are both strings. - for idx, v := range vars { - colName := columnNames[idx] - switch v := v.(type) { - case *string: - // special case for returning all labels - if colName == "fullLabels" { - var metric map[string]string - err := json.Unmarshal([]byte(*v), &metric) - if err != nil { - return nil, "", err - } - for key, val := range metric { - groupBy = append(groupBy, val) - groupAttributes[key] = val - } - } else { - groupBy = append(groupBy, *v) - groupAttributes[colName] = *v - } - case *time.Time: - metricPoint.Timestamp = v.UnixMilli() - case *float64: - metricPoint.Value = *v - case **float64: - // ch seems to return this type when column is derived from - // SELECT count(*)/ SELECT count(*) - floatVal := *v - if floatVal != nil { - metricPoint.Value = *floatVal - } - case *float32: - float32Val := float32(*v) - metricPoint.Value = float64(float32Val) - case *uint8, *uint64, *uint16, *uint32: - if _, ok := baseconst.ReservedColumnTargetAliases[colName]; ok { - metricPoint.Value = float64(reflect.ValueOf(v).Elem().Uint()) - } else { - groupBy = append(groupBy, fmt.Sprintf("%v", reflect.ValueOf(v).Elem().Uint())) - groupAttributes[colName] = fmt.Sprintf("%v", reflect.ValueOf(v).Elem().Uint()) - } - case *int8, *int16, *int32, *int64: - if _, ok := baseconst.ReservedColumnTargetAliases[colName]; ok { - metricPoint.Value = float64(reflect.ValueOf(v).Elem().Int()) - } else { - groupBy = append(groupBy, fmt.Sprintf("%v", reflect.ValueOf(v).Elem().Int())) - groupAttributes[colName] = fmt.Sprintf("%v", reflect.ValueOf(v).Elem().Int()) - } - default: - zap.L().Error("invalid var found in metric builder query result", zap.Any("var", v), zap.String("colName", colName)) - } - } - sort.Strings(groupBy) - key := strings.Join(groupBy, "") - attributesMap[key] = groupAttributes - metricPointsMap[key] = append(metricPointsMap[key], metricPoint) - } - - var seriesList []*basemodel.Series - for key := range metricPointsMap { - points := metricPointsMap[key] - // first point in each series could be invalid since the - // aggregations are applied with point from prev series - if len(points) != 0 && len(points) > 1 { - points = points[1:] - } - attributes := attributesMap[key] - series := basemodel.Series{Labels: attributes, Points: points} - seriesList = append(seriesList, &series) - } - // err = r.conn.Exec(ctx, "DROP TEMPORARY TABLE IF EXISTS getSubTreeSpans"+hash) - // if err != nil { - // zap.L().Error("Error in dropping temporary table: ", err) - // return nil, err - // } - if hash == "" { - return seriesList, hash, nil - } else { - return seriesList, "getSubTreeSpans" + hash, nil - } -} - -func (r *ClickhouseReader) getSubTreeSpansCustomFunction(ctx context.Context, query string, hash string) (string, string, error) { - - zap.L().Debug("Executing getSubTreeSpans function") - - // str1 := `select fromUnixTimestamp64Milli(intDiv( toUnixTimestamp64Milli ( timestamp ), 100) * 100) AS interval, toFloat64(count()) as count from (select timestamp, spanId, parentSpanId, durationNano from getSubTreeSpans(select * from signoz_traces.signoz_index_v2 where serviceName='frontend' and name='/driver.DriverService/FindNearest' and traceID='00000000000000004b0a863cb5ed7681') where name='FindDriverIDs' group by interval order by interval asc;` - - // process the query to fetch subTree query - var subtreeInput string - query, subtreeInput, hash = processQuery(query, hash) - - err := r.conn.Exec(ctx, "DROP TABLE IF EXISTS getSubTreeSpans"+hash) - if err != nil { - zap.L().Error("Error in dropping temporary table", zap.Error(err)) - return query, hash, err - } - - // Create temporary table to store the getSubTreeSpans() results - zap.L().Debug("Creating temporary table getSubTreeSpans", zap.String("hash", hash)) - err = r.conn.Exec(ctx, "CREATE TABLE IF NOT EXISTS "+"getSubTreeSpans"+hash+" (timestamp DateTime64(9) CODEC(DoubleDelta, LZ4), traceID FixedString(32) CODEC(ZSTD(1)), spanID String CODEC(ZSTD(1)), parentSpanID String CODEC(ZSTD(1)), rootSpanID String CODEC(ZSTD(1)), serviceName LowCardinality(String) CODEC(ZSTD(1)), name LowCardinality(String) CODEC(ZSTD(1)), rootName LowCardinality(String) CODEC(ZSTD(1)), durationNano UInt64 CODEC(T64, ZSTD(1)), kind Int8 CODEC(T64, ZSTD(1)), tagMap Map(LowCardinality(String), String) CODEC(ZSTD(1)), events Array(String) CODEC(ZSTD(2))) ENGINE = MergeTree() ORDER BY (timestamp)") - if err != nil { - zap.L().Error("Error in creating temporary table", zap.Error(err)) - return query, hash, err - } - - var getSpansSubQueryDBResponses []model.GetSpansSubQueryDBResponse - getSpansSubQuery := subtreeInput - // Execute the subTree query - zap.L().Debug("Executing subTree query", zap.String("query", getSpansSubQuery)) - err = r.conn.Select(ctx, &getSpansSubQueryDBResponses, getSpansSubQuery) - - // zap.L().Info(getSpansSubQuery) - - if err != nil { - zap.L().Error("Error in processing sql query", zap.Error(err)) - return query, hash, fmt.Errorf("error in processing sql query") - } - - var searchScanResponses []basemodel.SearchSpanDBResponseItem - - // TODO : @ankit: I think the algorithm does not need to assume that subtrees are from the same TraceID. We can take this as an improvement later. - // Fetch all the spans from of same TraceID so that we can build subtree - modelQuery := fmt.Sprintf("SELECT timestamp, traceID, model FROM %s.%s WHERE traceID=$1", r.TraceDB, r.SpansTable) - - if len(getSpansSubQueryDBResponses) == 0 { - return query, hash, fmt.Errorf("no spans found for the given query") - } - zap.L().Debug("Executing query to fetch all the spans from the same TraceID: ", zap.String("modelQuery", modelQuery)) - err = r.conn.Select(ctx, &searchScanResponses, modelQuery, getSpansSubQueryDBResponses[0].TraceID) - - if err != nil { - zap.L().Error("Error in processing sql query", zap.Error(err)) - return query, hash, fmt.Errorf("error in processing sql query") - } - - // Process model to fetch the spans - zap.L().Debug("Processing model to fetch the spans") - searchSpanResponses := []basemodel.SearchSpanResponseItem{} - for _, item := range searchScanResponses { - var jsonItem basemodel.SearchSpanResponseItem - json.Unmarshal([]byte(item.Model), &jsonItem) - jsonItem.TimeUnixNano = uint64(item.Timestamp.UnixNano()) - if jsonItem.Events == nil { - jsonItem.Events = []string{} - } - searchSpanResponses = append(searchSpanResponses, jsonItem) - } - // Build the subtree and store all the subtree spans in temporary table getSubTreeSpans+hash - // Use map to store pointer to the spans to avoid duplicates and save memory - zap.L().Debug("Building the subtree to store all the subtree spans in temporary table getSubTreeSpans", zap.String("hash", hash)) - - treeSearchResponse, err := getSubTreeAlgorithm(searchSpanResponses, getSpansSubQueryDBResponses) - if err != nil { - zap.L().Error("Error in getSubTreeAlgorithm function", zap.Error(err)) - return query, hash, err - } - zap.L().Debug("Preparing batch to store subtree spans in temporary table getSubTreeSpans", zap.String("hash", hash)) - statement, err := r.conn.PrepareBatch(context.Background(), fmt.Sprintf("INSERT INTO getSubTreeSpans"+hash)) - if err != nil { - zap.L().Error("Error in preparing batch statement", zap.Error(err)) - return query, hash, err - } - for _, span := range treeSearchResponse { - var parentID string - if len(span.References) > 0 && span.References[0].RefType == "CHILD_OF" { - parentID = span.References[0].SpanId - } - err = statement.Append( - time.Unix(0, int64(span.TimeUnixNano)), - span.TraceID, - span.SpanID, - parentID, - span.RootSpanID, - span.ServiceName, - span.Name, - span.RootName, - uint64(span.DurationNano), - int8(span.Kind), - span.TagMap, - span.Events, - ) - if err != nil { - zap.L().Error("Error in processing sql query", zap.Error(err)) - return query, hash, err - } - } - zap.L().Debug("Inserting the subtree spans in temporary table getSubTreeSpans", zap.String("hash", hash)) - err = statement.Send() - if err != nil { - zap.L().Error("Error in sending statement", zap.Error(err)) - return query, hash, err - } - return query, hash, nil -} - -//lint:ignore SA4009 return hash is feeded to the query -func processQuery(query string, hash string) (string, string, string) { - re3 := regexp.MustCompile(`getSubTreeSpans`) - - submatchall3 := re3.FindAllStringIndex(query, -1) - getSubtreeSpansMatchIndex := submatchall3[0][1] - - query2countParenthesis := query[getSubtreeSpansMatchIndex:] - - sqlCompleteIndex := 0 - countParenthesisImbalance := 0 - for i, char := range query2countParenthesis { - - if string(char) == "(" { - countParenthesisImbalance += 1 - } - if string(char) == ")" { - countParenthesisImbalance -= 1 - } - if countParenthesisImbalance == 0 { - sqlCompleteIndex = i - break - } - } - subtreeInput := query2countParenthesis[1:sqlCompleteIndex] - - // hash the subtreeInput - hmd5 := md5.Sum([]byte(subtreeInput)) - hash = fmt.Sprintf("%x", hmd5) - - // Reformat the query to use the getSubTreeSpans function - query = query[:getSubtreeSpansMatchIndex] + hash + " " + query2countParenthesis[sqlCompleteIndex+1:] - return query, subtreeInput, hash -} - -// getSubTreeAlgorithm is an algorithm to build the subtrees of the spans and return the list of spans -func getSubTreeAlgorithm(payload []basemodel.SearchSpanResponseItem, getSpansSubQueryDBResponses []model.GetSpansSubQueryDBResponse) (map[string]*basemodel.SearchSpanResponseItem, error) { - - var spans []*model.SpanForTraceDetails - for _, spanItem := range payload { - var parentID string - if len(spanItem.References) > 0 && spanItem.References[0].RefType == "CHILD_OF" { - parentID = spanItem.References[0].SpanId - } - span := &model.SpanForTraceDetails{ - TimeUnixNano: spanItem.TimeUnixNano, - SpanID: spanItem.SpanID, - TraceID: spanItem.TraceID, - ServiceName: spanItem.ServiceName, - Name: spanItem.Name, - Kind: spanItem.Kind, - DurationNano: spanItem.DurationNano, - TagMap: spanItem.TagMap, - ParentID: parentID, - Events: spanItem.Events, - HasError: spanItem.HasError, - } - spans = append(spans, span) - } - - zap.L().Debug("Building Tree") - roots, err := buildSpanTrees(&spans) - if err != nil { - return nil, err - } - searchSpansResult := make(map[string]*basemodel.SearchSpanResponseItem) - // Every span which was fetched from getSubTree Input SQL query is considered root - // For each root, get the subtree spans - for _, getSpansSubQueryDBResponse := range getSpansSubQueryDBResponses { - targetSpan := &model.SpanForTraceDetails{} - // zap.L().Debug("Building tree for span id: " + getSpansSubQueryDBResponse.SpanID + " " + strconv.Itoa(i+1) + " of " + strconv.Itoa(len(getSpansSubQueryDBResponses))) - // Search target span object in the tree - for _, root := range roots { - targetSpan, err = breadthFirstSearch(root, getSpansSubQueryDBResponse.SpanID) - if targetSpan != nil { - break - } - if err != nil { - zap.L().Error("Error during BreadthFirstSearch()", zap.Error(err)) - return nil, err - } - } - if targetSpan == nil { - return nil, nil - } - // Build subtree for the target span - // Mark the target span as root by setting parent ID as empty string - targetSpan.ParentID = "" - preParents := []*model.SpanForTraceDetails{targetSpan} - children := []*model.SpanForTraceDetails{} - - // Get the subtree child spans - for i := 0; len(preParents) != 0; i++ { - parents := []*model.SpanForTraceDetails{} - for _, parent := range preParents { - children = append(children, parent.Children...) - parents = append(parents, parent.Children...) - } - preParents = parents - } - - resultSpans := children - // Add the target span to the result spans - resultSpans = append(resultSpans, targetSpan) - - for _, item := range resultSpans { - references := []basemodel.OtelSpanRef{ - { - TraceId: item.TraceID, - SpanId: item.ParentID, - RefType: "CHILD_OF", - }, - } - - if item.Events == nil { - item.Events = []string{} - } - searchSpansResult[item.SpanID] = &basemodel.SearchSpanResponseItem{ - TimeUnixNano: item.TimeUnixNano, - SpanID: item.SpanID, - TraceID: item.TraceID, - ServiceName: item.ServiceName, - Name: item.Name, - Kind: item.Kind, - References: references, - DurationNano: item.DurationNano, - TagMap: item.TagMap, - Events: item.Events, - HasError: item.HasError, - RootSpanID: getSpansSubQueryDBResponse.SpanID, - RootName: targetSpan.Name, - } - } - } - return searchSpansResult, nil -} diff --git a/ee/query-service/app/server.go b/ee/query-service/app/server.go index 9845ee670b..54eb7bd1e5 100644 --- a/ee/query-service/app/server.go +++ b/ee/query-service/app/server.go @@ -170,6 +170,14 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) { return nil, err } } + var c cache.Cache + if serverOptions.CacheConfigPath != "" { + cacheOpts, err := cache.LoadFromYAMLCacheConfigFile(serverOptions.CacheConfigPath) + if err != nil { + return nil, err + } + c = cache.NewCache(cacheOpts) + } <-readerReady rm, err := makeRulesManager(serverOptions.PromConfigPath, @@ -177,6 +185,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) { serverOptions.RuleRepoURL, localDB, reader, + c, serverOptions.DisableRules, lm, serverOptions.UseLogsNewSchema, @@ -237,15 +246,6 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) { telemetry.GetInstance().SetReader(reader) telemetry.GetInstance().SetSaasOperator(constants.SaasSegmentKey) - var c cache.Cache - if serverOptions.CacheConfigPath != "" { - cacheOpts, err := cache.LoadFromYAMLCacheConfigFile(serverOptions.CacheConfigPath) - if err != nil { - return nil, err - } - c = cache.NewCache(cacheOpts) - } - fluxInterval, err := time.ParseDuration(serverOptions.FluxInterval) if err != nil { @@ -732,6 +732,7 @@ func makeRulesManager( ruleRepoURL string, db *sqlx.DB, ch baseint.Reader, + cache cache.Cache, disableRules bool, fm baseint.FeatureLookup, useLogsNewSchema bool) (*baserules.Manager, error) { @@ -760,6 +761,7 @@ func makeRulesManager( DisableRules: disableRules, FeatureFlags: fm, Reader: ch, + Cache: cache, EvalDelay: baseconst.GetEvalDelay(), PrepareTaskFunc: rules.PrepareTaskFunc, diff --git a/ee/query-service/model/plans.go b/ee/query-service/model/plans.go index dbd8b56965..5b695143b7 100644 --- a/ee/query-service/model/plans.go +++ b/ee/query-service/model/plans.go @@ -13,7 +13,6 @@ const Onboarding = "ONBOARDING" const ChatSupport = "CHAT_SUPPORT" const Gateway = "GATEWAY" const PremiumSupport = "PREMIUM_SUPPORT" -const QueryBuilderSearchV2 = "QUERY_BUILDER_SEARCH_V2" var BasicPlan = basemodel.FeatureSet{ basemodel.Feature{ @@ -129,7 +128,7 @@ var BasicPlan = basemodel.FeatureSet{ Route: "", }, basemodel.Feature{ - Name: QueryBuilderSearchV2, + Name: basemodel.AnomalyDetection, Active: false, Usage: 0, UsageLimit: -1, @@ -244,8 +243,8 @@ var ProPlan = basemodel.FeatureSet{ Route: "", }, basemodel.Feature{ - Name: QueryBuilderSearchV2, - Active: false, + Name: basemodel.AnomalyDetection, + Active: true, Usage: 0, UsageLimit: -1, Route: "", @@ -373,8 +372,8 @@ var EnterprisePlan = basemodel.FeatureSet{ Route: "", }, basemodel.Feature{ - Name: QueryBuilderSearchV2, - Active: false, + Name: basemodel.AnomalyDetection, + Active: true, Usage: 0, UsageLimit: -1, Route: "", diff --git a/ee/query-service/rules/anomaly.go b/ee/query-service/rules/anomaly.go new file mode 100644 index 0000000000..a04bfc2840 --- /dev/null +++ b/ee/query-service/rules/anomaly.go @@ -0,0 +1,393 @@ +package rules + +import ( + "context" + "encoding/json" + "fmt" + "math" + "strings" + "sync" + "time" + + "go.uber.org/zap" + + "go.signoz.io/signoz/ee/query-service/anomaly" + "go.signoz.io/signoz/pkg/query-service/cache" + "go.signoz.io/signoz/pkg/query-service/common" + "go.signoz.io/signoz/pkg/query-service/model" + + querierV2 "go.signoz.io/signoz/pkg/query-service/app/querier/v2" + "go.signoz.io/signoz/pkg/query-service/app/queryBuilder" + "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/utils/labels" + "go.signoz.io/signoz/pkg/query-service/utils/times" + "go.signoz.io/signoz/pkg/query-service/utils/timestamp" + + "go.signoz.io/signoz/pkg/query-service/formatter" + + baserules "go.signoz.io/signoz/pkg/query-service/rules" + + yaml "gopkg.in/yaml.v2" +) + +const ( + RuleTypeAnomaly = "anomaly_rule" +) + +type AnomalyRule struct { + *baserules.BaseRule + + mtx sync.Mutex + + reader interfaces.Reader + + // querierV2 is used for alerts created after the introduction of new metrics query builder + querierV2 interfaces.Querier + + provider anomaly.Provider + + seasonality anomaly.Seasonality +} + +func NewAnomalyRule( + id string, + p *baserules.PostableRule, + featureFlags interfaces.FeatureLookup, + reader interfaces.Reader, + cache cache.Cache, + opts ...baserules.RuleOption, +) (*AnomalyRule, error) { + + zap.L().Info("creating new AnomalyRule", zap.String("id", id), zap.Any("opts", opts)) + + baseRule, err := baserules.NewBaseRule(id, p, reader, opts...) + if err != nil { + return nil, err + } + + t := AnomalyRule{ + BaseRule: baseRule, + } + + switch strings.ToLower(p.RuleCondition.Seasonality) { + case "hourly": + t.seasonality = anomaly.SeasonalityHourly + case "daily": + t.seasonality = anomaly.SeasonalityDaily + case "weekly": + t.seasonality = anomaly.SeasonalityWeekly + default: + t.seasonality = anomaly.SeasonalityDaily + } + + zap.L().Info("using seasonality", zap.String("seasonality", t.seasonality.String())) + + querierOptsV2 := querierV2.QuerierOptions{ + Reader: reader, + Cache: cache, + KeyGenerator: queryBuilder.NewKeyGenerator(), + FeatureLookup: featureFlags, + } + + t.querierV2 = querierV2.NewQuerier(querierOptsV2) + t.reader = reader + if t.seasonality == anomaly.SeasonalityHourly { + t.provider = anomaly.NewHourlyProvider( + anomaly.WithCache[*anomaly.HourlyProvider](cache), + anomaly.WithKeyGenerator[*anomaly.HourlyProvider](queryBuilder.NewKeyGenerator()), + anomaly.WithReader[*anomaly.HourlyProvider](reader), + anomaly.WithFeatureLookup[*anomaly.HourlyProvider](featureFlags), + ) + } else if t.seasonality == anomaly.SeasonalityDaily { + t.provider = anomaly.NewDailyProvider( + anomaly.WithCache[*anomaly.DailyProvider](cache), + anomaly.WithKeyGenerator[*anomaly.DailyProvider](queryBuilder.NewKeyGenerator()), + anomaly.WithReader[*anomaly.DailyProvider](reader), + anomaly.WithFeatureLookup[*anomaly.DailyProvider](featureFlags), + ) + } else if t.seasonality == anomaly.SeasonalityWeekly { + t.provider = anomaly.NewWeeklyProvider( + anomaly.WithCache[*anomaly.WeeklyProvider](cache), + anomaly.WithKeyGenerator[*anomaly.WeeklyProvider](queryBuilder.NewKeyGenerator()), + anomaly.WithReader[*anomaly.WeeklyProvider](reader), + anomaly.WithFeatureLookup[*anomaly.WeeklyProvider](featureFlags), + ) + } + return &t, nil +} + +func (r *AnomalyRule) Type() baserules.RuleType { + return RuleTypeAnomaly +} + +func (r *AnomalyRule) prepareQueryRange(ts time.Time) (*v3.QueryRangeParamsV3, error) { + + zap.L().Info("prepareQueryRange", zap.Int64("ts", ts.UnixMilli()), zap.Int64("evalWindow", r.EvalWindow().Milliseconds()), zap.Int64("evalDelay", r.EvalDelay().Milliseconds())) + + start := ts.Add(-time.Duration(r.EvalWindow())).UnixMilli() + end := ts.UnixMilli() + + if r.EvalDelay() > 0 { + start = start - int64(r.EvalDelay().Milliseconds()) + end = end - int64(r.EvalDelay().Milliseconds()) + } + // round to minute otherwise we could potentially miss data + start = start - (start % (60 * 1000)) + end = end - (end % (60 * 1000)) + + compositeQuery := r.Condition().CompositeQuery + + if compositeQuery.PanelType != v3.PanelTypeGraph { + compositeQuery.PanelType = v3.PanelTypeGraph + } + + // default mode + return &v3.QueryRangeParamsV3{ + Start: start, + End: end, + Step: int64(math.Max(float64(common.MinAllowedStepInterval(start, end)), 60)), + CompositeQuery: compositeQuery, + Variables: make(map[string]interface{}, 0), + NoCache: false, + }, nil +} + +func (r *AnomalyRule) GetSelectedQuery() string { + return r.Condition().GetSelectedQueryName() +} + +func (r *AnomalyRule) buildAndRunQuery(ctx context.Context, ts time.Time) (baserules.Vector, error) { + + params, err := r.prepareQueryRange(ts) + if err != nil { + return nil, err + } + err = r.PopulateTemporality(ctx, params) + if err != nil { + return nil, fmt.Errorf("internal error while setting temporality") + } + + anomalies, err := r.provider.GetAnomalies(ctx, &anomaly.GetAnomaliesRequest{ + Params: params, + Seasonality: r.seasonality, + }) + if err != nil { + return nil, err + } + + var queryResult *v3.Result + for _, result := range anomalies.Results { + if result.QueryName == r.GetSelectedQuery() { + queryResult = result + break + } + } + + var resultVector baserules.Vector + + scoresJSON, _ := json.Marshal(queryResult.AnomalyScores) + zap.L().Info("anomaly scores", zap.String("scores", string(scoresJSON))) + + for _, series := range queryResult.AnomalyScores { + smpl, shouldAlert := r.ShouldAlert(*series) + if shouldAlert { + resultVector = append(resultVector, smpl) + } + } + return resultVector, nil +} + +func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, error) { + + prevState := r.State() + + valueFormatter := formatter.FromUnit(r.Unit()) + res, err := r.buildAndRunQuery(ctx, ts) + + if err != nil { + return nil, err + } + + r.mtx.Lock() + defer r.mtx.Unlock() + + resultFPs := map[uint64]struct{}{} + var alerts = make(map[uint64]*baserules.Alert, len(res)) + + for _, smpl := range res { + l := make(map[string]string, len(smpl.Metric)) + for _, lbl := range smpl.Metric { + l[lbl.Name] = lbl.Value + } + + value := valueFormatter.Format(smpl.V, r.Unit()) + threshold := valueFormatter.Format(r.TargetVal(), r.Unit()) + zap.L().Debug("Alert template data for rule", zap.String("name", r.Name()), zap.String("formatter", valueFormatter.Name()), zap.String("value", value), zap.String("threshold", threshold)) + + tmplData := baserules.AlertTemplateData(l, value, threshold) + // Inject some convenience variables that are easier to remember for users + // who are not used to Go's templating system. + defs := "{{$labels := .Labels}}{{$value := .Value}}{{$threshold := .Threshold}}" + + // utility function to apply go template on labels and annotations + expand := func(text string) string { + + tmpl := baserules.NewTemplateExpander( + ctx, + defs+text, + "__alert_"+r.Name(), + tmplData, + times.Time(timestamp.FromTime(ts)), + nil, + ) + result, err := tmpl.Expand() + if err != nil { + result = fmt.Sprintf("", err) + zap.L().Error("Expanding alert template failed", zap.Error(err), zap.Any("data", tmplData)) + } + return result + } + + lb := labels.NewBuilder(smpl.Metric).Del(labels.MetricNameLabel).Del(labels.TemporalityLabel) + resultLabels := labels.NewBuilder(smpl.MetricOrig).Del(labels.MetricNameLabel).Del(labels.TemporalityLabel).Labels() + + for name, value := range r.Labels().Map() { + lb.Set(name, expand(value)) + } + + lb.Set(labels.AlertNameLabel, r.Name()) + lb.Set(labels.AlertRuleIdLabel, r.ID()) + lb.Set(labels.RuleSourceLabel, r.GeneratorURL()) + + annotations := make(labels.Labels, 0, len(r.Annotations().Map())) + for name, value := range r.Annotations().Map() { + annotations = append(annotations, labels.Label{Name: common.NormalizeLabelName(name), Value: expand(value)}) + } + if smpl.IsMissing { + lb.Set(labels.AlertNameLabel, "[No data] "+r.Name()) + } + + lbs := lb.Labels() + h := lbs.Hash() + resultFPs[h] = struct{}{} + + if _, ok := alerts[h]; ok { + zap.L().Error("the alert query returns duplicate records", zap.String("ruleid", r.ID()), zap.Any("alert", alerts[h])) + err = fmt.Errorf("duplicate alert found, vector contains metrics with the same labelset after applying alert labels") + return nil, err + } + + alerts[h] = &baserules.Alert{ + Labels: lbs, + QueryResultLables: resultLabels, + Annotations: annotations, + ActiveAt: ts, + State: model.StatePending, + Value: smpl.V, + GeneratorURL: r.GeneratorURL(), + Receivers: r.PreferredChannels(), + Missing: smpl.IsMissing, + } + } + + zap.L().Info("number of alerts found", zap.String("name", r.Name()), zap.Int("count", len(alerts))) + + // alerts[h] is ready, add or update active list now + for h, a := range alerts { + // Check whether we already have alerting state for the identifying label set. + // Update the last value and annotations if so, create a new alert entry otherwise. + if alert, ok := r.Active[h]; ok && alert.State != model.StateInactive { + + alert.Value = a.Value + alert.Annotations = a.Annotations + alert.Receivers = r.PreferredChannels() + continue + } + + r.Active[h] = a + } + + itemsToAdd := []model.RuleStateHistory{} + + // Check if any pending alerts should be removed or fire now. Write out alert timeseries. + for fp, a := range r.Active { + labelsJSON, err := json.Marshal(a.QueryResultLables) + if err != nil { + zap.L().Error("error marshaling labels", zap.Error(err), zap.Any("labels", a.Labels)) + } + if _, ok := resultFPs[fp]; !ok { + // If the alert was previously firing, keep it around for a given + // retention time so it is reported as resolved to the AlertManager. + if a.State == model.StatePending || (!a.ResolvedAt.IsZero() && ts.Sub(a.ResolvedAt) > baserules.ResolvedRetention) { + delete(r.Active, fp) + } + if a.State != model.StateInactive { + a.State = model.StateInactive + a.ResolvedAt = ts + itemsToAdd = append(itemsToAdd, model.RuleStateHistory{ + RuleID: r.ID(), + RuleName: r.Name(), + State: model.StateInactive, + StateChanged: true, + UnixMilli: ts.UnixMilli(), + Labels: model.LabelsString(labelsJSON), + Fingerprint: a.QueryResultLables.Hash(), + Value: a.Value, + }) + } + continue + } + + if a.State == model.StatePending && ts.Sub(a.ActiveAt) >= r.HoldDuration() { + a.State = model.StateFiring + a.FiredAt = ts + state := model.StateFiring + if a.Missing { + state = model.StateNoData + } + itemsToAdd = append(itemsToAdd, model.RuleStateHistory{ + RuleID: r.ID(), + RuleName: r.Name(), + State: state, + StateChanged: true, + UnixMilli: ts.UnixMilli(), + Labels: model.LabelsString(labelsJSON), + Fingerprint: a.QueryResultLables.Hash(), + Value: a.Value, + }) + } + } + + currentState := r.State() + + overallStateChanged := currentState != prevState + for idx, item := range itemsToAdd { + item.OverallStateChanged = overallStateChanged + item.OverallState = currentState + itemsToAdd[idx] = item + } + + r.RecordRuleStateHistory(ctx, prevState, currentState, itemsToAdd) + + return len(r.Active), nil +} + +func (r *AnomalyRule) String() string { + + ar := baserules.PostableRule{ + AlertName: r.Name(), + RuleCondition: r.Condition(), + EvalWindow: baserules.Duration(r.EvalWindow()), + Labels: r.Labels().Map(), + Annotations: r.Annotations().Map(), + PreferredChannels: r.PreferredChannels(), + } + + byt, err := yaml.Marshal(ar) + if err != nil { + return fmt.Sprintf("error marshaling alerting rule: %s", err.Error()) + } + + return string(byt) +} diff --git a/ee/query-service/rules/manager.go b/ee/query-service/rules/manager.go index 2b80441f0c..5ed35d4d34 100644 --- a/ee/query-service/rules/manager.go +++ b/ee/query-service/rules/manager.go @@ -53,6 +53,25 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error) // create promql rule task for evalution task = newTask(baserules.TaskTypeProm, opts.TaskName, time.Duration(opts.Rule.Frequency), rules, opts.ManagerOpts, opts.NotifyFunc, opts.RuleDB) + } else if opts.Rule.RuleType == baserules.RuleTypeAnomaly { + // create anomaly rule + ar, err := NewAnomalyRule( + ruleId, + opts.Rule, + opts.FF, + opts.Reader, + opts.Cache, + baserules.WithEvalDelay(opts.ManagerOpts.EvalDelay), + ) + if err != nil { + return task, err + } + + rules = append(rules, ar) + + // create anomaly rule task for evalution + task = newTask(baserules.TaskTypeCh, opts.TaskName, time.Duration(opts.Rule.Frequency), rules, opts.ManagerOpts, opts.NotifyFunc, opts.RuleDB) + } else { return nil, fmt.Errorf("unsupported rule type. Supported types: %s, %s", baserules.RuleTypeProm, baserules.RuleTypeThreshold) } diff --git a/frontend/package.json b/frontend/package.json index 51097f7696..a9119d0e63 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -207,7 +207,6 @@ "eslint-plugin-sonarjs": "^0.12.0", "husky": "^7.0.4", "is-ci": "^3.0.1", - "jest-playwright-preset": "^1.7.2", "jest-styled-components": "^7.0.8", "lint-staged": "^12.5.0", "msw": "1.3.2", diff --git a/frontend/public/locales/en-GB/alerts.json b/frontend/public/locales/en-GB/alerts.json index a43d04ab59..86f21c8c78 100644 --- a/frontend/public/locales/en-GB/alerts.json +++ b/frontend/public/locales/en-GB/alerts.json @@ -53,6 +53,7 @@ "option_atleastonce": "at least once", "option_onaverage": "on average", "option_intotal": "in total", + "option_last": "last", "option_above": "above", "option_below": "below", "option_equal": "is equal to", diff --git a/frontend/public/locales/en-GB/rules.json b/frontend/public/locales/en-GB/rules.json index 9d55a0ba0f..9ac3641c7a 100644 --- a/frontend/public/locales/en-GB/rules.json +++ b/frontend/public/locales/en-GB/rules.json @@ -40,6 +40,7 @@ "option_atleastonce": "at least once", "option_onaverage": "on average", "option_intotal": "in total", + "option_last": "last", "option_above": "above", "option_below": "below", "option_equal": "is equal to", diff --git a/frontend/public/locales/en-GB/services.json b/frontend/public/locales/en-GB/services.json index 4c49847031..f04c851759 100644 --- a/frontend/public/locales/en-GB/services.json +++ b/frontend/public/locales/en-GB/services.json @@ -1,3 +1,3 @@ { - "rps_over_100": "You are sending data at more than 100 RPS, your ingestion may be rate limited. Please reach out to us via Intercom support." + "rps_over_100": "You are sending data at more than 100 RPS, your ingestion may be rate limited. Please reach out to us via Intercom support or " } diff --git a/frontend/public/locales/en/alerts.json b/frontend/public/locales/en/alerts.json index e7ed6232ad..02d20a2977 100644 --- a/frontend/public/locales/en/alerts.json +++ b/frontend/public/locales/en/alerts.json @@ -53,6 +53,7 @@ "option_atleastonce": "at least once", "option_onaverage": "on average", "option_intotal": "in total", + "option_last": "last", "option_above": "above", "option_below": "below", "option_equal": "is equal to", diff --git a/frontend/public/locales/en/rules.json b/frontend/public/locales/en/rules.json index 9d55a0ba0f..9ac3641c7a 100644 --- a/frontend/public/locales/en/rules.json +++ b/frontend/public/locales/en/rules.json @@ -40,6 +40,7 @@ "option_atleastonce": "at least once", "option_onaverage": "on average", "option_intotal": "in total", + "option_last": "last", "option_above": "above", "option_below": "below", "option_equal": "is equal to", diff --git a/frontend/public/locales/en/services.json b/frontend/public/locales/en/services.json index 4c49847031..f04c851759 100644 --- a/frontend/public/locales/en/services.json +++ b/frontend/public/locales/en/services.json @@ -1,3 +1,3 @@ { - "rps_over_100": "You are sending data at more than 100 RPS, your ingestion may be rate limited. Please reach out to us via Intercom support." + "rps_over_100": "You are sending data at more than 100 RPS, your ingestion may be rate limited. Please reach out to us via Intercom support or " } diff --git a/frontend/src/AppRoutes/index.tsx b/frontend/src/AppRoutes/index.tsx index b900255172..8400afbde3 100644 --- a/frontend/src/AppRoutes/index.tsx +++ b/frontend/src/AppRoutes/index.tsx @@ -137,7 +137,6 @@ function App(): JSX.Element { window.analytics.identify(email, sanitizedIdentifyPayload); window.analytics.group(domain, groupTraits); - window.clarity('identify', email, name); posthog?.identify(email, { email, diff --git a/frontend/src/components/DraggableTableRow/tests/DraggableTableRow.test.tsx b/frontend/src/components/DraggableTableRow/tests/DraggableTableRow.test.tsx index f938a19203..67bbeb56f2 100644 --- a/frontend/src/components/DraggableTableRow/tests/DraggableTableRow.test.tsx +++ b/frontend/src/components/DraggableTableRow/tests/DraggableTableRow.test.tsx @@ -12,6 +12,20 @@ beforeAll(() => { matchMedia(); }); +jest.mock('uplot', () => { + const paths = { + spline: jest.fn(), + bars: jest.fn(), + }; + const uplotMock = jest.fn(() => ({ + paths, + })); + return { + paths, + default: uplotMock, + }; +}); + jest.mock('react-dnd', () => ({ useDrop: jest.fn().mockImplementation(() => [jest.fn(), jest.fn(), jest.fn()]), useDrag: jest.fn().mockImplementation(() => [jest.fn(), jest.fn(), jest.fn()]), diff --git a/frontend/src/components/Graph/utils.ts b/frontend/src/components/Graph/utils.ts index db30b6a8ce..f002d1402f 100644 --- a/frontend/src/components/Graph/utils.ts +++ b/frontend/src/components/Graph/utils.ts @@ -139,6 +139,7 @@ export const getGraphOptions = ( }, scales: { x: { + stacked: isStacked, grid: { display: true, color: getGridColor(), @@ -165,6 +166,7 @@ export const getGraphOptions = ( ticks: { color: getAxisLabelColor(currentTheme) }, }, y: { + stacked: isStacked, display: true, grid: { display: true, @@ -178,9 +180,6 @@ export const getGraphOptions = ( }, }, }, - stacked: { - display: isStacked === undefined ? false : 'auto', - }, }, elements: { line: { diff --git a/frontend/src/components/LogDetail/constants.ts b/frontend/src/components/LogDetail/constants.ts index 92199d4441..dea5121dd1 100644 --- a/frontend/src/components/LogDetail/constants.ts +++ b/frontend/src/components/LogDetail/constants.ts @@ -2,6 +2,14 @@ export const VIEW_TYPES = { OVERVIEW: 'OVERVIEW', JSON: 'JSON', CONTEXT: 'CONTEXT', + INFRAMETRICS: 'INFRAMETRICS', } as const; export type VIEWS = typeof VIEW_TYPES[keyof typeof VIEW_TYPES]; + +export const RESOURCE_KEYS = { + CLUSTER_NAME: 'k8s.cluster.name', + POD_NAME: 'k8s.pod.name', + NODE_NAME: 'k8s.node.name', + HOST_NAME: 'host.name', +} as const; diff --git a/frontend/src/components/LogDetail/index.tsx b/frontend/src/components/LogDetail/index.tsx index b138718ed9..4748312ceb 100644 --- a/frontend/src/components/LogDetail/index.tsx +++ b/frontend/src/components/LogDetail/index.tsx @@ -9,6 +9,7 @@ import cx from 'classnames'; import { LogType } from 'components/Logs/LogStateIndicator/LogStateIndicator'; import { LOCALSTORAGE } from 'constants/localStorage'; import ContextView from 'container/LogDetailedView/ContextView/ContextView'; +import InfraMetrics from 'container/LogDetailedView/InfraMetrics/InfraMetrics'; import JSONView from 'container/LogDetailedView/JsonView'; import Overview from 'container/LogDetailedView/Overview'; import { @@ -22,6 +23,7 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useIsDarkMode } from 'hooks/useDarkMode'; import { useNotifications } from 'hooks/useNotifications'; import { + BarChart2, Braces, Copy, Filter, @@ -36,7 +38,7 @@ import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData'; import { DataSource, StringOperators } from 'types/common/queryBuilder'; import { FORBID_DOM_PURIFY_TAGS } from 'utils/app'; -import { VIEW_TYPES, VIEWS } from './constants'; +import { RESOURCE_KEYS, VIEW_TYPES, VIEWS } from './constants'; import { LogDetailProps } from './LogDetail.interfaces'; import QueryBuilderSearchWrapper from './QueryBuilderSearchWrapper'; @@ -192,6 +194,17 @@ function LogDetail({ Context + +
+ + Metrics +
+
{selectedView === VIEW_TYPES.JSON && ( @@ -246,6 +259,15 @@ function LogDetail({ isEdit={isEdit} /> )} + {selectedView === VIEW_TYPES.INFRAMETRICS && ( + + )} ); } diff --git a/frontend/src/components/Logs/AddToQueryHOC.tsx b/frontend/src/components/Logs/AddToQueryHOC.tsx index df222b7552..d7e2c7156e 100644 --- a/frontend/src/components/Logs/AddToQueryHOC.tsx +++ b/frontend/src/components/Logs/AddToQueryHOC.tsx @@ -15,7 +15,7 @@ function AddToQueryHOC({ }: AddToQueryHOCProps): JSX.Element { const handleQueryAdd = (event: MouseEvent): void => { event.stopPropagation(); - onAddToQuery(fieldKey, fieldValue, OPERATORS.IN); + onAddToQuery(fieldKey, fieldValue, OPERATORS['=']); }; const popOverContent = useMemo(() => Add to query: {fieldKey}, [ diff --git a/frontend/src/components/Logs/LogStateIndicator/LogStateIndicator.styles.scss b/frontend/src/components/Logs/LogStateIndicator/LogStateIndicator.styles.scss index 61870abc71..2260bf5aa3 100644 --- a/frontend/src/components/Logs/LogStateIndicator/LogStateIndicator.styles.scss +++ b/frontend/src/components/Logs/LogStateIndicator/LogStateIndicator.styles.scss @@ -22,26 +22,21 @@ } &.INFO { - background-color: var(--bg-slate-400); + background-color: var(--bg-robin-500); } - &.WARNING, &.WARN { background-color: var(--bg-amber-500); } - &.ERROR { background-color: var(--bg-cherry-500); } - &.TRACE { - background-color: var(--bg-robin-300); + background-color: var(--bg-forest-400); } - &.DEBUG { - background-color: var(--bg-forest-500); + background-color: var(--bg-aqua-500); } - &.FATAL { background-color: var(--bg-sakura-500); } diff --git a/frontend/src/container/BillingContainer/BillingContainer.styles.scss b/frontend/src/container/BillingContainer/BillingContainer.styles.scss index e4c7deec06..2bc41d89e6 100644 --- a/frontend/src/container/BillingContainer/BillingContainer.styles.scss +++ b/frontend/src/container/BillingContainer/BillingContainer.styles.scss @@ -50,6 +50,13 @@ align-items: center; } } + + .billing-update-note { + text-align: left; + font-size: 13px; + color: var(--bg-vanilla-200); + margin-top: 16px; + } } .ant-skeleton.ant-skeleton-element.ant-skeleton-active { @@ -75,5 +82,9 @@ } } } + + .billing-update-note { + color: var(--bg-ink-200); + } } } diff --git a/frontend/src/container/BillingContainer/BillingContainer.tsx b/frontend/src/container/BillingContainer/BillingContainer.tsx index e366f068b2..449474a429 100644 --- a/frontend/src/container/BillingContainer/BillingContainer.tsx +++ b/frontend/src/container/BillingContainer/BillingContainer.tsx @@ -348,7 +348,12 @@ export default function BillingContainer(): JSX.Element { const BillingUsageGraphCallback = useCallback( () => !isLoading && !isFetchingBillingData ? ( - + <> + +
+ Note: Billing metrics are updated once every 24 hours. +
+ ) : ( diff --git a/frontend/src/container/CreateAlertRule/defaults.ts b/frontend/src/container/CreateAlertRule/defaults.ts index 20cd020158..f9735e7644 100644 --- a/frontend/src/container/CreateAlertRule/defaults.ts +++ b/frontend/src/container/CreateAlertRule/defaults.ts @@ -65,7 +65,7 @@ export const logAlertDefaults: AlertDef = { chQueries: { A: { name: 'A', - query: `select \ntoStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 30 MINUTE) AS interval, \ntoFloat64(count()) as value \nFROM signoz_logs.distributed_logs \nWHERE timestamp BETWEEN {{.start_timestamp_nano}} AND {{.end_timestamp_nano}} \nGROUP BY interval;\n\n-- available variables:\n-- \t{{.start_timestamp_nano}}\n-- \t{{.end_timestamp_nano}}\n\n-- required columns (or alias):\n-- \tvalue\n-- \tinterval`, + query: `select \ntoStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 30 MINUTE) AS interval, \ntoFloat64(count()) as value \nFROM signoz_logs.distributed_logs_v2 \nWHERE timestamp BETWEEN {{.start_timestamp_nano}} AND {{.end_timestamp_nano}} \nGROUP BY interval;\n\n-- available variables:\n-- \t{{.start_timestamp_nano}}\n-- \t{{.end_timestamp_nano}}\n\n-- required columns (or alias):\n-- \tvalue\n-- \tinterval`, legend: '', disabled: false, }, @@ -95,7 +95,7 @@ export const traceAlertDefaults: AlertDef = { chQueries: { A: { name: 'A', - query: `SELECT \n\ttoStartOfInterval(timestamp, INTERVAL 1 MINUTE) AS interval, \n\ttagMap['peer.service'] AS op_name, \n\ttoFloat64(avg(durationNano)) AS value \nFROM signoz_traces.distributed_signoz_index_v2 \nWHERE tagMap['peer.service']!='' \nAND timestamp BETWEEN {{.start_datetime}} AND {{.end_datetime}} \nGROUP BY (op_name, interval);\n\n-- available variables:\n-- \t{{.start_datetime}}\n-- \t{{.end_datetime}}\n\n-- required column alias:\n-- \tvalue\n-- \tinterval`, + query: `SELECT \n\ttoStartOfInterval(timestamp, INTERVAL 1 MINUTE) AS interval, \n\tstringTagMap['peer.service'] AS op_name, \n\ttoFloat64(avg(durationNano)) AS value \nFROM signoz_traces.distributed_signoz_index_v2 \nWHERE stringTagMap['peer.service']!='' \nAND timestamp BETWEEN {{.start_datetime}} AND {{.end_datetime}} \nGROUP BY (op_name, interval);\n\n-- available variables:\n-- \t{{.start_datetime}}\n-- \t{{.end_datetime}}\n\n-- required column alias:\n-- \tvalue\n-- \tinterval`, legend: '', disabled: false, }, diff --git a/frontend/src/container/FormAlertRules/RuleOptions.tsx b/frontend/src/container/FormAlertRules/RuleOptions.tsx index 2ef6bba4c0..da265f34cc 100644 --- a/frontend/src/container/FormAlertRules/RuleOptions.tsx +++ b/frontend/src/container/FormAlertRules/RuleOptions.tsx @@ -103,6 +103,7 @@ function RuleOptions({ {t('option_allthetimes')} {t('option_onaverage')} {t('option_intotal')} + {t('option_last')} ); diff --git a/frontend/src/container/FormAlertRules/index.tsx b/frontend/src/container/FormAlertRules/index.tsx index f53a6b2cfe..2947b2a0b3 100644 --- a/frontend/src/container/FormAlertRules/index.tsx +++ b/frontend/src/container/FormAlertRules/index.tsx @@ -370,7 +370,10 @@ function FormAlertRules({ }); // invalidate rule in cache - ruleCache.invalidateQueries([REACT_QUERY_KEY.ALERT_RULE_DETAILS, ruleId]); + ruleCache.invalidateQueries([ + REACT_QUERY_KEY.ALERT_RULE_DETAILS, + `${ruleId}`, + ]); // eslint-disable-next-line sonarjs/no-identical-functions setTimeout(() => { diff --git a/frontend/src/container/GridCardLayout/GridCard/FullView/WidgetFullView.styles.scss b/frontend/src/container/GridCardLayout/GridCard/FullView/WidgetFullView.styles.scss index 29d578f096..78c4459929 100644 --- a/frontend/src/container/GridCardLayout/GridCard/FullView/WidgetFullView.styles.scss +++ b/frontend/src/container/GridCardLayout/GridCard/FullView/WidgetFullView.styles.scss @@ -15,6 +15,13 @@ box-sizing: border-box; margin: 16px 0; border-radius: 3px; + + .global-search { + .ant-input-group-addon { + border: none; + background-color: var(--bg-ink-300); + } + } } .height-widget { @@ -55,3 +62,15 @@ } } } + +.lightMode { + .full-view-container { + .graph-container { + .global-search { + .ant-input-group-addon { + background-color: var(--bg-vanilla-200); + } + } + } + } +} diff --git a/frontend/src/container/GridCardLayout/GridCard/FullView/index.tsx b/frontend/src/container/GridCardLayout/GridCard/FullView/index.tsx index 7511a4d445..d682af12a8 100644 --- a/frontend/src/container/GridCardLayout/GridCard/FullView/index.tsx +++ b/frontend/src/container/GridCardLayout/GridCard/FullView/index.tsx @@ -1,7 +1,11 @@ import './WidgetFullView.styles.scss'; -import { LoadingOutlined, SyncOutlined } from '@ant-design/icons'; -import { Button, Spin } from 'antd'; +import { + LoadingOutlined, + SearchOutlined, + SyncOutlined, +} from '@ant-design/icons'; +import { Button, Input, Spin } from 'antd'; import cx from 'classnames'; import { ToggleGraphProps } from 'components/Graph/types'; import Spinner from 'components/Spinner'; @@ -140,7 +144,7 @@ function FullView({ const [graphsVisibilityStates, setGraphsVisibilityStates] = useState< boolean[] - >(Array(response.data?.payload.data.result.length).fill(true)); + >(Array(response.data?.payload?.data?.result?.length).fill(true)); useEffect(() => { const { @@ -172,6 +176,10 @@ function FullView({ const isListView = widget.panelTypes === PANEL_TYPES.LIST; + const isTablePanel = widget.panelTypes === PANEL_TYPES.TABLE; + + const [searchTerm, setSearchTerm] = useState(''); + if (response.isLoading && widget.panelTypes !== PANEL_TYPES.LIST) { return ; } @@ -216,6 +224,18 @@ function FullView({ }} isGraphLegendToggleAvailable={canModifyChart} > + {isTablePanel && ( + } + className="global-search" + placeholder="Search..." + allowClear + key={widget.id} + onChange={(e): void => { + setSearchTerm(e.target.value || ''); + }} + /> + )} diff --git a/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx b/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx index b76c7c9f73..4d5c7fa94c 100644 --- a/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx +++ b/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx @@ -234,6 +234,8 @@ function WidgetGraphComponent({ }); }; + const [searchTerm, setSearchTerm] = useState(''); + const loadingState = (queryResponse.isLoading || queryResponse.status === 'idle') && widget.panelTypes !== PANEL_TYPES.LIST; @@ -317,6 +319,7 @@ function WidgetGraphComponent({ isWarning={isWarning} isFetchingResponse={isFetchingResponse} tableProcessedDataRef={tableProcessedDataRef} + setSearchTerm={setSearchTerm} /> {queryResponse.isLoading && widget.panelTypes !== PANEL_TYPES.LIST && ( @@ -337,6 +340,7 @@ function WidgetGraphComponent({ onDragSelect={onDragSelect} tableProcessedDataRef={tableProcessedDataRef} customTooltipElement={customTooltipElement} + searchTerm={searchTerm} /> )} diff --git a/frontend/src/container/GridCardLayout/GridCard/index.tsx b/frontend/src/container/GridCardLayout/GridCard/index.tsx index a618f807a5..66ce70fb86 100644 --- a/frontend/src/container/GridCardLayout/GridCard/index.tsx +++ b/frontend/src/container/GridCardLayout/GridCard/index.tsx @@ -11,6 +11,7 @@ import { isEqual } from 'lodash-es'; import isEmpty from 'lodash-es/isEmpty'; import { useDashboard } from 'providers/Dashboard/Dashboard'; import { memo, useEffect, useRef, useState } from 'react'; +import { useQueryClient } from 'react-query'; import { useDispatch, useSelector } from 'react-redux'; import { UpdateTimeInterval } from 'store/actions'; import { AppState } from 'store/reducers'; @@ -48,6 +49,7 @@ function GridCardGraph({ AppState, GlobalReducer >((state) => state.globalTime); + const queryClient = useQueryClient(); const handleBackNavigation = (): void => { const searchParams = new URLSearchParams(window.location.search); @@ -136,6 +138,25 @@ function GridCardGraph({ }; }); + // TODO [vikrantgupta25] remove this useEffect with refactor as this is prone to race condition + // this is added to tackle the case of async communication between VariableItem.tsx and GridCard.tsx + useEffect(() => { + if (variablesToGetUpdated.length > 0) { + queryClient.cancelQueries([ + maxTime, + minTime, + globalSelectedInterval, + variables, + widget?.query, + widget?.panelTypes, + widget.timePreferance, + widget.fillSpans, + requestData, + ]); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [variablesToGetUpdated]); + useEffect(() => { if (!isEqual(updatedQuery, requestData.query)) { setRequestData((prev) => ({ diff --git a/frontend/src/container/GridCardLayout/WidgetHeader/WidgetHeader.styles.scss b/frontend/src/container/GridCardLayout/WidgetHeader/WidgetHeader.styles.scss index 2fcb3e8e6f..11659e9a3e 100644 --- a/frontend/src/container/GridCardLayout/WidgetHeader/WidgetHeader.styles.scss +++ b/frontend/src/container/GridCardLayout/WidgetHeader/WidgetHeader.styles.scss @@ -2,7 +2,7 @@ display: flex; justify-content: space-between; align-items: center; - height: 30px; + height: 36px; width: 100%; padding: 0.5rem; box-sizing: border-box; @@ -10,6 +10,14 @@ font-weight: 600; cursor: move; + + .ant-input-group-addon { + border: none; + background-color: var(--bg-ink-500); + } + .search-header-icons { + cursor: pointer; + } } .widget-header-title { @@ -19,6 +27,7 @@ .widget-header-actions { display: flex; align-items: center; + gap: 8px; } .widget-header-more-options { visibility: hidden; @@ -30,6 +39,10 @@ padding: 8px; } +.widget-header-more-options-visible { + visibility: visible; +} + .widget-header-hover { visibility: visible; } @@ -37,3 +50,11 @@ .widget-api-actions { padding-right: 0.25rem; } + +.lightMode { + .widget-header-container { + .ant-input-group-addon { + background-color: inherit; + } + } +} diff --git a/frontend/src/container/GridCardLayout/WidgetHeader/index.tsx b/frontend/src/container/GridCardLayout/WidgetHeader/index.tsx index 7daa4e553d..d4aa6a4c09 100644 --- a/frontend/src/container/GridCardLayout/WidgetHeader/index.tsx +++ b/frontend/src/container/GridCardLayout/WidgetHeader/index.tsx @@ -9,9 +9,10 @@ import { ExclamationCircleOutlined, FullscreenOutlined, MoreOutlined, + SearchOutlined, WarningOutlined, } from '@ant-design/icons'; -import { Dropdown, MenuProps, Tooltip, Typography } from 'antd'; +import { Dropdown, Input, MenuProps, Tooltip, Typography } from 'antd'; import Spinner from 'components/Spinner'; import { QueryParams } from 'constants/query'; import { PANEL_TYPES } from 'constants/queryBuilder'; @@ -20,8 +21,9 @@ import useComponentPermission from 'hooks/useComponentPermission'; import history from 'lib/history'; import { RowData } from 'lib/query/createTableColumnsFromQuery'; import { isEmpty } from 'lodash-es'; +import { X } from 'lucide-react'; import { unparse } from 'papaparse'; -import { ReactNode, useCallback, useMemo } from 'react'; +import { ReactNode, useCallback, useMemo, useState } from 'react'; import { UseQueryResult } from 'react-query'; import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; @@ -51,6 +53,7 @@ interface IWidgetHeaderProps { isWarning: boolean; isFetchingResponse: boolean; tableProcessedDataRef: React.MutableRefObject; + setSearchTerm: React.Dispatch>; } function WidgetHeader({ @@ -67,6 +70,7 @@ function WidgetHeader({ isWarning, isFetchingResponse, tableProcessedDataRef, + setSearchTerm, }: IWidgetHeaderProps): JSX.Element | null { const onEditHandler = useCallback((): void => { const widgetId = widget.id; @@ -187,6 +191,10 @@ function WidgetHeader({ const updatedMenuList = useMemo(() => generateMenuList(actions), [actions]); + const [showGlobalSearch, setShowGlobalSearch] = useState(false); + + const globalSearchAvailable = widget.panelTypes === PANEL_TYPES.TABLE; + const menu = useMemo( () => ({ items: updatedMenuList, @@ -201,46 +209,80 @@ function WidgetHeader({ return (
- - {title} - -
-
{threshold}
- {isFetchingResponse && !queryResponse.isError && ( - - )} - {queryResponse.isError && ( - } + placeholder="Search..." + bordered={false} + data-testid="widget-header-search-input" + autoFocus + addonAfter={ + { + e.stopPropagation(); + e.preventDefault(); + setShowGlobalSearch(false); + }} + className="search-header-icons" + /> + } + key={widget.id} + onChange={(e): void => { + setSearchTerm(e.target.value || ''); + }} + /> + ) : ( + <> + - - - )} - - {isWarning && ( - - - - )} - - - -
+ {title} + +
+
{threshold}
+ {isFetchingResponse && !queryResponse.isError && ( + + )} + {queryResponse.isError && ( + + + + )} + + {isWarning && ( + + + + )} + {globalSearchAvailable && ( + setShowGlobalSearch(true)} + data-testid="widget-header-search" + /> + )} + + + +
+ + )}
); } diff --git a/frontend/src/container/GridCardLayout/styles.ts b/frontend/src/container/GridCardLayout/styles.ts index e3f24308de..df2004da52 100644 --- a/frontend/src/container/GridCardLayout/styles.ts +++ b/frontend/src/container/GridCardLayout/styles.ts @@ -33,7 +33,14 @@ export const Card = styled(CardComponent)` } .ant-card-body { - height: calc(100% - 30px); + ${({ $panelType }): StyledCSS => + $panelType === PANEL_TYPES.TABLE + ? css` + height: 100%; + ` + : css` + height: calc(100% - 30px); + `} padding: 0; } `; diff --git a/frontend/src/container/GridTableComponent/index.tsx b/frontend/src/container/GridTableComponent/index.tsx index 676a745b65..fbd3892c48 100644 --- a/frontend/src/container/GridTableComponent/index.tsx +++ b/frontend/src/container/GridTableComponent/index.tsx @@ -4,7 +4,7 @@ import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig'; import { Events } from 'constants/events'; import { QueryTable } from 'container/QueryTable'; import { RowData } from 'lib/query/createTableColumnsFromQuery'; -import { cloneDeep, get, isEmpty, set } from 'lodash-es'; +import { cloneDeep, get, isEmpty } from 'lodash-es'; import { memo, ReactNode, useCallback, useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { eventEmitter } from 'utils/getEventEmitter'; @@ -38,15 +38,13 @@ function GridTableComponent({ const createDataInCorrectFormat = useCallback( (dataSource: RowData[]): RowData[] => dataSource.map((d) => { - const finalObject = {}; + const finalObject: Record = {}; // we use the order of the columns here to have similar download as the user view + // the [] access for the object is used because the titles can contain dot(.) as well columns.forEach((k) => { - set( - finalObject, - get(k, 'title', '') as string, - get(d, get(k, 'dataIndex', ''), 'n/a'), - ); + finalObject[`${get(k, 'title', '')}`] = + d[`${get(k, 'dataIndex', '')}`] || 'n/a'; }); return finalObject as RowData; }), @@ -86,6 +84,7 @@ function GridTableComponent({ applyColumnUnits, originalDataSource, ]); + useEffect(() => { if (tableProcessedDataRef) { // eslint-disable-next-line no-param-reassign diff --git a/frontend/src/container/GridTableComponent/types.ts b/frontend/src/container/GridTableComponent/types.ts index 6088f9dcb8..883e280b38 100644 --- a/frontend/src/container/GridTableComponent/types.ts +++ b/frontend/src/container/GridTableComponent/types.ts @@ -14,6 +14,7 @@ export type GridTableComponentProps = { columnUnits?: ColumnUnit; tableProcessedDataRef?: React.MutableRefObject; sticky?: TableProps['sticky']; + searchTerm?: string; } & Pick & Omit, 'columns' | 'dataSource'>; diff --git a/frontend/src/container/ListOfDashboard/DashboardList.styles.scss b/frontend/src/container/ListOfDashboard/DashboardList.styles.scss index cf9ec283d2..6a5a148180 100644 --- a/frontend/src/container/ListOfDashboard/DashboardList.styles.scss +++ b/frontend/src/container/ListOfDashboard/DashboardList.styles.scss @@ -64,9 +64,9 @@ .dashboard-icon { display: inline-block; - margin-top: 4px; - margin-right: 4px; line-height: 20px; + height: 14px; + width: 14px; } .dot { @@ -75,6 +75,12 @@ border-radius: 50%; } + .title-link { + display: flex; + align-items: center; + gap: 8px; + } + .title { color: var(--bg-vanilla-100); font-size: var(--font-size-sm); diff --git a/frontend/src/container/ListOfDashboard/DashboardsList.tsx b/frontend/src/container/ListOfDashboard/DashboardsList.tsx index 421c7e31c4..9908374a1b 100644 --- a/frontend/src/container/ListOfDashboard/DashboardsList.tsx +++ b/frontend/src/container/ListOfDashboard/DashboardsList.tsx @@ -91,6 +91,7 @@ function DashboardsList(): JSX.Element { const { data: dashboardListResponse, isLoading: isDashboardListLoading, + isRefetching: isDashboardListRefetching, error: dashboardFetchError, refetch: refetchDashboardList, } = useGetAllDashboard(); @@ -458,17 +459,19 @@ function DashboardsList(): JSX.Element { placement="left" overlayClassName="title-toolip" > - - - dashboard-image + + dashboard-image + {dashboard.name} - - + + @@ -703,7 +706,9 @@ function DashboardsList(): JSX.Element { - {isDashboardListLoading || isFilteringDashboards ? ( + {isDashboardListLoading || + isFilteringDashboards || + isDashboardListRefetching ? (
@@ -902,7 +907,11 @@ function DashboardsList(): JSX.Element { columns={columns} dataSource={data} showSorterTooltip - loading={isDashboardListLoading || isFilteringDashboards} + loading={ + isDashboardListLoading || + isFilteringDashboards || + isDashboardListRefetching + } showHeader={false} pagination={paginationConfig} /> diff --git a/frontend/src/container/LogDetailedView/InfraMetrics/InfraMetrics.styles.scss b/frontend/src/container/LogDetailedView/InfraMetrics/InfraMetrics.styles.scss new file mode 100644 index 0000000000..9e49bcba94 --- /dev/null +++ b/frontend/src/container/LogDetailedView/InfraMetrics/InfraMetrics.styles.scss @@ -0,0 +1,34 @@ +.empty-container { + display: flex; + justify-content: center; + align-items: center; + height: 100%; +} + +.infra-metrics-container { + .views-tabs { + margin-bottom: 1rem; + } +} + +.infra-metrics-card { + margin: 1rem 0; + height: 300px; + padding: 10px; + + .ant-card-body { + padding: 0; + } + + .chart-container { + width: 100%; + height: 100%; + } + + .no-data-container { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } +} diff --git a/frontend/src/container/LogDetailedView/InfraMetrics/InfraMetrics.tsx b/frontend/src/container/LogDetailedView/InfraMetrics/InfraMetrics.tsx new file mode 100644 index 0000000000..78a1d21b16 --- /dev/null +++ b/frontend/src/container/LogDetailedView/InfraMetrics/InfraMetrics.tsx @@ -0,0 +1,94 @@ +import './InfraMetrics.styles.scss'; + +import { Empty, Radio } from 'antd'; +import { RadioChangeEvent } from 'antd/lib'; +import { History, Table } from 'lucide-react'; +import { useState } from 'react'; + +import { VIEW_TYPES } from './constants'; +import NodeMetrics from './NodeMetrics'; +import PodMetrics from './PodMetrics'; + +interface MetricsDataProps { + podName: string; + nodeName: string; + hostName: string; + clusterName: string; + logLineTimestamp: string; +} + +function InfraMetrics({ + podName, + nodeName, + hostName, + clusterName, + logLineTimestamp, +}: MetricsDataProps): JSX.Element { + const [selectedView, setSelectedView] = useState(() => + podName ? VIEW_TYPES.POD : VIEW_TYPES.NODE, + ); + + const handleModeChange = (e: RadioChangeEvent): void => { + setSelectedView(e.target.value); + }; + + if (!podName && !nodeName && !hostName) { + return ( +
+ +
+ ); + } + + return ( +
+ + +
+ + Node + + + {podName && ( + +
+ + Pod +
+
+ )} + + {/* TODO(Rahul): Make a common config driven component for this and other infra metrics components */} + {selectedView === VIEW_TYPES.NODE && ( + + )} + {selectedView === VIEW_TYPES.POD && podName && ( + + )} + + ); +} + +export default InfraMetrics; diff --git a/frontend/src/container/LogDetailedView/InfraMetrics/NodeMetrics.tsx b/frontend/src/container/LogDetailedView/InfraMetrics/NodeMetrics.tsx new file mode 100644 index 0000000000..3c935c8b89 --- /dev/null +++ b/frontend/src/container/LogDetailedView/InfraMetrics/NodeMetrics.tsx @@ -0,0 +1,140 @@ +import { Card, Col, Row, Skeleton, Typography } from 'antd'; +import cx from 'classnames'; +import Uplot from 'components/Uplot'; +import { ENTITY_VERSION_V4 } from 'constants/app'; +import dayjs from 'dayjs'; +import { useIsDarkMode } from 'hooks/useDarkMode'; +import { useResizeObserver } from 'hooks/useDimensions'; +import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults'; +import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions'; +import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData'; +import { useMemo, useRef } from 'react'; +import { useQueries, UseQueryResult } from 'react-query'; +import { SuccessResponse } from 'types/api'; +import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; + +import { + getHostQueryPayload, + getNodeQueryPayload, + hostWidgetInfo, + nodeWidgetInfo, +} from './constants'; + +function NodeMetrics({ + nodeName, + clusterName, + hostName, + logLineTimestamp, +}: { + nodeName: string; + clusterName: string; + hostName: string; + logLineTimestamp: string; +}): JSX.Element { + const { start, end, verticalLineTimestamp } = useMemo(() => { + const logTimestamp = dayjs(logLineTimestamp); + const now = dayjs(); + const startTime = logTimestamp.subtract(3, 'hour'); + + const endTime = logTimestamp.add(3, 'hour').isBefore(now) + ? logTimestamp.add(3, 'hour') + : now; + + return { + start: startTime.unix(), + end: endTime.unix(), + verticalLineTimestamp: logTimestamp.unix(), + }; + }, [logLineTimestamp]); + + const queryPayloads = useMemo(() => { + if (nodeName) { + return getNodeQueryPayload(clusterName, nodeName, start, end); + } + return getHostQueryPayload(hostName, start, end); + }, [nodeName, hostName, clusterName, start, end]); + + const widgetInfo = nodeName ? nodeWidgetInfo : hostWidgetInfo; + const queries = useQueries( + queryPayloads.map((payload) => ({ + queryKey: ['metrics', payload, ENTITY_VERSION_V4, 'NODE'], + queryFn: (): Promise> => + GetMetricQueryRange(payload, ENTITY_VERSION_V4), + enabled: !!payload, + })), + ); + + const isDarkMode = useIsDarkMode(); + const graphRef = useRef(null); + const dimensions = useResizeObserver(graphRef); + + const chartData = useMemo( + () => queries.map(({ data }) => getUPlotChartData(data?.payload)), + [queries], + ); + + const options = useMemo( + () => + queries.map(({ data }, idx) => + getUPlotChartOptions({ + apiResponse: data?.payload, + isDarkMode, + dimensions, + yAxisUnit: widgetInfo[idx].yAxisUnit, + softMax: null, + softMin: null, + minTimeScale: start, + maxTimeScale: end, + verticalLineTimestamp, + }), + ), + [ + queries, + isDarkMode, + dimensions, + widgetInfo, + start, + verticalLineTimestamp, + end, + ], + ); + + const renderCardContent = ( + query: UseQueryResult, unknown>, + idx: number, + ): JSX.Element => { + if (query.isLoading) { + return ; + } + + if (query.error) { + const errorMessage = + (query.error as Error)?.message || 'Something went wrong'; + return
{errorMessage}
; + } + return ( +
+ +
+ ); + }; + return ( + + {queries.map((query, idx) => ( +
+ {widgetInfo[idx].title} + + {renderCardContent(query, idx)} + + + ))} + + ); +} + +export default NodeMetrics; diff --git a/frontend/src/container/LogDetailedView/InfraMetrics/PodMetrics.tsx b/frontend/src/container/LogDetailedView/InfraMetrics/PodMetrics.tsx new file mode 100644 index 0000000000..99391d65e0 --- /dev/null +++ b/frontend/src/container/LogDetailedView/InfraMetrics/PodMetrics.tsx @@ -0,0 +1,121 @@ +import { Card, Col, Row, Skeleton, Typography } from 'antd'; +import cx from 'classnames'; +import Uplot from 'components/Uplot'; +import { ENTITY_VERSION_V4 } from 'constants/app'; +import dayjs from 'dayjs'; +import { useIsDarkMode } from 'hooks/useDarkMode'; +import { useResizeObserver } from 'hooks/useDimensions'; +import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults'; +import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions'; +import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData'; +import { useMemo, useRef } from 'react'; +import { useQueries, UseQueryResult } from 'react-query'; +import { SuccessResponse } from 'types/api'; +import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; + +import { getPodQueryPayload, podWidgetInfo } from './constants'; + +function PodMetrics({ + podName, + clusterName, + logLineTimestamp, +}: { + podName: string; + clusterName: string; + logLineTimestamp: string; +}): JSX.Element { + const { start, end, verticalLineTimestamp } = useMemo(() => { + const logTimestamp = dayjs(logLineTimestamp); + const now = dayjs(); + const startTime = logTimestamp.subtract(3, 'hour'); + + const endTime = logTimestamp.add(3, 'hour').isBefore(now) + ? logTimestamp.add(3, 'hour') + : now; + + return { + start: startTime.unix(), + end: endTime.unix(), + verticalLineTimestamp: logTimestamp.unix(), + }; + }, [logLineTimestamp]); + const queryPayloads = useMemo( + () => getPodQueryPayload(clusterName, podName, start, end), + [clusterName, end, podName, start], + ); + const queries = useQueries( + queryPayloads.map((payload) => ({ + queryKey: ['metrics', payload, ENTITY_VERSION_V4, 'POD'], + queryFn: (): Promise> => + GetMetricQueryRange(payload, ENTITY_VERSION_V4), + enabled: !!payload, + })), + ); + + const isDarkMode = useIsDarkMode(); + const graphRef = useRef(null); + const dimensions = useResizeObserver(graphRef); + + const chartData = useMemo( + () => queries.map(({ data }) => getUPlotChartData(data?.payload)), + [queries], + ); + + const options = useMemo( + () => + queries.map(({ data }, idx) => + getUPlotChartOptions({ + apiResponse: data?.payload, + isDarkMode, + dimensions, + yAxisUnit: podWidgetInfo[idx].yAxisUnit, + softMax: null, + softMin: null, + minTimeScale: start, + maxTimeScale: end, + verticalLineTimestamp, + }), + ), + [queries, isDarkMode, dimensions, start, verticalLineTimestamp, end], + ); + + const renderCardContent = ( + query: UseQueryResult, unknown>, + idx: number, + ): JSX.Element => { + if (query.isLoading) { + return ; + } + + if (query.error) { + const errorMessage = + (query.error as Error)?.message || 'Something went wrong'; + return
{errorMessage}
; + } + return ( +
+ +
+ ); + }; + + return ( + + {queries.map((query, idx) => ( +
+ {podWidgetInfo[idx].title} + + {renderCardContent(query, idx)} + + + ))} + + ); +} + +export default PodMetrics; diff --git a/frontend/src/container/LogDetailedView/InfraMetrics/constants.ts b/frontend/src/container/LogDetailedView/InfraMetrics/constants.ts new file mode 100644 index 0000000000..39130e7f56 --- /dev/null +++ b/frontend/src/container/LogDetailedView/InfraMetrics/constants.ts @@ -0,0 +1,3033 @@ +/* eslint-disable sonarjs/no-duplicate-string */ +import { PANEL_TYPES } from 'constants/queryBuilder'; +import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults'; +import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { EQueryType } from 'types/common/dashboard'; +import { DataSource } from 'types/common/queryBuilder'; + +export const getPodQueryPayload = ( + clusterName: string, + podName: string, + start: number, + end: number, +): GetQueryResultsProps[] => [ + { + selectedTime: 'GLOBAL_TIME', + graphType: PANEL_TYPES.TIME_SERIES, + query: { + builder: { + queryData: [ + { + aggregateAttribute: { + dataType: DataTypes.Float64, + id: 'container_cpu_utilization--float64--Gauge--true', + isColumn: true, + isJSON: false, + key: 'container_cpu_utilization', + type: 'Gauge', + }, + aggregateOperator: 'avg', + dataSource: DataSource.METRICS, + disabled: false, + expression: 'A', + filters: { + items: [ + { + id: '6e050953', + key: { + dataType: DataTypes.String, + id: 'k8s_cluster_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'k8s_cluster_name', + type: 'tag', + }, + op: '=', + value: clusterName, + }, + { + id: '60fe5e62', + key: { + dataType: DataTypes.String, + id: 'k8s_pod_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'k8s_pod_name', + type: 'tag', + }, + op: '=', + value: podName, + }, + ], + op: 'AND', + }, + functions: [], + groupBy: [ + { + dataType: DataTypes.String, + id: 'k8s_pod_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'k8s_pod_name', + type: 'tag', + }, + ], + having: [], + legend: '{{k8s_pod_name}}', + limit: null, + orderBy: [], + queryName: 'A', + reduceTo: 'avg', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'avg', + }, + ], + queryFormulas: [], + }, + clickhouse_sql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + id: '9b92756a-b445-45f8-90f4-d26f3ef28f8f', + promql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + queryType: EQueryType.QUERY_BUILDER, + }, + variables: {}, + formatForWeb: false, + start, + end, + }, + { + selectedTime: 'GLOBAL_TIME', + graphType: PANEL_TYPES.TIME_SERIES, + query: { + builder: { + queryData: [ + { + aggregateAttribute: { + dataType: DataTypes.Float64, + id: 'container_memory_usage--float64--Gauge--true', + isColumn: true, + isJSON: false, + key: 'container_memory_usage', + type: 'Gauge', + }, + aggregateOperator: 'avg', + dataSource: DataSource.METRICS, + disabled: false, + expression: 'A', + filters: { + items: [ + { + id: 'a4250695', + key: { + dataType: DataTypes.String, + id: 'k8s_cluster_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'k8s_cluster_name', + type: 'tag', + }, + op: '=', + value: clusterName, + }, + { + id: '3b2bc32b', + key: { + dataType: DataTypes.String, + id: 'k8s_pod_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'k8s_pod_name', + type: 'tag', + }, + op: '=', + value: podName, + }, + ], + op: 'AND', + }, + functions: [], + groupBy: [ + { + dataType: DataTypes.String, + id: 'k8s_pod_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'k8s_pod_name', + type: 'tag', + }, + ], + having: [], + legend: '{{k8s_pod_name}}', + limit: null, + orderBy: [], + queryName: 'A', + reduceTo: 'avg', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'avg', + }, + ], + queryFormulas: [], + }, + clickhouse_sql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + id: 'a22c1e03-4876-4b3e-9a96-a3c3a28f9c0f', + promql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + queryType: EQueryType.QUERY_BUILDER, + }, + variables: {}, + formatForWeb: false, + start, + end, + }, + { + selectedTime: 'GLOBAL_TIME', + graphType: PANEL_TYPES.TIME_SERIES, + query: { + builder: { + queryData: [ + { + aggregateAttribute: { + dataType: DataTypes.Float64, + id: 'container_cpu_utilization--float64--Gauge--true', + isColumn: true, + isJSON: false, + key: 'container_cpu_utilization', + type: 'Gauge', + }, + aggregateOperator: 'avg', + dataSource: DataSource.METRICS, + disabled: true, + expression: 'A', + filters: { + items: [ + { + id: '8426b52f', + key: { + dataType: DataTypes.String, + id: 'k8s_cluster_name--string--tag--false', + isColumn: false, + key: 'k8s_cluster_name', + type: 'tag', + }, + op: '=', + value: clusterName, + }, + { + id: '2f67240c', + key: { + dataType: DataTypes.String, + id: 'k8s_pod_name--string--tag--false', + isColumn: false, + key: 'k8s_pod_name', + type: 'tag', + }, + op: '=', + value: podName, + }, + ], + op: 'AND', + }, + functions: [], + groupBy: [ + { + dataType: DataTypes.String, + id: 'k8s_pod_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'k8s_pod_name', + type: 'tag', + }, + ], + having: [], + legend: '', + limit: null, + orderBy: [], + queryName: 'A', + reduceTo: 'sum', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'avg', + }, + { + aggregateAttribute: { + dataType: DataTypes.Float64, + id: 'k8s_container_cpu_request--float64--Gauge--true', + isColumn: true, + isJSON: false, + key: 'k8s_container_cpu_request', + type: 'Gauge', + }, + aggregateOperator: 'latest', + dataSource: DataSource.METRICS, + disabled: true, + expression: 'B', + filters: { + items: [ + { + id: '8c4667e1', + key: { + dataType: DataTypes.String, + id: 'k8s_cluster_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'k8s_cluster_name', + type: 'tag', + }, + op: '=', + value: clusterName, + }, + { + id: 'b16e7306', + key: { + dataType: DataTypes.String, + id: 'k8s_pod_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'k8s_pod_name', + type: 'tag', + }, + op: 'in', + value: podName, + }, + ], + op: 'AND', + }, + functions: [], + groupBy: [ + { + dataType: DataTypes.String, + id: 'k8s_pod_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'k8s_pod_name', + type: 'tag', + }, + ], + having: [], + legend: '', + limit: null, + orderBy: [], + queryName: 'B', + reduceTo: 'avg', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'latest', + }, + ], + queryFormulas: [ + { + disabled: false, + expression: 'A*100/B', + legend: '{{k8s_pod_name}}', + queryName: 'F1', + }, + ], + }, + clickhouse_sql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + id: '7bb3a6f5-d1c6-4f2e-9cc9-7dcc46db398f', + promql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + queryType: EQueryType.QUERY_BUILDER, + }, + variables: {}, + formatForWeb: false, + start, + end, + }, + { + selectedTime: 'GLOBAL_TIME', + graphType: PANEL_TYPES.TIME_SERIES, + query: { + builder: { + queryData: [ + { + aggregateAttribute: { + dataType: DataTypes.Float64, + id: 'container_cpu_utilization--float64--Gauge--true', + isColumn: true, + isJSON: false, + key: 'container_cpu_utilization', + type: 'Gauge', + }, + aggregateOperator: 'avg', + dataSource: DataSource.METRICS, + disabled: true, + expression: 'A', + filters: { + items: [ + { + id: '0a862947', + key: { + dataType: DataTypes.String, + id: 'k8s_cluster_name--string--tag--false', + isColumn: false, + key: 'k8s_cluster_name', + type: 'tag', + }, + op: '=', + value: clusterName, + }, + { + id: 'cd13fbf0', + key: { + dataType: DataTypes.String, + id: 'k8s_pod_name--string--tag--false', + isColumn: false, + key: 'k8s_pod_name', + type: 'tag', + }, + op: '=', + value: podName, + }, + ], + op: 'AND', + }, + functions: [], + groupBy: [ + { + dataType: DataTypes.String, + id: 'k8s_pod_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'k8s_pod_name', + type: 'tag', + }, + ], + having: [], + legend: 'usage - {{k8s_pod_name}}', + limit: null, + orderBy: [], + queryName: 'A', + reduceTo: 'sum', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'avg', + }, + { + aggregateAttribute: { + dataType: DataTypes.Float64, + id: 'k8s_container_cpu_limit--float64--Gauge--true', + isColumn: true, + isJSON: false, + key: 'k8s_container_cpu_limit', + type: 'Gauge', + }, + aggregateOperator: 'latest', + dataSource: DataSource.METRICS, + disabled: true, + expression: 'B', + filters: { + items: [ + { + id: 'bfb8acf7', + key: { + dataType: DataTypes.String, + id: 'k8s_cluster_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'k8s_cluster_name', + type: 'tag', + }, + op: '=', + value: clusterName, + }, + { + id: 'e09ba819', + key: { + dataType: DataTypes.String, + id: 'k8s_pod_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'k8s_pod_name', + type: 'tag', + }, + op: 'in', + value: podName, + }, + ], + op: 'AND', + }, + functions: [], + groupBy: [ + { + dataType: DataTypes.String, + id: 'k8s_pod_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'k8s_pod_name', + type: 'tag', + }, + ], + having: [], + legend: 'limit - {{k8s_pod_name}}', + limit: null, + orderBy: [], + queryName: 'B', + reduceTo: 'avg', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'latest', + }, + ], + queryFormulas: [ + { + disabled: false, + expression: 'A*100/B', + legend: '{{k8s_pod_name}}', + queryName: 'F1', + }, + ], + }, + clickhouse_sql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + id: '6d5ccd81-0ea1-4fb9-a66b-7f0fe2f15165', + promql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + queryType: EQueryType.QUERY_BUILDER, + }, + variables: {}, + formatForWeb: false, + start, + end, + }, + { + selectedTime: 'GLOBAL_TIME', + graphType: PANEL_TYPES.TIME_SERIES, + query: { + builder: { + queryData: [ + { + aggregateAttribute: { + dataType: DataTypes.Float64, + id: 'container_memory_usage--float64--Gauge--true', + isColumn: true, + isJSON: false, + key: 'container_memory_usage', + type: 'Gauge', + }, + aggregateOperator: 'avg', + dataSource: DataSource.METRICS, + disabled: true, + expression: 'A', + filters: { + items: [ + { + id: 'ea3df3e7', + key: { + dataType: DataTypes.String, + id: 'k8s_cluster_name--string--tag--false', + isColumn: false, + key: 'k8s_cluster_name', + type: 'tag', + }, + op: '=', + value: clusterName, + }, + { + id: '39b21fe0', + key: { + dataType: DataTypes.String, + id: 'k8s_pod_name--string--tag--false', + isColumn: false, + key: 'k8s_pod_name', + type: 'tag', + }, + op: 'in', + value: podName, + }, + ], + op: 'AND', + }, + functions: [], + groupBy: [ + { + dataType: DataTypes.String, + id: 'k8s_pod_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'k8s_pod_name', + type: 'tag', + }, + ], + having: [], + legend: '', + limit: null, + orderBy: [], + queryName: 'A', + reduceTo: 'sum', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'avg', + }, + { + aggregateAttribute: { + dataType: DataTypes.Float64, + id: 'k8s_container_memory_request--float64--Gauge--true', + isColumn: true, + isJSON: false, + key: 'k8s_container_memory_request', + type: 'Gauge', + }, + aggregateOperator: 'latest', + dataSource: DataSource.METRICS, + disabled: true, + expression: 'B', + filters: { + items: [ + { + id: '7401a4b9', + key: { + dataType: DataTypes.String, + id: 'k8s_cluster_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'k8s_cluster_name', + type: 'tag', + }, + op: '=', + value: clusterName, + }, + { + id: '7cdad1cb', + key: { + dataType: DataTypes.String, + id: 'k8s_pod_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'k8s_pod_name', + type: 'tag', + }, + op: '=', + value: podName, + }, + ], + op: 'AND', + }, + functions: [], + groupBy: [ + { + dataType: DataTypes.String, + id: 'k8s_pod_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'k8s_pod_name', + type: 'tag', + }, + ], + having: [], + legend: '', + limit: null, + orderBy: [], + queryName: 'B', + reduceTo: 'avg', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'latest', + }, + ], + queryFormulas: [ + { + disabled: false, + expression: 'A*100/B', + legend: '{{k8s_pod_name}}', + queryName: 'F1', + }, + ], + }, + clickhouse_sql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + id: '4d03a0ff-4fa5-4b19-b397-97f80ba9e0ac', + promql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + queryType: EQueryType.QUERY_BUILDER, + }, + + variables: {}, + formatForWeb: false, + start, + end, + }, + { + selectedTime: 'GLOBAL_TIME', + graphType: PANEL_TYPES.TIME_SERIES, + query: { + builder: { + queryData: [ + { + aggregateAttribute: { + dataType: DataTypes.Float64, + id: 'container_memory_usage--float64--Gauge--true', + isColumn: true, + isJSON: false, + key: 'container_memory_usage', + type: 'Gauge', + }, + aggregateOperator: 'avg', + dataSource: DataSource.METRICS, + disabled: true, + expression: 'A', + filters: { + items: [ + { + id: 'f2a3175c', + key: { + dataType: DataTypes.String, + id: 'k8s_cluster_name--string--tag--false', + isColumn: false, + key: 'k8s_cluster_name', + type: 'tag', + }, + op: '=', + value: clusterName, + }, + { + id: 'fc17ff21', + key: { + dataType: DataTypes.String, + id: 'k8s_pod_name--string--tag--false', + isColumn: false, + key: 'k8s_pod_name', + type: 'tag', + }, + op: '=', + value: podName, + }, + ], + op: 'AND', + }, + functions: [], + groupBy: [ + { + dataType: DataTypes.String, + id: 'k8s_pod_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'k8s_pod_name', + type: 'tag', + }, + ], + having: [], + legend: '', + limit: null, + orderBy: [], + queryName: 'A', + reduceTo: 'sum', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'avg', + }, + { + aggregateAttribute: { + dataType: DataTypes.Float64, + id: 'k8s_container_memory_limit--float64--Gauge--true', + isColumn: true, + isJSON: false, + key: 'k8s_container_memory_limit', + type: 'Gauge', + }, + aggregateOperator: 'latest', + dataSource: DataSource.METRICS, + disabled: true, + expression: 'B', + filters: { + items: [ + { + id: '175e96b7', + key: { + dataType: DataTypes.String, + id: 'k8s_cluster_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'k8s_cluster_name', + type: 'tag', + }, + op: '=', + value: clusterName, + }, + { + id: '1d9fbe48', + key: { + dataType: DataTypes.String, + id: 'k8s_pod_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'k8s_pod_name', + type: 'tag', + }, + op: 'in', + value: podName, + }, + ], + op: 'AND', + }, + functions: [], + groupBy: [ + { + dataType: DataTypes.String, + id: 'k8s_pod_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'k8s_pod_name', + type: 'tag', + }, + ], + having: [], + legend: '', + limit: null, + orderBy: [], + queryName: 'B', + reduceTo: 'avg', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'latest', + }, + ], + queryFormulas: [ + { + disabled: false, + expression: 'A*100/B', + legend: '{{k8s_pod_name}}', + queryName: 'F1', + }, + ], + }, + clickhouse_sql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + id: 'ad491f19-0f83-4dd4-bb8f-bec295c18d1b', + promql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + queryType: EQueryType.QUERY_BUILDER, + }, + variables: {}, + formatForWeb: false, + start, + end, + }, + { + selectedTime: 'GLOBAL_TIME', + graphType: PANEL_TYPES.TIME_SERIES, + query: { + builder: { + queryData: [ + { + aggregateAttribute: { + dataType: DataTypes.Float64, + id: 'k8s_pod_filesystem_available--float64--Gauge--true', + isColumn: true, + isJSON: false, + key: 'k8s_pod_filesystem_available', + type: 'Gauge', + }, + aggregateOperator: 'avg', + dataSource: DataSource.METRICS, + disabled: true, + expression: 'A', + filters: { + items: [ + { + id: '877385bf', + key: { + dataType: DataTypes.String, + id: 'k8s_cluster_name--string--tag--false', + isColumn: false, + key: 'k8s_cluster_name', + type: 'tag', + }, + op: '=', + value: clusterName, + }, + { + id: '877385cd', + key: { + dataType: DataTypes.String, + id: 'k8s_pod_name--string--tag--false', + isColumn: false, + key: 'k8s_pod_name', + type: 'tag', + }, + op: '=', + value: podName, + }, + ], + op: 'AND', + }, + functions: [], + groupBy: [ + { + dataType: DataTypes.String, + id: 'k8s_pod_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'k8s_pod_name', + type: 'tag', + }, + ], + having: [], + legend: '', + limit: null, + orderBy: [], + queryName: 'A', + reduceTo: 'sum', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'avg', + }, + { + aggregateAttribute: { + dataType: DataTypes.Float64, + id: 'k8s_pod_filesystem_capacity--float64--Gauge--true', + isColumn: true, + isJSON: false, + key: 'k8s_pod_filesystem_capacity', + type: 'Gauge', + }, + aggregateOperator: 'avg', + dataSource: DataSource.METRICS, + disabled: true, + expression: 'B', + filters: { + items: [ + { + id: '877385bf', + key: { + dataType: DataTypes.String, + id: 'k8s_cluster_name--string--tag--false', + isColumn: false, + key: 'k8s_cluster_name', + type: 'tag', + }, + op: '=', + value: clusterName, + }, + { + id: '877385cd', + key: { + dataType: DataTypes.String, + id: 'k8s_pod_name--string--tag--false', + isColumn: false, + key: 'k8s_pod_name', + type: 'tag', + }, + op: '=', + value: podName, + }, + ], + op: 'AND', + }, + functions: [], + groupBy: [ + { + dataType: DataTypes.String, + id: 'k8s_pod_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'k8s_pod_name', + type: 'tag', + }, + ], + having: [], + legend: '', + limit: null, + orderBy: [], + queryName: 'B', + reduceTo: 'sum', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'avg', + }, + ], + queryFormulas: [ + { + disabled: false, + expression: '(B-A)/B', + legend: '{{k8s_pod_name}}', + queryName: 'F1', + }, + ], + }, + clickhouse_sql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + id: '16908d4e-1565-4847-8d87-01ebb8fc494a', + promql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + queryType: EQueryType.QUERY_BUILDER, + }, + variables: {}, + fillGaps: false, + formatForWeb: false, + start, + end, + }, + { + selectedTime: 'GLOBAL_TIME', + graphType: PANEL_TYPES.TIME_SERIES, + query: { + builder: { + queryData: [ + { + aggregateAttribute: { + dataType: DataTypes.Float64, + id: 'k8s_pod_network_io--float64--Sum--true', + isColumn: true, + isJSON: false, + key: 'k8s_pod_network_io', + type: 'Sum', + }, + aggregateOperator: 'rate', + dataSource: DataSource.METRICS, + disabled: false, + expression: 'A', + filters: { + items: [ + { + id: '877385bf', + key: { + dataType: DataTypes.String, + id: 'k8s_cluster_name--string--tag--false', + isColumn: false, + key: 'k8s_cluster_name', + type: 'tag', + }, + op: '=', + value: clusterName, + }, + { + id: '9613b4da', + key: { + dataType: DataTypes.String, + id: 'k8s_pod_name--string--tag--false', + isColumn: false, + key: 'k8s_pod_name', + type: 'tag', + }, + op: '=', + value: podName, + }, + ], + op: 'AND', + }, + functions: [], + groupBy: [ + { + dataType: DataTypes.String, + id: 'k8s_pod_name--string--tag--false', + isColumn: false, + key: 'k8s_pod_name', + type: 'tag', + }, + ], + having: [], + legend: '{{k8s_pod_name}}', + limit: null, + orderBy: [], + queryName: 'A', + reduceTo: 'sum', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'rate', + }, + ], + queryFormulas: [], + }, + clickhouse_sql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + id: '4b255d6d-4cde-474d-8866-f4418583c18b', + promql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + queryType: EQueryType.QUERY_BUILDER, + }, + variables: {}, + formatForWeb: false, + start, + end, + }, +]; + +export const getNodeQueryPayload = ( + clusterName: string, + nodeName: string, + start: number, + end: number, +): GetQueryResultsProps[] => [ + { + selectedTime: 'GLOBAL_TIME', + graphType: PANEL_TYPES.TIME_SERIES, + query: { + builder: { + queryData: [ + { + aggregateAttribute: { + dataType: DataTypes.Float64, + id: 'k8s_node_cpu_time--float64--Sum--true', + isColumn: true, + isJSON: false, + key: 'k8s_node_cpu_time', + type: 'Sum', + }, + aggregateOperator: 'rate', + dataSource: DataSource.METRICS, + disabled: true, + expression: 'A', + filters: { + items: [ + { + id: '91223422', + key: { + dataType: DataTypes.String, + id: 'k8s_cluster_name--string--tag--false', + isColumn: false, + key: 'k8s_cluster_name', + type: 'tag', + }, + op: '=', + value: clusterName, + }, + { + id: '91223422', + key: { + dataType: DataTypes.String, + id: 'k8s_node_name--string--tag--false', + isColumn: false, + key: 'k8s_node_name', + type: 'tag', + }, + op: 'in', + value: nodeName, + }, + ], + op: 'AND', + }, + functions: [], + groupBy: [ + { + dataType: DataTypes.String, + id: 'k8s_node_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'k8s_node_name', + type: 'tag', + }, + ], + having: [], + legend: '{{k8s_node_name}}', + limit: null, + orderBy: [], + queryName: 'A', + reduceTo: 'sum', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'rate', + }, + { + aggregateAttribute: { + dataType: DataTypes.Float64, + id: 'k8s_node_allocatable_cpu--float64--Gauge--true', + isColumn: true, + isJSON: false, + key: 'k8s_node_allocatable_cpu', + type: 'Gauge', + }, + aggregateOperator: 'avg', + dataSource: DataSource.METRICS, + disabled: true, + expression: 'B', + filters: { + items: [ + { + id: '9700f1d4', + key: { + dataType: DataTypes.String, + id: 'k8s_node_name--string--tag--false', + isColumn: false, + key: 'k8s_node_name', + type: 'tag', + }, + op: 'in', + value: nodeName, + }, + ], + op: 'AND', + }, + functions: [], + groupBy: [ + { + dataType: DataTypes.String, + id: 'k8s_node_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'k8s_node_name', + type: 'tag', + }, + ], + having: [], + legend: '{{k8s_node_name}}', + limit: null, + orderBy: [], + queryName: 'B', + reduceTo: 'sum', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'avg', + }, + ], + queryFormulas: [ + { + disabled: false, + expression: 'A/B', + legend: '{{k8s_node_name}}', + queryName: 'F1', + }, + ], + }, + clickhouse_sql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + id: '259295b5-774d-4b2e-8a4f-e5dd63e6c38d', + promql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + queryType: EQueryType.QUERY_BUILDER, + }, + variables: {}, + fillGaps: false, + formatForWeb: false, + start, + end, + }, + { + selectedTime: 'GLOBAL_TIME', + graphType: PANEL_TYPES.TIME_SERIES, + query: { + builder: { + queryData: [ + { + aggregateAttribute: { + dataType: DataTypes.Float64, + id: 'k8s_node_memory_working_set--float64--Gauge--true', + isColumn: true, + isJSON: false, + key: 'k8s_node_memory_working_set', + type: 'Gauge', + }, + aggregateOperator: 'avg', + dataSource: DataSource.METRICS, + disabled: true, + expression: 'A', + filters: { + items: [ + { + id: 'a9f58cf3', + key: { + dataType: DataTypes.String, + id: 'k8s_cluster_name--string--tag--false', + isColumn: false, + key: 'k8s_cluster_name', + type: 'tag', + }, + op: '=', + value: clusterName, + }, + { + id: '8430c9a0', + key: { + dataType: DataTypes.String, + id: 'k8s_node_name--string--tag--false', + isColumn: false, + key: 'k8s_node_name', + type: 'tag', + }, + op: 'in', + value: nodeName, + }, + ], + op: 'AND', + }, + functions: [], + groupBy: [ + { + dataType: DataTypes.String, + id: 'k8s_node_name--string--tag--false', + isColumn: false, + key: 'k8s_node_name', + type: 'tag', + }, + ], + having: [], + legend: '', + limit: null, + orderBy: [], + queryName: 'A', + reduceTo: 'sum', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'avg', + }, + { + aggregateAttribute: { + dataType: DataTypes.Float64, + id: 'k8s_node_allocatable_memory--float64--Gauge--true', + isColumn: true, + isJSON: false, + key: 'k8s_node_allocatable_memory', + type: 'Gauge', + }, + aggregateOperator: 'avg', + dataSource: DataSource.METRICS, + disabled: true, + expression: 'B', + filters: { + items: [ + { + id: 'cb274856', + key: { + dataType: DataTypes.String, + id: 'k8s_node_name--string--tag--false', + isColumn: false, + key: 'k8s_node_name', + type: 'tag', + }, + op: 'in', + value: nodeName, + }, + ], + op: 'AND', + }, + functions: [], + groupBy: [ + { + dataType: DataTypes.String, + id: 'k8s_node_name--string--tag--false', + isColumn: false, + key: 'k8s_node_name', + type: 'tag', + }, + ], + having: [], + legend: '', + limit: null, + orderBy: [], + queryName: 'B', + reduceTo: 'sum', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'avg', + }, + ], + queryFormulas: [ + { + disabled: false, + expression: 'A/B', + legend: '{{k8s_node_name}}', + queryName: 'F1', + }, + ], + }, + clickhouse_sql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + id: '486af4da-2a1a-4b8f-992c-eba098d3a6f9', + promql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + queryType: EQueryType.QUERY_BUILDER, + }, + variables: {}, + fillGaps: false, + formatForWeb: false, + start, + end, + }, + { + selectedTime: 'GLOBAL_TIME', + graphType: PANEL_TYPES.TIME_SERIES, + query: { + builder: { + queryData: [ + { + aggregateAttribute: { + dataType: DataTypes.Float64, + id: 'k8s_node_network_io--float64--Sum--true', + isColumn: true, + isJSON: false, + key: 'k8s_node_network_io', + type: 'Sum', + }, + aggregateOperator: 'rate', + dataSource: DataSource.METRICS, + disabled: false, + expression: 'A', + filters: { + items: [ + { + id: '91223422', + key: { + dataType: DataTypes.String, + id: 'k8s_cluster_name--string--tag--false', + isColumn: false, + key: 'k8s_cluster_name', + type: 'tag', + }, + op: '=', + value: clusterName, + }, + { + id: '66308505', + key: { + dataType: DataTypes.String, + id: 'k8s_node_name--string--tag--false', + isColumn: false, + key: 'k8s_node_name', + type: 'tag', + }, + op: 'in', + value: nodeName, + }, + ], + op: 'AND', + }, + functions: [], + groupBy: [ + { + dataType: DataTypes.String, + id: 'interface--string--tag--false', + isColumn: false, + key: 'interface', + type: 'tag', + }, + { + dataType: DataTypes.String, + id: 'direction--string--tag--false', + isColumn: false, + key: 'direction', + type: 'tag', + }, + { + dataType: DataTypes.String, + id: 'k8s_node_name--string--tag--false', + isColumn: false, + key: 'k8s_node_name', + type: 'tag', + }, + ], + having: [], + legend: '{{k8s_node_name}}-{{interface}}-{{direction}}', + limit: null, + orderBy: [], + queryName: 'A', + reduceTo: 'sum', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'rate', + }, + ], + queryFormulas: [], + }, + clickhouse_sql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + id: 'b56143c0-7d2f-4425-97c5-65ad6fc87366', + promql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + queryType: EQueryType.QUERY_BUILDER, + }, + variables: {}, + formatForWeb: false, + start, + end, + }, + { + selectedTime: 'GLOBAL_TIME', + graphType: PANEL_TYPES.TIME_SERIES, + query: { + builder: { + queryData: [ + { + aggregateAttribute: { + dataType: DataTypes.Float64, + id: 'k8s_node_filesystem_available--float64--Gauge--true', + isColumn: true, + isJSON: false, + key: 'k8s_node_filesystem_available', + type: 'Gauge', + }, + aggregateOperator: 'avg', + dataSource: DataSource.METRICS, + disabled: true, + expression: 'A', + filters: { + items: [ + { + id: '91223422', + key: { + dataType: DataTypes.String, + id: 'k8s_cluster_name--string--tag--false', + isColumn: false, + key: 'k8s_cluster_name', + type: 'tag', + }, + op: '=', + value: clusterName, + }, + { + id: 'a5dffef6', + key: { + dataType: DataTypes.String, + id: 'k8s_node_name--string--tag--false', + isColumn: false, + key: 'k8s_node_name', + type: 'tag', + }, + op: 'in', + value: nodeName, + }, + ], + op: 'AND', + }, + functions: [], + groupBy: [ + { + dataType: DataTypes.String, + id: 'k8s_node_name--string--tag--false', + isColumn: false, + key: 'k8s_node_name', + type: 'tag', + }, + ], + having: [], + legend: '', + limit: null, + orderBy: [], + queryName: 'A', + reduceTo: 'sum', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'avg', + }, + { + aggregateAttribute: { + dataType: DataTypes.Float64, + id: 'k8s_node_filesystem_capacity--float64--Gauge--true', + isColumn: true, + isJSON: false, + key: 'k8s_node_filesystem_capacity', + type: 'Gauge', + }, + aggregateOperator: 'avg', + dataSource: DataSource.METRICS, + disabled: true, + expression: 'B', + filters: { + items: [ + { + id: '91223422', + key: { + dataType: DataTypes.String, + id: 'k8s_cluster_name--string--tag--false', + isColumn: false, + key: 'k8s_cluster_name', + type: 'tag', + }, + op: '=', + value: clusterName, + }, + { + id: 'c79d5a16', + key: { + dataType: DataTypes.String, + id: 'k8s_node_name--string--tag--false', + isColumn: false, + key: 'k8s_node_name', + type: 'tag', + }, + op: 'in', + value: nodeName, + }, + ], + op: 'AND', + }, + functions: [], + groupBy: [ + { + dataType: DataTypes.String, + id: 'k8s_node_name--string--tag--false', + isColumn: false, + key: 'k8s_node_name', + type: 'tag', + }, + ], + having: [], + legend: '', + limit: null, + orderBy: [], + queryName: 'B', + reduceTo: 'sum', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'avg', + }, + ], + queryFormulas: [ + { + disabled: false, + expression: '(B-A)/B', + legend: '{{k8s_node_name}}', + queryName: 'F1', + }, + ], + }, + clickhouse_sql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + id: '57eeac15-615c-4a71-9c61-8e0c0c76b045', + promql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + queryType: EQueryType.QUERY_BUILDER, + }, + variables: {}, + formatForWeb: false, + start, + end, + }, +]; + +export const getHostQueryPayload = ( + hostName: string, + start: number, + end: number, +): GetQueryResultsProps[] => [ + { + selectedTime: 'GLOBAL_TIME', + graphType: PANEL_TYPES.TIME_SERIES, + query: { + builder: { + queryData: [ + { + aggregateAttribute: { + dataType: DataTypes.Float64, + id: 'system_cpu_time--float64--Sum--true', + isColumn: true, + isJSON: false, + key: 'system_cpu_time', + type: 'Sum', + }, + aggregateOperator: 'rate', + dataSource: DataSource.METRICS, + disabled: true, + expression: 'A', + filters: { + items: [ + { + id: 'ad316791', + key: { + dataType: DataTypes.String, + id: 'host_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'host_name', + type: 'tag', + }, + op: '=', + value: hostName, + }, + ], + op: 'AND', + }, + functions: [], + groupBy: [ + { + dataType: DataTypes.String, + id: 'state--string--tag--false', + isColumn: false, + isJSON: false, + key: 'state', + type: 'tag', + }, + ], + having: [], + legend: '{{state}}', + limit: null, + orderBy: [], + queryName: 'A', + reduceTo: 'avg', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'rate', + }, + { + aggregateAttribute: { + dataType: DataTypes.Float64, + id: 'system_cpu_time--float64--Sum--true', + isColumn: true, + isJSON: false, + key: 'system_cpu_time', + type: 'Sum', + }, + aggregateOperator: 'rate', + dataSource: DataSource.METRICS, + disabled: true, + expression: 'B', + filters: { + items: [ + { + id: '6baf116b', + key: { + dataType: DataTypes.String, + id: 'host_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'host_name', + type: 'tag', + }, + op: '=', + value: hostName, + }, + ], + op: 'AND', + }, + functions: [], + groupBy: [], + having: [], + legend: '{{state}}', + limit: null, + orderBy: [], + queryName: 'B', + reduceTo: 'avg', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'rate', + }, + ], + queryFormulas: [ + { + disabled: false, + expression: 'A/B', + legend: '{{state}}', + queryName: 'F1', + }, + ], + }, + clickhouse_sql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + id: '315b15fa-ff0c-442f-89f8-2bf4fb1af2f2', + promql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + queryType: EQueryType.QUERY_BUILDER, + }, + variables: {}, + formatForWeb: false, + start, + end, + }, + { + selectedTime: 'GLOBAL_TIME', + graphType: PANEL_TYPES.TIME_SERIES, + query: { + builder: { + queryData: [ + { + aggregateAttribute: { + dataType: DataTypes.Float64, + id: 'system_memory_usage--float64--Gauge--true', + isColumn: true, + isJSON: false, + key: 'system_memory_usage', + type: 'Gauge', + }, + aggregateOperator: 'avg', + dataSource: DataSource.METRICS, + disabled: false, + expression: 'A', + filters: { + items: [ + { + id: '8026009e', + key: { + dataType: DataTypes.String, + id: 'host_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'host_name', + type: 'tag', + }, + op: '=', + value: hostName, + }, + ], + op: 'AND', + }, + functions: [], + groupBy: [ + { + dataType: DataTypes.String, + id: 'state--string--tag--false', + isColumn: false, + isJSON: false, + key: 'state', + type: 'tag', + }, + ], + having: [], + legend: '{{state}}', + limit: null, + orderBy: [], + queryName: 'A', + reduceTo: 'avg', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'avg', + }, + ], + queryFormulas: [], + }, + clickhouse_sql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + id: '40218bfb-a9b7-4974-aead-5bf666e139bf', + promql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + queryType: EQueryType.QUERY_BUILDER, + }, + variables: {}, + formatForWeb: false, + start, + end, + }, + { + selectedTime: 'GLOBAL_TIME', + graphType: PANEL_TYPES.TIME_SERIES, + query: { + builder: { + queryData: [ + { + aggregateAttribute: { + dataType: DataTypes.Float64, + id: 'system_cpu_load_average_1m--float64--Gauge--true', + isColumn: true, + isJSON: false, + key: 'system_cpu_load_average_1m', + type: 'Gauge', + }, + aggregateOperator: 'avg', + dataSource: DataSource.METRICS, + disabled: false, + expression: 'A', + filters: { + items: [ + { + id: '4167fbb1', + key: { + dataType: DataTypes.String, + id: 'host_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'host_name', + type: 'tag', + }, + op: '=', + value: hostName, + }, + ], + op: 'AND', + }, + functions: [], + groupBy: [], + having: [], + legend: '1m', + limit: 30, + orderBy: [], + queryName: 'A', + reduceTo: 'avg', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'avg', + }, + { + aggregateAttribute: { + dataType: DataTypes.Float64, + id: 'system_cpu_load_average_5m--float64--Gauge--true', + isColumn: true, + isJSON: false, + key: 'system_cpu_load_average_5m', + type: 'Gauge', + }, + aggregateOperator: 'avg', + dataSource: DataSource.METRICS, + disabled: false, + expression: 'B', + filters: { + items: [ + { + id: '0c2cfeca', + key: { + dataType: DataTypes.String, + id: 'host_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'host_name', + type: 'tag', + }, + op: '=', + value: hostName, + }, + ], + op: 'AND', + }, + functions: [], + groupBy: [], + having: [], + legend: '5m', + limit: 30, + orderBy: [], + queryName: 'B', + reduceTo: 'avg', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'avg', + }, + { + aggregateAttribute: { + dataType: DataTypes.Float64, + id: 'system_cpu_load_average_15m--float64--Gauge--true', + isColumn: true, + isJSON: false, + key: 'system_cpu_load_average_15m', + type: 'Gauge', + }, + aggregateOperator: 'avg', + dataSource: DataSource.METRICS, + disabled: false, + expression: 'C', + filters: { + items: [ + { + id: '28693375', + key: { + dataType: DataTypes.String, + id: 'host_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'host_name', + type: 'tag', + }, + op: '=', + value: hostName, + }, + ], + op: 'AND', + }, + functions: [], + groupBy: [], + having: [], + legend: '15m', + limit: 30, + orderBy: [], + queryName: 'C', + reduceTo: 'avg', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'avg', + }, + ], + queryFormulas: [], + }, + clickhouse_sql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + id: '8e6485ea-7018-43b0-ab27-b210f77b59ad', + promql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + queryType: EQueryType.QUERY_BUILDER, + }, + variables: {}, + formatForWeb: false, + start, + end, + }, + { + selectedTime: 'GLOBAL_TIME', + graphType: PANEL_TYPES.TIME_SERIES, + query: { + builder: { + queryData: [ + { + aggregateAttribute: { + dataType: DataTypes.Float64, + id: 'system_network_io--float64--Sum--true', + isColumn: true, + isJSON: false, + key: 'system_network_io', + type: 'Sum', + }, + aggregateOperator: 'rate', + dataSource: DataSource.METRICS, + disabled: false, + expression: 'A', + filters: { + items: [ + { + id: '3a03bc80', + key: { + dataType: DataTypes.String, + id: 'host_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'host_name', + type: 'tag', + }, + op: '=', + value: hostName, + }, + ], + op: 'AND', + }, + functions: [], + groupBy: [ + { + dataType: DataTypes.String, + id: 'direction--string--tag--false', + isColumn: false, + isJSON: false, + key: 'direction', + type: 'tag', + }, + { + dataType: DataTypes.String, + id: 'device--string--tag--false', + isColumn: false, + isJSON: false, + key: 'device', + type: 'tag', + }, + ], + having: [ + { + columnName: 'SUM(system_network_io)', + op: '>', + value: 0, + }, + ], + legend: '{{device}}::{{direction}}', + limit: 30, + orderBy: [], + queryName: 'A', + reduceTo: 'avg', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'rate', + }, + ], + queryFormulas: [], + }, + clickhouse_sql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + id: '47173220-44df-4ef6-87f4-31e333c180c7', + promql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + queryType: EQueryType.QUERY_BUILDER, + }, + variables: {}, + formatForWeb: false, + start, + end, + }, + { + selectedTime: 'GLOBAL_TIME', + graphType: PANEL_TYPES.TIME_SERIES, + query: { + builder: { + queryData: [ + { + aggregateAttribute: { + dataType: DataTypes.Float64, + id: 'system_network_packets--float64--Sum--true', + isColumn: true, + isJSON: false, + key: 'system_network_packets', + type: 'Sum', + }, + aggregateOperator: 'rate', + dataSource: DataSource.METRICS, + disabled: false, + expression: 'A', + filters: { + items: [ + { + id: '3082ef53', + key: { + dataType: DataTypes.String, + id: 'host_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'host_name', + type: 'tag', + }, + op: '=', + value: hostName, + }, + ], + op: 'AND', + }, + functions: [], + groupBy: [ + { + dataType: DataTypes.String, + id: 'direction--string--tag--false', + isColumn: false, + isJSON: false, + key: 'direction', + type: 'tag', + }, + { + dataType: DataTypes.String, + id: 'device--string--tag--false', + isColumn: false, + isJSON: false, + key: 'device', + type: 'tag', + }, + ], + having: [], + legend: '{{device}}::{{direction}}', + limit: 30, + orderBy: [], + queryName: 'A', + reduceTo: 'avg', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'rate', + }, + ], + queryFormulas: [], + }, + clickhouse_sql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + id: '62eedbc6-c8ad-4d13-80a8-129396e1d1dc', + promql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + queryType: EQueryType.QUERY_BUILDER, + }, + variables: {}, + formatForWeb: false, + start, + end, + }, + { + selectedTime: 'GLOBAL_TIME', + graphType: PANEL_TYPES.TIME_SERIES, + query: { + builder: { + queryData: [ + { + aggregateAttribute: { + dataType: DataTypes.Float64, + id: 'system_network_errors--float64--Sum--true', + isColumn: true, + isJSON: false, + key: 'system_network_errors', + type: 'Sum', + }, + aggregateOperator: 'rate', + dataSource: DataSource.METRICS, + disabled: false, + expression: 'A', + filters: { + items: [ + { + id: '8859bc50', + key: { + dataType: DataTypes.String, + id: 'host_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'host_name', + type: 'tag', + }, + op: '=', + value: hostName, + }, + ], + op: 'AND', + }, + functions: [], + groupBy: [ + { + dataType: DataTypes.String, + id: 'direction--string--tag--false', + isColumn: false, + isJSON: false, + key: 'direction', + type: 'tag', + }, + { + dataType: DataTypes.String, + id: 'device--string--tag--false', + isColumn: false, + isJSON: false, + key: 'device', + type: 'tag', + }, + ], + having: [], + legend: '{{device}}::{{direction}}', + limit: 30, + orderBy: [], + queryName: 'A', + reduceTo: 'avg', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'rate', + }, + ], + queryFormulas: [], + }, + clickhouse_sql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + id: '5ddb1b38-53bb-46f5-b4fe-fe832d6b9b24', + promql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + queryType: EQueryType.QUERY_BUILDER, + }, + variables: {}, + formatForWeb: false, + start, + end, + }, + { + selectedTime: 'GLOBAL_TIME', + graphType: PANEL_TYPES.TIME_SERIES, + query: { + builder: { + queryData: [ + { + aggregateAttribute: { + dataType: DataTypes.Float64, + id: 'system_network_dropped--float64--Sum--true', + isColumn: true, + isJSON: false, + key: 'system_network_dropped', + type: 'Sum', + }, + aggregateOperator: 'rate', + dataSource: DataSource.METRICS, + disabled: false, + expression: 'A', + filters: { + items: [ + { + id: '40fec2e3', + key: { + dataType: DataTypes.String, + id: 'host_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'host_name', + type: 'tag', + }, + op: '=', + value: hostName, + }, + ], + op: 'AND', + }, + functions: [], + groupBy: [ + { + dataType: DataTypes.String, + id: 'direction--string--tag--false', + isColumn: false, + isJSON: false, + key: 'direction', + type: 'tag', + }, + { + dataType: DataTypes.String, + id: 'device--string--tag--false', + isColumn: false, + isJSON: false, + key: 'device', + type: 'tag', + }, + ], + having: [], + legend: '{{device}}::{{direction}}', + limit: 30, + orderBy: [], + queryName: 'A', + reduceTo: 'avg', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'rate', + }, + ], + queryFormulas: [], + }, + clickhouse_sql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + id: 'a849bcce-7684-4852-9134-530b45419b8f', + promql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + queryType: EQueryType.QUERY_BUILDER, + }, + variables: {}, + formatForWeb: false, + start, + end, + }, + { + selectedTime: 'GLOBAL_TIME', + graphType: PANEL_TYPES.TIME_SERIES, + query: { + builder: { + queryData: [ + { + aggregateAttribute: { + dataType: DataTypes.Float64, + id: 'system_network_connections--float64--Gauge--true', + isColumn: true, + isJSON: false, + key: 'system_network_connections', + type: 'Gauge', + }, + aggregateOperator: 'avg', + dataSource: DataSource.METRICS, + disabled: false, + expression: 'A', + filters: { + items: [ + { + id: '87f665b5', + key: { + dataType: DataTypes.String, + id: 'host_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'host_name', + type: 'tag', + }, + op: '=', + value: hostName, + }, + ], + op: 'AND', + }, + functions: [], + groupBy: [ + { + dataType: DataTypes.String, + id: 'protocol--string--tag--false', + isColumn: false, + isJSON: false, + key: 'protocol', + type: 'tag', + }, + { + dataType: DataTypes.String, + id: 'state--string--tag--false', + isColumn: false, + isJSON: false, + key: 'state', + type: 'tag', + }, + ], + having: [], + legend: '{{protocol}}::{{state}}', + limit: 30, + orderBy: [], + queryName: 'A', + reduceTo: 'avg', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'avg', + }, + ], + queryFormulas: [], + }, + clickhouse_sql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + id: 'ab685a3d-fa4c-4663-8d94-c452e59038f3', + promql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + queryType: EQueryType.QUERY_BUILDER, + }, + variables: {}, + formatForWeb: false, + start, + end, + }, + { + selectedTime: 'GLOBAL_TIME', + graphType: PANEL_TYPES.TIME_SERIES, + query: { + builder: { + queryData: [ + { + aggregateAttribute: { + dataType: DataTypes.Float64, + id: 'system_disk_io--float64--Sum--true', + isColumn: true, + isJSON: false, + key: 'system_disk_io', + type: 'Sum', + }, + aggregateOperator: 'rate', + dataSource: DataSource.METRICS, + disabled: false, + expression: 'A', + filters: { + items: [ + { + id: '6039199f', + key: { + dataType: DataTypes.String, + id: 'host_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'host_name', + type: 'tag', + }, + op: '=', + value: hostName, + }, + ], + op: 'AND', + }, + functions: [], + groupBy: [], + having: [], + legend: '', + limit: null, + orderBy: [], + queryName: 'A', + reduceTo: 'avg', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'rate', + }, + ], + queryFormulas: [], + }, + clickhouse_sql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + id: '9bd40b51-0790-4cdd-9718-551b2ded5926', + promql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + queryType: EQueryType.QUERY_BUILDER, + }, + variables: {}, + formatForWeb: false, + start, + end, + }, + { + selectedTime: 'GLOBAL_TIME', + graphType: PANEL_TYPES.TIME_SERIES, + query: { + builder: { + queryData: [ + { + aggregateAttribute: { + dataType: DataTypes.Float64, + id: 'system_disk_operation_time--float64--Sum--true', + isColumn: true, + isJSON: false, + key: 'system_disk_operation_time', + type: 'Sum', + }, + aggregateOperator: 'rate', + dataSource: DataSource.METRICS, + disabled: false, + expression: 'A', + filters: { + items: [ + { + id: 'd21dc017', + key: { + dataType: DataTypes.String, + id: 'host_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'host_name', + type: 'tag', + }, + op: '=', + value: hostName, + }, + ], + op: 'AND', + }, + functions: [], + groupBy: [ + { + dataType: DataTypes.String, + id: 'device--string--tag--false', + isColumn: false, + isJSON: false, + key: 'device', + type: 'tag', + }, + { + dataType: DataTypes.String, + id: 'direction--string--tag--false', + isColumn: false, + isJSON: false, + key: 'direction', + type: 'tag', + }, + ], + having: [ + { + columnName: 'SUM(system_disk_operation_time)', + op: '>', + value: 0, + }, + ], + legend: '{{device}}::{{direction}}', + limit: null, + orderBy: [], + queryName: 'A', + reduceTo: 'avg', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'rate', + }, + ], + queryFormulas: [], + }, + clickhouse_sql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + id: '9c6d18ad-89ff-4e38-a15a-440e72ed6ca8', + promql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + queryType: EQueryType.QUERY_BUILDER, + }, + variables: {}, + formatForWeb: false, + start, + end, + }, + { + selectedTime: 'GLOBAL_TIME', + graphType: PANEL_TYPES.TIME_SERIES, + query: { + builder: { + queryData: [ + { + aggregateAttribute: { + dataType: DataTypes.Float64, + id: 'system_disk_pending_operations--float64--Gauge--true', + isColumn: true, + isJSON: false, + key: 'system_disk_pending_operations', + type: 'Gauge', + }, + aggregateOperator: 'max', + dataSource: DataSource.METRICS, + disabled: false, + expression: 'A', + filters: { + items: [ + { + id: 'a1023af9', + key: { + dataType: DataTypes.String, + id: 'host_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'host_name', + type: 'tag', + }, + op: '=', + value: hostName, + }, + ], + op: 'AND', + }, + functions: [], + groupBy: [ + { + dataType: DataTypes.String, + id: 'device--string--tag--false', + isColumn: false, + isJSON: false, + key: 'device', + type: 'tag', + }, + ], + having: [ + { + columnName: 'SUM(system_disk_pending_operations)', + op: '>', + value: 0, + }, + ], + legend: '{{device}}', + limit: null, + orderBy: [], + queryName: 'A', + reduceTo: 'avg', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'max', + }, + ], + queryFormulas: [], + }, + clickhouse_sql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + id: 'f4cfc2a5-78fc-42cc-8f4a-194c8c916132', + promql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + queryType: EQueryType.QUERY_BUILDER, + }, + variables: {}, + formatForWeb: false, + start, + end, + }, + { + selectedTime: 'GLOBAL_TIME', + graphType: PANEL_TYPES.TIME_SERIES, + query: { + builder: { + queryData: [ + { + aggregateAttribute: { + dataType: DataTypes.Float64, + id: 'system_disk_operation_time--float64--Sum--true', + isColumn: true, + isJSON: false, + key: 'system_disk_operation_time', + type: 'Sum', + }, + aggregateOperator: 'rate', + dataSource: DataSource.METRICS, + disabled: false, + expression: 'A', + filters: { + items: [ + { + id: 'd21dc017', + key: { + dataType: DataTypes.String, + id: 'host_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'host_name', + type: 'tag', + }, + op: '=', + value: hostName, + }, + ], + op: 'AND', + }, + functions: [], + groupBy: [ + { + dataType: DataTypes.String, + id: 'device--string--tag--false', + isColumn: false, + isJSON: false, + key: 'device', + type: 'tag', + }, + { + dataType: DataTypes.String, + id: 'direction--string--tag--false', + isColumn: false, + isJSON: false, + key: 'direction', + type: 'tag', + }, + ], + having: [ + { + columnName: 'SUM(system_disk_operation_time)', + op: '>', + value: 0, + }, + ], + legend: '{{device}}::{{direction}}', + limit: null, + orderBy: [], + queryName: 'A', + reduceTo: 'avg', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'rate', + }, + ], + queryFormulas: [], + }, + clickhouse_sql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + id: '9c6d18ad-89ff-4e38-a15a-440e72ed6ca8', + promql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + queryType: EQueryType.QUERY_BUILDER, + }, + variables: {}, + formatForWeb: false, + start, + end, + }, +]; + +export const podWidgetInfo = [ + { + title: 'CPU usage', + yAxisUnit: '', + }, + { + title: 'Memory Usage', + yAxisUnit: 'bytes', + }, + { + title: 'Pod CPU usage [% of Request]', + yAxisUnit: 'percent', + }, + { + title: 'Pod CPU usage [% of Limit]', + yAxisUnit: 'percent', + }, + { + title: 'Pod memory usage [% of Request]', + yAxisUnit: 'percent', + }, + { + title: 'Pod memory usage [% of Limit]', + yAxisUnit: 'percent', + }, + { + title: 'Pod filesystem usage [%]', + yAxisUnit: 'percentunit', + }, + { + title: 'Pod network IO', + yAxisUnit: 'binBps', + }, +]; + +export const VIEW_TYPES = { + NODE: 'node', + POD: 'pod', +}; + +export const nodeWidgetInfo = [ + { + title: 'Node CPU usage', + yAxisUnit: 'percentunit', + }, + { + title: 'Node memory usage (WSS)', + yAxisUnit: 'percentunit', + }, + { + title: 'Node network IO', + yAxisUnit: 'binBps', + }, + { + title: 'Node filesystem usage', + yAxisUnit: 'percentunit', + }, +]; + +export const hostWidgetInfo = [ + { title: 'CPU Usage', yAxisUnit: 'percentunit' }, + { title: 'Memory Usage', yAxisUnit: 'bytes' }, + { title: 'System Load Average', yAxisUnit: '' }, + { title: 'Network usage (bytes)', yAxisUnit: 'bytes' }, + { title: 'Network usage (packet/s)', yAxisUnit: 'pps' }, + { title: 'Network errors', yAxisUnit: 'short' }, + { title: 'Network drops', yAxisUnit: 'short' }, + { title: 'Network connections', yAxisUnit: 'short' }, + { title: 'System disk io (bytes transferred)', yAxisUnit: 'bytes' }, + { title: 'System disk operations/s', yAxisUnit: 'short' }, + { title: 'Queue size', yAxisUnit: 'short' }, + { title: 'Disk operations time', yAxisUnit: 's' }, +]; diff --git a/frontend/src/container/LogDetailedView/TableView.tsx b/frontend/src/container/LogDetailedView/TableView.tsx index 591109ac3c..45e8417659 100644 --- a/frontend/src/container/LogDetailedView/TableView.tsx +++ b/frontend/src/container/LogDetailedView/TableView.tsx @@ -122,10 +122,10 @@ function TableView({ fieldValue: string, ) => (): void => { handleClick(operator, fieldKey, fieldValue); - if (operator === OPERATORS.IN) { + if (operator === OPERATORS['=']) { setIsFilterInLoading(true); } - if (operator === OPERATORS.NIN) { + if (operator === OPERATORS['!=']) { setIsFilterOutLoading(true); } }; diff --git a/frontend/src/container/LogDetailedView/TableView/TableViewActions.tsx b/frontend/src/container/LogDetailedView/TableView/TableViewActions.tsx index 74b30bf6de..57ceea5072 100644 --- a/frontend/src/container/LogDetailedView/TableView/TableViewActions.tsx +++ b/frontend/src/container/LogDetailedView/TableView/TableViewActions.tsx @@ -139,7 +139,7 @@ export function TableViewActions( ) } - onClick={onClickHandler(OPERATORS.IN, fieldFilterKey, fieldData.value)} + onClick={onClickHandler(OPERATORS['='], fieldFilterKey, fieldData.value)} /> @@ -152,7 +152,11 @@ export function TableViewActions( ) } - onClick={onClickHandler(OPERATORS.NIN, fieldFilterKey, fieldData.value)} + onClick={onClickHandler( + OPERATORS['!='], + fieldFilterKey, + fieldData.value, + )} /> {!isOldLogsExplorerOrLiveLogsPage && ( diff --git a/frontend/src/container/LogDetailedView/index.tsx b/frontend/src/container/LogDetailedView/index.tsx index 4ff7ab6a99..1b2ecddc08 100644 --- a/frontend/src/container/LogDetailedView/index.tsx +++ b/frontend/src/container/LogDetailedView/index.tsx @@ -1,6 +1,7 @@ import LogDetail from 'components/LogDetail'; import { VIEW_TYPES } from 'components/LogDetail/constants'; import ROUTES from 'constants/routes'; +import { getOldLogsOperatorFromNew } from 'hooks/logs/useActiveLog'; import { getGeneratedFilterQueryString } from 'lib/getGeneratedFilterQueryString'; import getStep from 'lib/getStep'; import { getIdConditions } from 'pages/Logs/utils'; @@ -57,10 +58,11 @@ function LogDetailedView({ const handleAddToQuery = useCallback( (fieldKey: string, fieldValue: string, operator: string) => { + const newOperator = getOldLogsOperatorFromNew(operator); const updatedQueryString = getGeneratedFilterQueryString( fieldKey, fieldValue, - operator, + newOperator, queryString, ); @@ -71,10 +73,11 @@ function LogDetailedView({ const handleClickActionItem = useCallback( (fieldKey: string, fieldValue: string, operator: string): void => { + const newOperator = getOldLogsOperatorFromNew(operator); const updatedQueryString = getGeneratedFilterQueryString( fieldKey, fieldValue, - operator, + newOperator, queryString, ); diff --git a/frontend/src/container/LogExplorerQuerySection/index.tsx b/frontend/src/container/LogExplorerQuerySection/index.tsx index f807103f68..c49990861f 100644 --- a/frontend/src/container/LogExplorerQuerySection/index.tsx +++ b/frontend/src/container/LogExplorerQuerySection/index.tsx @@ -1,6 +1,5 @@ import './LogsExplorerQuerySection.styles.scss'; -import { FeatureKeys } from 'constants/features'; import { initialQueriesMap, OPERATORS, @@ -9,14 +8,12 @@ import { import ExplorerOrderBy from 'container/ExplorerOrderBy'; import { QueryBuilder } from 'container/QueryBuilder'; import { OrderByFilterProps } from 'container/QueryBuilder/filters/OrderByFilter/OrderByFilter.interfaces'; -import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch'; import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2'; import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces'; import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations'; import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl'; -import useFeatureFlags from 'hooks/useFeatureFlag'; import { prepareQueryWithDefaultTimestamp, SELECTED_VIEWS, @@ -89,26 +86,15 @@ function LogExplorerQuerySection({ [handleChangeQueryData], ); - const isSearchV2Enabled = - useFeatureFlags(FeatureKeys.QUERY_BUILDER_SEARCH_V2)?.active || false; - return ( <> {selectedView === SELECTED_VIEWS.SEARCH && (
- {isSearchV2Enabled ? ( - - ) : ( - - )} +
)} diff --git a/frontend/src/container/LogsExplorerChart/LogsExplorerChart.interfaces.ts b/frontend/src/container/LogsExplorerChart/LogsExplorerChart.interfaces.ts index a19a41d778..ee447242eb 100644 --- a/frontend/src/container/LogsExplorerChart/LogsExplorerChart.interfaces.ts +++ b/frontend/src/container/LogsExplorerChart/LogsExplorerChart.interfaces.ts @@ -3,6 +3,7 @@ import { QueryData } from 'types/api/widgets/getQuery'; export type LogsExplorerChartProps = { data: QueryData[]; isLoading: boolean; + isLogsExplorerViews?: boolean; isLabelEnabled?: boolean; className?: string; }; diff --git a/frontend/src/container/LogsExplorerChart/index.tsx b/frontend/src/container/LogsExplorerChart/index.tsx index 7f4d648529..7ac1934bb7 100644 --- a/frontend/src/container/LogsExplorerChart/index.tsx +++ b/frontend/src/container/LogsExplorerChart/index.tsx @@ -16,12 +16,14 @@ import { UpdateTimeInterval } from 'store/actions'; import { LogsExplorerChartProps } from './LogsExplorerChart.interfaces'; import { CardStyled } from './LogsExplorerChart.styled'; +import { getColorsForSeverityLabels } from './utils'; function LogsExplorerChart({ data, isLoading, isLabelEnabled = true, className, + isLogsExplorerViews = false, }: LogsExplorerChartProps): JSX.Element { const dispatch = useDispatch(); const urlQuery = useUrlQuery(); @@ -29,15 +31,19 @@ function LogsExplorerChart({ const handleCreateDatasets: Required['createDataset'] = useCallback( (element, index, allLabels) => ({ data: element, - backgroundColor: colors[index % colors.length] || themeColors.red, - borderColor: colors[index % colors.length] || themeColors.red, + backgroundColor: isLogsExplorerViews + ? getColorsForSeverityLabels(allLabels[index], index) + : colors[index % colors.length] || themeColors.red, + borderColor: isLogsExplorerViews + ? getColorsForSeverityLabels(allLabels[index], index) + : colors[index % colors.length] || themeColors.red, ...(isLabelEnabled ? { label: allLabels[index], } : {}), }), - [isLabelEnabled], + [isLabelEnabled, isLogsExplorerViews], ); const onDragSelect = useCallback( @@ -112,6 +118,7 @@ function LogsExplorerChart({ (1); const [logs, setLogs] = useState([]); + const [lastLogLineTimestamp, setLastLogLineTimestamp] = useState< + number | string | null + >(); const [requestData, setRequestData] = useState(null); const [showFormatMenuItems, setShowFormatMenuItems] = useState(false); const [queryId, setQueryId] = useState(v4()); @@ -188,6 +192,16 @@ function LogsExplorerViews({ const modifiedQueryData: IBuilderQuery = { ...listQuery, aggregateOperator: LogsAggregatorOperator.COUNT, + groupBy: [ + { + key: 'severity_text', + dataType: DataTypes.String, + type: '', + isColumn: true, + isJSON: false, + id: 'severity_text--string----true', + }, + ], }; const modifiedQuery: Query = { @@ -259,6 +273,14 @@ function LogsExplorerViews({ start: minTime, end: maxTime, }), + // send the lastLogTimeStamp only when the panel type is list and the orderBy is timestamp and the order is desc + lastLogLineTimestamp: + panelType === PANEL_TYPES.LIST && + requestData?.builder?.queryData?.[0]?.orderBy?.[0]?.columnName === + 'timestamp' && + requestData?.builder?.queryData?.[0]?.orderBy?.[0]?.order === 'desc' + ? lastLogLineTimestamp + : undefined, }, undefined, listQueryKeyRef, @@ -336,6 +358,10 @@ function LogsExplorerViews({ pageSize: nextPageSize, }); + // initialise the last log timestamp to null as we don't have the logs. + // as soon as we scroll to the end of the logs we set the lastLogLineTimestamp to the last log timestamp. + setLastLogLineTimestamp(lastLog.timestamp); + setPage((prevPage) => prevPage + 1); setRequestData(newRequestData); @@ -528,6 +554,11 @@ function LogsExplorerViews({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [data]); + useEffect(() => { + // clear the lastLogLineTimestamp when the data changes + setLastLogLineTimestamp(null); + }, [data]); + useEffect(() => { if ( requestData?.id !== stagedQuery?.id || @@ -661,6 +692,7 @@ function LogsExplorerViews({ className="logs-histogram" isLoading={isFetchingListChartData || isLoadingListChartData} data={chartData} + isLogsExplorerViews={panelType === PANEL_TYPES.LIST} /> )} diff --git a/frontend/src/container/LogsExplorerViews/tests/LogsExplorerViews.test.tsx b/frontend/src/container/LogsExplorerViews/tests/LogsExplorerViews.test.tsx index 8262d6f9bc..6271ff793e 100644 --- a/frontend/src/container/LogsExplorerViews/tests/LogsExplorerViews.test.tsx +++ b/frontend/src/container/LogsExplorerViews/tests/LogsExplorerViews.test.tsx @@ -28,6 +28,20 @@ const lodsQueryServerRequest = (): void => ), ); +jest.mock('uplot', () => { + const paths = { + spline: jest.fn(), + bars: jest.fn(), + }; + const uplotMock = jest.fn(() => ({ + paths, + })); + return { + paths, + default: uplotMock, + }; +}); + // mocking the graph components in this test as this should be handled separately jest.mock( 'container/TimeSeriesView/TimeSeriesView', diff --git a/frontend/src/container/LogsPanelTable/LogsPanelComponent.styles.scss b/frontend/src/container/LogsPanelTable/LogsPanelComponent.styles.scss index 6317ea2134..b355c90551 100644 --- a/frontend/src/container/LogsPanelTable/LogsPanelComponent.styles.scss +++ b/frontend/src/container/LogsPanelTable/LogsPanelComponent.styles.scss @@ -63,6 +63,8 @@ height: 40px; justify-content: end; padding: 0 8px; + margin-top: 12px; + margin-bottom: 2px; } } diff --git a/frontend/src/container/NewDashboard/DashboardDescription/Description.styles.scss b/frontend/src/container/NewDashboard/DashboardDescription/Description.styles.scss index d82c2da7b6..0f4b2dcc95 100644 --- a/frontend/src/container/NewDashboard/DashboardDescription/Description.styles.scss +++ b/frontend/src/container/NewDashboard/DashboardDescription/Description.styles.scss @@ -130,12 +130,16 @@ .left-section { display: flex; - flex-wrap: wrap; align-items: center; gap: 8px; width: 45%; + .dashboard-img { + height: 16px; + width: 16px; + } + .dashboard-title { color: #fff; font-family: Inter; diff --git a/frontend/src/container/NewDashboard/DashboardDescription/index.tsx b/frontend/src/container/NewDashboard/DashboardDescription/index.tsx index 31b5e4c247..ea59dc4bcf 100644 --- a/frontend/src/container/NewDashboard/DashboardDescription/index.tsx +++ b/frontend/src/container/NewDashboard/DashboardDescription/index.tsx @@ -306,16 +306,13 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
+ dashboard-img 30 ? title : ''}> - dashboard-img{' '} + {' '} {title} diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/DashboardVariableSelection.styles.scss b/frontend/src/container/NewDashboard/DashboardVariablesSelection/DashboardVariableSelection.styles.scss index f7fcb83a53..6df3e79906 100644 --- a/frontend/src/container/NewDashboard/DashboardVariablesSelection/DashboardVariableSelection.styles.scss +++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/DashboardVariableSelection.styles.scss @@ -43,6 +43,15 @@ .ant-select-item { display: flex; align-items: center; + gap: 8px; + } + + .rc-virtual-list-holder { + [data-testid='option-ALL'] { + border-bottom: 1px solid var(--bg-slate-400); + padding-bottom: 12px; + margin-bottom: 8px; + } } .all-label { @@ -56,28 +65,25 @@ } .dropdown-value { - display: flex; - justify-content: space-between; - align-items: center; + display: grid; + grid-template-columns: 1fr max-content; .option-text { - max-width: 180px; padding: 0 8px; } .toggle-tag-label { padding-left: 8px; right: 40px; - font-weight: normal; - position: absolute; + font-weight: 500; } } } } .dropdown-styles { - min-width: 300px; - max-width: 350px; + min-width: 400px; + max-width: 500px; } .lightMode { diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.test.tsx b/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.test.tsx index 0c8fbd51ae..1cb89d6b95 100644 --- a/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.test.tsx +++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.test.tsx @@ -1,14 +1,8 @@ import '@testing-library/jest-dom/extend-expect'; -import { - act, - fireEvent, - render, - screen, - waitFor, -} from '@testing-library/react'; import MockQueryClientProvider from 'providers/test/MockQueryClientProvider'; import React, { useEffect } from 'react'; +import { act, fireEvent, render, screen, waitFor } from 'tests/test-utils'; import { IDashboardVariable } from 'types/api/dashboard/getAll'; import VariableItem from './VariableItem'; diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.tsx b/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.tsx index baa8228b3c..a0a444a715 100644 --- a/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.tsx +++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.tsx @@ -1,3 +1,4 @@ +/* eslint-disable sonarjs/cognitive-complexity */ /* eslint-disable jsx-a11y/click-events-have-key-events */ /* eslint-disable jsx-a11y/no-static-element-interactions */ /* eslint-disable @typescript-eslint/no-explicit-any */ @@ -25,8 +26,11 @@ import { debounce, isArray, isString } from 'lodash-es'; import map from 'lodash-es/map'; import { ChangeEvent, memo, useEffect, useMemo, useState } from 'react'; import { useQuery } from 'react-query'; +import { useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; import { IDashboardVariable } from 'types/api/dashboard/getAll'; import { VariableResponseProps } from 'types/api/dashboard/variables/query'; +import { GlobalReducer } from 'types/reducer/globalTime'; import { popupContainer } from 'utils/selectPopupContainer'; import { variablePropsToPayloadVariables } from '../utils'; @@ -58,14 +62,14 @@ interface VariableItemProps { const getSelectValue = ( selectedValue: IDashboardVariable['selectedValue'], variableData: IDashboardVariable, -): string | string[] => { +): string | string[] | undefined => { if (Array.isArray(selectedValue)) { if (!variableData.multiSelect && selectedValue.length === 1) { - return selectedValue[0]?.toString() || ''; + return selectedValue[0]?.toString(); } return selectedValue.map((item) => item.toString()); } - return selectedValue?.toString() || ''; + return selectedValue?.toString(); }; // eslint-disable-next-line sonarjs/cognitive-complexity @@ -80,6 +84,23 @@ function VariableItem({ [], ); + const { maxTime, minTime } = useSelector( + (state) => state.globalTime, + ); + + useEffect(() => { + if (variableData.allSelected && variableData.type === 'QUERY') { + setVariablesToGetUpdated((prev) => { + const variablesQueue = [...prev.filter((v) => v !== variableData.name)]; + if (variableData.name) { + variablesQueue.push(variableData.name); + } + return variablesQueue; + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [minTime, maxTime]); + const [errorMessage, setErrorMessage] = useState(null); const getDependentVariables = (queryValue: string): string[] => { @@ -111,7 +132,14 @@ function VariableItem({ const variableKey = dependentVariablesStr.replace(/\s/g, ''); - return [REACT_QUERY_KEY.DASHBOARD_BY_ID, variableName, variableKey]; + // added this time dependency for variables query as API respects the passed time range now + return [ + REACT_QUERY_KEY.DASHBOARD_BY_ID, + variableName, + variableKey, + `${minTime}`, + `${maxTime}`, + ]; }; // eslint-disable-next-line sonarjs/cognitive-complexity @@ -151,10 +179,14 @@ function VariableItem({ valueNotInList = true; } } + // variablesData.allSelected is added for the case where on change of options we need to update the + // local storage if ( variableData.type === 'QUERY' && variableData.name && - (variablesToGetUpdated.includes(variableData.name) || valueNotInList) + (variablesToGetUpdated.includes(variableData.name) || + valueNotInList || + variableData.allSelected) ) { let value = variableData.selectedValue; let allSelected = false; @@ -268,7 +300,7 @@ function VariableItem({ e.stopPropagation(); e.preventDefault(); const isChecked = - variableData.allSelected || selectValue.includes(ALL_SELECT_VALUE); + variableData.allSelected || selectValue?.includes(ALL_SELECT_VALUE); if (isChecked) { handleChange([]); @@ -338,8 +370,8 @@ function VariableItem({ (Array.isArray(selectValue) && selectValue?.includes(option.toString())); if (isChecked) { - if (mode === ToggleTagValue.Only) { - handleChange(option.toString()); + if (mode === ToggleTagValue.Only && variableData.multiSelect) { + handleChange([option.toString()]); } else if (!variableData.multiSelect) { handleChange(option.toString()); } else { @@ -430,6 +462,7 @@ function VariableItem({ + {omittedValues.length} )} + allowClear > {enableSelectAll && ( @@ -468,11 +501,17 @@ function VariableItem({ {...retProps(option as string)} onClick={(e): void => handleToggle(e as any, option as string)} > - - - {option.toString()} - - + + {option.toString()} + {variableData.multiSelect && optionState.tag === option.toString() && diff --git a/frontend/src/container/NewWidget/RightContainer/constants.ts b/frontend/src/container/NewWidget/RightContainer/constants.ts index 171f6b81d3..03cee96d21 100644 --- a/frontend/src/container/NewWidget/RightContainer/constants.ts +++ b/frontend/src/container/NewWidget/RightContainer/constants.ts @@ -74,7 +74,7 @@ export const panelTypeVsYAxisUnit: { [key in PANEL_TYPES]: boolean } = { [PANEL_TYPES.VALUE]: true, [PANEL_TYPES.TABLE]: false, [PANEL_TYPES.LIST]: false, - [PANEL_TYPES.PIE]: false, + [PANEL_TYPES.PIE]: true, [PANEL_TYPES.BAR]: true, [PANEL_TYPES.HISTOGRAM]: false, [PANEL_TYPES.TRACE]: false, diff --git a/frontend/src/container/NewWidget/RightContainer/index.tsx b/frontend/src/container/NewWidget/RightContainer/index.tsx index 84400737d4..43e3b5611d 100644 --- a/frontend/src/container/NewWidget/RightContainer/index.tsx +++ b/frontend/src/container/NewWidget/RightContainer/index.tsx @@ -211,7 +211,11 @@ function RightContainer({ )} {allowSoftMinMax && ( diff --git a/frontend/src/container/PanelWrapper/PanelWrapper.tsx b/frontend/src/container/PanelWrapper/PanelWrapper.tsx index ed105b3948..2f5b35485e 100644 --- a/frontend/src/container/PanelWrapper/PanelWrapper.tsx +++ b/frontend/src/container/PanelWrapper/PanelWrapper.tsx @@ -16,6 +16,7 @@ function PanelWrapper({ selectedGraph, tableProcessedDataRef, customTooltipElement, + searchTerm, }: PanelWrapperProps): JSX.Element { const Component = PanelTypeVsPanelWrapper[ selectedGraph || widget.panelTypes @@ -39,6 +40,7 @@ function PanelWrapper({ selectedGraph={selectedGraph} tableProcessedDataRef={tableProcessedDataRef} customTooltipElement={customTooltipElement} + searchTerm={searchTerm} /> ); } diff --git a/frontend/src/container/PanelWrapper/PiePanelWrapper.tsx b/frontend/src/container/PanelWrapper/PiePanelWrapper.tsx index a176247781..dce84ad78d 100644 --- a/frontend/src/container/PanelWrapper/PiePanelWrapper.tsx +++ b/frontend/src/container/PanelWrapper/PiePanelWrapper.tsx @@ -4,6 +4,7 @@ import { Color } from '@signozhq/design-tokens'; import { Group } from '@visx/group'; import { Pie } from '@visx/shape'; import { useTooltip, useTooltipInPortal } from '@visx/tooltip'; +import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig'; import { themeColors } from 'constants/theme'; import { useIsDarkMode } from 'hooks/useDarkMode'; import { generateColor } from 'lib/uPlotLib/utils/generateColor'; @@ -129,7 +130,12 @@ function PiePanelWrapper({ showTooltip({ tooltipData: { label, - value: arc.data.value, + // do not update the unit in the data as the arc allotment is based on value + // and treats 4K smaller than 40 + value: getYAxisFormattedValue( + arc.data.value, + widget?.yAxisUnit || 'none', + ), color: arc.data.color, key: label, }, diff --git a/frontend/src/container/PanelWrapper/TablePanelWrapper.tsx b/frontend/src/container/PanelWrapper/TablePanelWrapper.tsx index 0eab4143a2..c5222e8d53 100644 --- a/frontend/src/container/PanelWrapper/TablePanelWrapper.tsx +++ b/frontend/src/container/PanelWrapper/TablePanelWrapper.tsx @@ -8,6 +8,7 @@ function TablePanelWrapper({ widget, queryResponse, tableProcessedDataRef, + searchTerm, }: PanelWrapperProps): JSX.Element { const panelData = (queryResponse.data?.payload?.data?.result?.[0] as any)?.table || []; @@ -20,6 +21,7 @@ function TablePanelWrapper({ columnUnits={widget.columnUnits} tableProcessedDataRef={tableProcessedDataRef} sticky={widget.panelTypes === PANEL_TYPES.TABLE} + searchTerm={searchTerm} // eslint-disable-next-line react/jsx-props-no-spreading {...GRID_TABLE_CONFIG} /> diff --git a/frontend/src/container/PanelWrapper/panelWrapper.types.ts b/frontend/src/container/PanelWrapper/panelWrapper.types.ts index 7d5e3122e8..4778ffdb97 100644 --- a/frontend/src/container/PanelWrapper/panelWrapper.types.ts +++ b/frontend/src/container/PanelWrapper/panelWrapper.types.ts @@ -23,6 +23,7 @@ export type PanelWrapperProps = { onDragSelect: (start: number, end: number) => void; selectedGraph?: PANEL_TYPES; tableProcessedDataRef?: React.MutableRefObject; + searchTerm?: string; customTooltipElement?: HTMLDivElement; }; diff --git a/frontend/src/container/PipelinePage/Layouts/ChangeHistory/tests/ChangeHistory.test.tsx b/frontend/src/container/PipelinePage/Layouts/ChangeHistory/tests/ChangeHistory.test.tsx index 194acbea0a..88fdb5d594 100644 --- a/frontend/src/container/PipelinePage/Layouts/ChangeHistory/tests/ChangeHistory.test.tsx +++ b/frontend/src/container/PipelinePage/Layouts/ChangeHistory/tests/ChangeHistory.test.tsx @@ -9,6 +9,20 @@ import store from 'store'; import ChangeHistory from '../index'; import { pipelineData, pipelineDataHistory } from './testUtils'; +jest.mock('uplot', () => { + const paths = { + spline: jest.fn(), + bars: jest.fn(), + }; + const uplotMock = jest.fn(() => ({ + paths, + })); + return { + paths, + default: uplotMock, + }; +}); + const queryClient = new QueryClient({ defaultOptions: { queries: { diff --git a/frontend/src/container/PipelinePage/tests/AddNewPipeline.test.tsx b/frontend/src/container/PipelinePage/tests/AddNewPipeline.test.tsx index 8990ffa4e7..360a7c5925 100644 --- a/frontend/src/container/PipelinePage/tests/AddNewPipeline.test.tsx +++ b/frontend/src/container/PipelinePage/tests/AddNewPipeline.test.tsx @@ -9,6 +9,20 @@ import store from 'store'; import { pipelineMockData } from '../mocks/pipeline'; import AddNewPipeline from '../PipelineListsView/AddNewPipeline'; +jest.mock('uplot', () => { + const paths = { + spline: jest.fn(), + bars: jest.fn(), + }; + const uplotMock = jest.fn(() => ({ + paths, + })); + return { + paths, + default: uplotMock, + }; +}); + export function matchMedia(): void { Object.defineProperty(window, 'matchMedia', { writable: true, diff --git a/frontend/src/container/PipelinePage/tests/AddNewProcessor.test.tsx b/frontend/src/container/PipelinePage/tests/AddNewProcessor.test.tsx index a4d2e680a4..d3f236437f 100644 --- a/frontend/src/container/PipelinePage/tests/AddNewProcessor.test.tsx +++ b/frontend/src/container/PipelinePage/tests/AddNewProcessor.test.tsx @@ -9,6 +9,20 @@ import { pipelineMockData } from '../mocks/pipeline'; import AddNewProcessor from '../PipelineListsView/AddNewProcessor'; import { matchMedia } from './AddNewPipeline.test'; +jest.mock('uplot', () => { + const paths = { + spline: jest.fn(), + bars: jest.fn(), + }; + const uplotMock = jest.fn(() => ({ + paths, + })); + return { + paths, + default: uplotMock, + }; +}); + beforeAll(() => { matchMedia(); }); diff --git a/frontend/src/container/PipelinePage/tests/DeleteAction.test.tsx b/frontend/src/container/PipelinePage/tests/DeleteAction.test.tsx index 451ef8807f..3b2fdfeb34 100644 --- a/frontend/src/container/PipelinePage/tests/DeleteAction.test.tsx +++ b/frontend/src/container/PipelinePage/tests/DeleteAction.test.tsx @@ -6,6 +6,20 @@ import { MemoryRouter } from 'react-router-dom'; import i18n from 'ReactI18'; import store from 'store'; +jest.mock('uplot', () => { + const paths = { + spline: jest.fn(), + bars: jest.fn(), + }; + const uplotMock = jest.fn(() => ({ + paths, + })); + return { + paths, + default: uplotMock, + }; +}); + describe('PipelinePage container test', () => { it('should render DeleteAction section', () => { const { asFragment } = render( diff --git a/frontend/src/container/PipelinePage/tests/DragAction.test.tsx b/frontend/src/container/PipelinePage/tests/DragAction.test.tsx index 168b3f042f..9f64714072 100644 --- a/frontend/src/container/PipelinePage/tests/DragAction.test.tsx +++ b/frontend/src/container/PipelinePage/tests/DragAction.test.tsx @@ -6,6 +6,20 @@ import { MemoryRouter } from 'react-router-dom'; import i18n from 'ReactI18'; import store from 'store'; +jest.mock('uplot', () => { + const paths = { + spline: jest.fn(), + bars: jest.fn(), + }; + const uplotMock = jest.fn(() => ({ + paths, + })); + return { + paths, + default: uplotMock, + }; +}); + describe('PipelinePage container test', () => { it('should render DragAction section', () => { const { asFragment } = render( diff --git a/frontend/src/container/PipelinePage/tests/EditAction.test.tsx b/frontend/src/container/PipelinePage/tests/EditAction.test.tsx index c52991bf6d..56dd779600 100644 --- a/frontend/src/container/PipelinePage/tests/EditAction.test.tsx +++ b/frontend/src/container/PipelinePage/tests/EditAction.test.tsx @@ -6,6 +6,20 @@ import { MemoryRouter } from 'react-router-dom'; import i18n from 'ReactI18'; import store from 'store'; +jest.mock('uplot', () => { + const paths = { + spline: jest.fn(), + bars: jest.fn(), + }; + const uplotMock = jest.fn(() => ({ + paths, + })); + return { + paths, + default: uplotMock, + }; +}); + describe('PipelinePage container test', () => { it('should render EditAction section', () => { const { asFragment } = render( diff --git a/frontend/src/container/PipelinePage/tests/PipelineActions.test.tsx b/frontend/src/container/PipelinePage/tests/PipelineActions.test.tsx index 83f503107b..d472f4745c 100644 --- a/frontend/src/container/PipelinePage/tests/PipelineActions.test.tsx +++ b/frontend/src/container/PipelinePage/tests/PipelineActions.test.tsx @@ -8,6 +8,20 @@ import store from 'store'; import { pipelineMockData } from '../mocks/pipeline'; import PipelineActions from '../PipelineListsView/TableComponents/PipelineActions'; +jest.mock('uplot', () => { + const paths = { + spline: jest.fn(), + bars: jest.fn(), + }; + const uplotMock = jest.fn(() => ({ + paths, + })); + return { + paths, + default: uplotMock, + }; +}); + describe('PipelinePage container test', () => { it('should render PipelineActions section', () => { const { asFragment } = render( diff --git a/frontend/src/container/PipelinePage/tests/PipelineExpandView.test.tsx b/frontend/src/container/PipelinePage/tests/PipelineExpandView.test.tsx index 6875d11259..b9c78091dd 100644 --- a/frontend/src/container/PipelinePage/tests/PipelineExpandView.test.tsx +++ b/frontend/src/container/PipelinePage/tests/PipelineExpandView.test.tsx @@ -9,6 +9,20 @@ import { pipelineMockData } from '../mocks/pipeline'; import PipelineExpandView from '../PipelineListsView/PipelineExpandView'; import { matchMedia } from './AddNewPipeline.test'; +jest.mock('uplot', () => { + const paths = { + spline: jest.fn(), + bars: jest.fn(), + }; + const uplotMock = jest.fn(() => ({ + paths, + })); + return { + paths, + default: uplotMock, + }; +}); + beforeAll(() => { matchMedia(); }); diff --git a/frontend/src/container/PipelinePage/tests/PipelineListsView.test.tsx b/frontend/src/container/PipelinePage/tests/PipelineListsView.test.tsx index 74f1f125e0..517e623ebe 100644 --- a/frontend/src/container/PipelinePage/tests/PipelineListsView.test.tsx +++ b/frontend/src/container/PipelinePage/tests/PipelineListsView.test.tsx @@ -11,6 +11,20 @@ import store from 'store'; import { pipelineApiResponseMockData } from '../mocks/pipeline'; import PipelineListsView from '../PipelineListsView'; +jest.mock('uplot', () => { + const paths = { + spline: jest.fn(), + bars: jest.fn(), + }; + const uplotMock = jest.fn(() => ({ + paths, + })); + return { + paths, + default: uplotMock, + }; +}); + const samplePipelinePreviewResponse = { isLoading: false, logs: [ diff --git a/frontend/src/container/PipelinePage/tests/PipelinePageLayout.test.tsx b/frontend/src/container/PipelinePage/tests/PipelinePageLayout.test.tsx index 91d5dfe244..a71bc1266d 100644 --- a/frontend/src/container/PipelinePage/tests/PipelinePageLayout.test.tsx +++ b/frontend/src/container/PipelinePage/tests/PipelinePageLayout.test.tsx @@ -11,6 +11,20 @@ import { v4 } from 'uuid'; import PipelinePageLayout from '../Layouts/Pipeline'; import { matchMedia } from './AddNewPipeline.test'; +jest.mock('uplot', () => { + const paths = { + spline: jest.fn(), + bars: jest.fn(), + }; + const uplotMock = jest.fn(() => ({ + paths, + })); + return { + paths, + default: uplotMock, + }; +}); + beforeAll(() => { matchMedia(); }); diff --git a/frontend/src/container/PipelinePage/tests/TagInput.test.tsx b/frontend/src/container/PipelinePage/tests/TagInput.test.tsx index 24cedc2eb0..e95efb6715 100644 --- a/frontend/src/container/PipelinePage/tests/TagInput.test.tsx +++ b/frontend/src/container/PipelinePage/tests/TagInput.test.tsx @@ -7,6 +7,20 @@ import store from 'store'; import TagInput from '../components/TagInput'; +jest.mock('uplot', () => { + const paths = { + spline: jest.fn(), + bars: jest.fn(), + }; + const uplotMock = jest.fn(() => ({ + paths, + })); + return { + paths, + default: uplotMock, + }; +}); + describe('Pipeline Page', () => { it('should render TagInput section', () => { const { asFragment } = render( diff --git a/frontend/src/container/PipelinePage/tests/utils.test.ts b/frontend/src/container/PipelinePage/tests/utils.test.ts index c21e8c5a4b..707ad06c2d 100644 --- a/frontend/src/container/PipelinePage/tests/utils.test.ts +++ b/frontend/src/container/PipelinePage/tests/utils.test.ts @@ -11,6 +11,20 @@ import { getTableColumn, } from '../PipelineListsView/utils'; +jest.mock('uplot', () => { + const paths = { + spline: jest.fn(), + bars: jest.fn(), + }; + const uplotMock = jest.fn(() => ({ + paths, + })); + return { + paths, + default: uplotMock, + }; +}); + describe('Utils testing of Pipeline Page', () => { test('it should be check form field of add pipeline', () => { expect(pipelineFields.length).toBe(3); diff --git a/frontend/src/container/QueryBuilder/components/Query/Query.tsx b/frontend/src/container/QueryBuilder/components/Query/Query.tsx index 747198abfb..453cf063f8 100644 --- a/frontend/src/container/QueryBuilder/components/Query/Query.tsx +++ b/frontend/src/container/QueryBuilder/components/Query/Query.tsx @@ -23,6 +23,7 @@ import { import AggregateEveryFilter from 'container/QueryBuilder/filters/AggregateEveryFilter'; import LimitFilter from 'container/QueryBuilder/filters/LimitFilter/LimitFilter'; import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch'; +import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations'; // ** Hooks @@ -81,6 +82,10 @@ export const Query = memo(function Query({ entityVersion: version, }); + const isLogsExplorerPage = useMemo(() => pathname === ROUTES.LOGS_EXPLORER, [ + pathname, + ]); + const handleChangeAggregateEvery = useCallback( (value: IBuilderQuery['stepInterval']) => { handleChangeQueryData('stepInterval', value); @@ -452,11 +457,19 @@ export const Query = memo(function Query({ )}
- + {isLogsExplorerPage ? ( + + ) : ( + + )} diff --git a/frontend/src/container/QueryBuilder/filters/HavingFilter/HavingFilter.tsx b/frontend/src/container/QueryBuilder/filters/HavingFilter/HavingFilter.tsx index 7d11d018cc..3eab3e50ee 100644 --- a/frontend/src/container/QueryBuilder/filters/HavingFilter/HavingFilter.tsx +++ b/frontend/src/container/QueryBuilder/filters/HavingFilter/HavingFilter.tsx @@ -1,3 +1,4 @@ +import { Color } from '@signozhq/design-tokens'; import { Select } from 'antd'; import { ENTITY_VERSION_V4 } from 'constants/app'; // ** Constants @@ -34,6 +35,7 @@ export function HavingFilter({ const [currentFormValue, setCurrentFormValue] = useState( initialHavingValues, ); + const [errorMessage, setErrorMessage] = useState(null); const { isMulti } = useTagValidation( currentFormValue.op, @@ -198,6 +200,29 @@ export function HavingFilter({ resetChanges(); }; + const handleFocus = useCallback(() => { + setErrorMessage(null); + }, []); + + const handleBlur = useCallback((): void => { + if (searchText) { + const { columnName, op, value } = getHavingObject(searchText); + const isCompleteHavingClause = + columnName && op && value.every((v) => v !== ''); + + if (isCompleteHavingClause && isValidHavingValue(searchText)) { + setLocalValues((prev) => { + const updatedValues = [...prev, searchText]; + onChange(updatedValues.map(transformFromStringToHaving)); + return updatedValues; + }); + setSearchText(''); + } else { + setErrorMessage('Invalid HAVING clause'); + } + } + }, [searchText, onChange]); + useEffect(() => { parseSearchText(searchText); }, [searchText, parseSearchText]); @@ -209,28 +234,36 @@ export function HavingFilter({ const isMetricsDataSource = query.dataSource === DataSource.METRICS; return ( - + <> + + {errorMessage && ( +
{errorMessage}
+ )} + ); } diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.styles.scss b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.styles.scss index 1ca8bd7529..624546fed5 100644 --- a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.styles.scss +++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.styles.scss @@ -2,6 +2,10 @@ display: flex; gap: 4px; + .ant-select-dropdown { + padding: 0px; + } + .show-all-filters { .content { .rc-virtual-list-holder { @@ -231,16 +235,16 @@ } &.resource { - border: 1px solid rgba(242, 71, 105, 0.2); + border: 1px solid #4bcff920; .ant-typography { - color: var(--bg-sakura-400); - background: rgba(245, 108, 135, 0.1); + color: var(--bg-aqua-400); + background: #4bcff910; font-size: 14px; } .ant-tag-close-icon { - background: rgba(245, 108, 135, 0.1); + background: #4bcff910; } } &.tag { @@ -259,3 +263,110 @@ } } } + +.lightMode { + .query-builder-search-v2 { + .content { + .operator-for { + .operator-for-text { + color: var(--bg-ink-200); + } + + .operator-for-value { + background: rgba(255, 255, 255, 0.1); + color: var(--bg-ink-200); + } + } + + .value-for { + .value-for-text { + color: var(--bg-ink-200); + } + + .value-for-value { + background: rgba(255, 255, 255, 0.1); + color: var(--bg-ink-400); + } + } + .example-queries { + cursor: default; + .heading { + color: var(--bg-slate-50); + } + + .query-container { + .example-query { + background: var(--bg-vanilla-200); + color: var(--bg-ink-400); + } + + .example-query:hover { + color: var(--bg-ink-100); + } + } + } + } + + .keyboard-shortcuts { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-200); + + .icons { + border-top: 1.143px solid var(--bg-ink-200); + border-right: 1.143px solid var(--bg-ink-200); + border-bottom: 2.286px solid var(--bg-ink-200); + border-left: 1.143px solid var(--bg-ink-200); + background: var(--bg-vanilla-300); + } + + .keyboard-text { + color: var(--bg-ink-400); + } + + .navigate { + border-right: 1px solid #1d212d; + } + + .show-all-filter-items { + border-left: 1px solid #1d212d; + } + } + + .qb-search-bar-tokenised-tags { + .ant-tag { + border: 1px solid var(--bg-slate-100); + background: var(--bg-vanilla-300); + box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1); + + .ant-typography { + color: var(--bg-ink-100); + } + + &.resource { + border: 1px solid #4bcff920; + + .ant-typography { + color: var(--bg-aqua-400); + background: #4bcff910; + } + + .ant-tag-close-icon { + background: #4bcff910; + } + } + &.tag { + border: 1px solid rgba(189, 153, 121, 0.2); + + .ant-typography { + color: var(--bg-sienna-400); + background: rgba(189, 153, 121, 0.1); + } + + .ant-tag-close-icon { + background: rgba(189, 153, 121, 0.1); + } + } + } + } + } +} diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.tsx b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.tsx index 3ddeef85bc..0925c10d97 100644 --- a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.tsx +++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.tsx @@ -286,16 +286,62 @@ function QueryBuilderSearchV2( parsedValue = value; } if (currentState === DropdownState.ATTRIBUTE_KEY) { - setCurrentFilterItem((prev) => ({ - ...prev, - key: parsedValue as BaseAutocompleteData, - op: '', - value: '', - })); - setCurrentState(DropdownState.OPERATOR); - setSearchValue((parsedValue as BaseAutocompleteData)?.key); + // Case - convert abc def ghi type attribute keys directly to body contains abc def ghi + if ( + isObject(parsedValue) && + parsedValue?.key && + parsedValue?.key?.split(' ').length > 1 + ) { + setTags((prev) => [ + ...prev, + { + key: { + key: 'body', + dataType: DataTypes.String, + type: '', + isColumn: true, + isJSON: false, + // eslint-disable-next-line sonarjs/no-duplicate-string + id: 'body--string----true', + }, + op: OPERATORS.CONTAINS, + value: (parsedValue as BaseAutocompleteData)?.key, + }, + ]); + setCurrentFilterItem(undefined); + setSearchValue(''); + setCurrentState(DropdownState.ATTRIBUTE_KEY); + } else { + setCurrentFilterItem((prev) => ({ + ...prev, + key: parsedValue as BaseAutocompleteData, + op: '', + value: '', + })); + setCurrentState(DropdownState.OPERATOR); + setSearchValue((parsedValue as BaseAutocompleteData)?.key); + } } else if (currentState === DropdownState.OPERATOR) { - if (value === OPERATORS.EXISTS || value === OPERATORS.NOT_EXISTS) { + if (isEmpty(value) && currentFilterItem?.key?.key) { + setTags((prev) => [ + ...prev, + { + key: { + key: 'body', + dataType: DataTypes.String, + type: '', + isColumn: true, + isJSON: false, + id: 'body--string----true', + }, + op: OPERATORS.CONTAINS, + value: currentFilterItem?.key?.key, + }, + ]); + setCurrentFilterItem(undefined); + setSearchValue(''); + setCurrentState(DropdownState.ATTRIBUTE_KEY); + } else if (value === OPERATORS.EXISTS || value === OPERATORS.NOT_EXISTS) { setTags((prev) => [ ...prev, { @@ -399,6 +445,7 @@ function QueryBuilderSearchV2( whereClauseConfig?.customKey === 'body' && whereClauseConfig?.customOp === OPERATORS.CONTAINS ) { + // eslint-disable-next-line sonarjs/no-identical-functions setTags((prev) => [ ...prev, { @@ -519,19 +566,20 @@ function QueryBuilderSearchV2( setCurrentState(DropdownState.OPERATOR); } } - if (suggestionsData?.payload?.attributes?.length === 0) { + // again let's not auto select anything for the user + if (tagOperator) { setCurrentFilterItem({ key: { - key: tagKey.split(' ')[0], + key: tagKey, dataType: DataTypes.EMPTY, type: '', isColumn: false, isJSON: false, }, - op: '', + op: tagOperator, value: '', }); - setCurrentState(DropdownState.OPERATOR); + setCurrentState(DropdownState.ATTRIBUTE_VALUE); } } else if ( // Case 2 - if key is defined but the search text doesn't match with the set key, @@ -607,13 +655,32 @@ function QueryBuilderSearchV2( // the useEffect takes care of setting the dropdown values correctly on change of the current state useEffect(() => { if (currentState === DropdownState.ATTRIBUTE_KEY) { + const { tagKey } = getTagToken(searchValue); if (isLogsExplorerPage) { - setDropdownOptions( - suggestionsData?.payload?.attributes?.map((key) => ({ + // add the user typed option in the dropdown to select that and move ahead irrespective of the matches and all + setDropdownOptions([ + ...(!isEmpty(tagKey) && + !suggestionsData?.payload?.attributes?.some((val) => + isEqual(val.key, tagKey), + ) + ? [ + { + label: tagKey, + value: { + key: tagKey, + dataType: DataTypes.EMPTY, + type: '', + isColumn: false, + isJSON: false, + }, + }, + ] + : []), + ...(suggestionsData?.payload?.attributes?.map((key) => ({ label: key.key, value: key, - })) || [], - ); + })) || []), + ]); } else { setDropdownOptions( data?.payload?.attributeKeys?.map((key) => ({ @@ -643,12 +710,14 @@ function QueryBuilderSearchV2( op.label.startsWith(partialOperator.toLocaleUpperCase()), ); } + operatorOptions = [{ label: '', value: '' }, ...operatorOptions]; setDropdownOptions(operatorOptions); } else if (strippedKey.endsWith('[*]') && strippedKey.startsWith('body.')) { operatorOptions = [OPERATORS.HAS, OPERATORS.NHAS].map((operator) => ({ label: operator, value: operator, })); + operatorOptions = [{ label: '', value: '' }, ...operatorOptions]; setDropdownOptions(operatorOptions); } else { operatorOptions = QUERY_BUILDER_OPERATORS_BY_TYPES.universal.map( @@ -663,6 +732,7 @@ function QueryBuilderSearchV2( op.label.startsWith(partialOperator.toLocaleUpperCase()), ); } + operatorOptions = [{ label: '', value: '' }, ...operatorOptions]; setDropdownOptions(operatorOptions); } } @@ -729,7 +799,8 @@ function QueryBuilderSearchV2( }, [tags]); useEffect(() => { - if (!isEqual(query.filters.items, tags)) { + // convert the query and tags to same format before comparison + if (!isEqual(getInitTags(query), tags)) { setTags(getInitTags(query)); } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -769,7 +840,7 @@ function QueryBuilderSearchV2( ); const queryTags = useMemo( - () => tags.map((tag) => `${tag.key.key} ${tag.op} ${tag.value}`), + () => tags.map((tag) => `${tag.key?.key} ${tag.op} ${tag.value}`), [tags], ); diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/Suggestions.styles.scss b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/Suggestions.styles.scss index bd7ad36a5a..153f32e5ee 100644 --- a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/Suggestions.styles.scss +++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/Suggestions.styles.scss @@ -77,14 +77,14 @@ &.resource { border-radius: 50px; - background: rgba(245, 108, 135, 0.1) !important; - color: var(--bg-sakura-400) !important; + background: #4bcff910 !important; + color: var(--bg-aqua-400) !important; .dot { - background-color: var(--bg-sakura-400); + background-color: var(--bg-aqua-400); } .text { - color: var(--bg-sakura-400); + color: var(--bg-aqua-400); font-family: Inter; font-size: 12px; font-style: normal; @@ -168,3 +168,59 @@ } } } + +.lightMode { + .text { + color: var(--bg-ink-400); + } + + .option { + .container { + display: flex; + align-items: center; + justify-content: space-between; + + .right-section { + .data-type { + background: var(--bg-vanilla-300); + } + } + .option-meta-data-container { + display: flex; + gap: 8px; + } + } + + .container-without-tag { + .left { + .OPERATOR { + color: var(--bg-ink-400); + } + + .VALUE { + color: var(--bg-ink-400); + } + } + + .right { + .data-type { + background: var(--bg-vanilla-300); + } + } + } + } + .option:hover { + .container { + .left-section { + .value { + color: var(--bg-ink-100); + } + } + } + .container-without-tag { + .value { + color: var(--bg-ink-100); + } + } + } +} diff --git a/frontend/src/container/QueryTable/QueryTable.intefaces.ts b/frontend/src/container/QueryTable/QueryTable.intefaces.ts index 7576d796ec..254e4885e7 100644 --- a/frontend/src/container/QueryTable/QueryTable.intefaces.ts +++ b/frontend/src/container/QueryTable/QueryTable.intefaces.ts @@ -19,4 +19,5 @@ export type QueryTableProps = Omit< columns?: ColumnsType; dataSource?: RowData[]; sticky?: TableProps['sticky']; + searchTerm?: string; }; diff --git a/frontend/src/container/QueryTable/QueryTable.tsx b/frontend/src/container/QueryTable/QueryTable.tsx index 1786e5d4e3..e438070173 100644 --- a/frontend/src/container/QueryTable/QueryTable.tsx +++ b/frontend/src/container/QueryTable/QueryTable.tsx @@ -3,8 +3,11 @@ import './QueryTable.styles.scss'; import { ResizeTable } from 'components/ResizeTable'; import Download from 'container/Download/Download'; import { IServiceName } from 'container/MetricsApplication/Tabs/types'; -import { createTableColumnsFromQuery } from 'lib/query/createTableColumnsFromQuery'; -import { useMemo } from 'react'; +import { + createTableColumnsFromQuery, + RowData, +} from 'lib/query/createTableColumnsFromQuery'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { useParams } from 'react-router-dom'; import { QueryTableProps } from './QueryTable.intefaces'; @@ -20,6 +23,7 @@ export function QueryTable({ columns, dataSource, sticky, + searchTerm, ...props }: QueryTableProps): JSX.Element { const { isDownloadEnabled = false, fileName = '' } = downloadOption || {}; @@ -55,6 +59,27 @@ export function QueryTable({ hideOnSinglePage: true, }; + const [filterTable, setFilterTable] = useState(null); + + const onTableSearch = useCallback( + (value?: string): void => { + const filterTable = newDataSource.filter((o) => + Object.keys(o).some((k) => + String(o[k]) + .toLowerCase() + .includes(value?.toLowerCase() || ''), + ), + ); + + setFilterTable(filterTable); + }, + [newDataSource], + ); + + useEffect(() => { + onTableSearch(searchTerm); + }, [newDataSource, onTableSearch, searchTerm]); + return (
{isDownloadEnabled && ( @@ -69,7 +94,7 @@ export function QueryTable({ ({ + ...jest.requireActual('react-router-dom'), + useLocation: (): { pathname: string } => ({ + pathname: ``, + }), +})); + +// Mock useDashabord hook +jest.mock('providers/Dashboard/Dashboard', () => ({ + useDashboard: (): any => ({ + selectedDashboard: { + data: { + variables: [], + }, + }, + }), +})); + +describe('QueryTable -', () => { + it('should render correctly with all the data rows', () => { + const { container } = render(); + const tableRows = container.querySelectorAll('tr.ant-table-row'); + expect(tableRows.length).toBe(QueryTableProps.queryTableData.rows.length); + }); + + it('should render correctly with searchTerm', () => { + const { container } = render( + , + ); + const tableRows = container.querySelectorAll('tr.ant-table-row'); + expect(tableRows.length).toBe(3); + }); +}); + +const setSearchTerm = jest.fn(); +describe('WidgetHeader -', () => { + it('global search option should be working', () => { + const { getByText, getByTestId } = render( + , + ); + expect(getByText('Table - Panel')).toBeInTheDocument(); + const searchWidget = getByTestId('widget-header-search'); + expect(searchWidget).toBeInTheDocument(); + // click and open the search input + fireEvent.click(searchWidget); + // check if input is opened + const searchInput = getByTestId('widget-header-search-input'); + expect(searchInput).toBeInTheDocument(); + + // enter search term + fireEvent.change(searchInput, { target: { value: 'frontend' } }); + // check if search term is set + expect(setSearchTerm).toHaveBeenCalledWith('frontend'); + expect(searchInput).toHaveValue('frontend'); + }); + + it('global search should not be present for non-table panel', () => { + const { queryByTestId } = render( + , + ); + expect(queryByTestId('widget-header-search')).not.toBeInTheDocument(); + }); +}); diff --git a/frontend/src/container/QueryTable/__test__/mocks.ts b/frontend/src/container/QueryTable/__test__/mocks.ts new file mode 100644 index 0000000000..abdb7bcfe3 --- /dev/null +++ b/frontend/src/container/QueryTable/__test__/mocks.ts @@ -0,0 +1,797 @@ +/* eslint-disable sonarjs/no-duplicate-string */ +export const QueryTableProps: any = { + props: { + loading: false, + size: 'small', + }, + queryTableData: { + columns: [ + { + name: 'resource_host_name', + queryName: '', + isValueColumn: false, + }, + { + name: 'service_name', + queryName: '', + isValueColumn: false, + }, + { + name: 'operation', + queryName: '', + isValueColumn: false, + }, + { + name: 'A', + queryName: 'A', + isValueColumn: true, + }, + ], + rows: [ + { + data: { + A: 11.5, + operation: 'GetDriver', + resource_host_name: 'test-hs-name', + service_name: 'redis', + }, + }, + { + data: { + A: 10.13, + operation: 'HTTP GET', + resource_host_name: 'test-hs-name', + service_name: 'frontend', + }, + }, + { + data: { + A: 9.21, + operation: 'HTTP GET /route', + resource_host_name: 'test-hs-name', + service_name: 'route', + }, + }, + { + data: { + A: 9.21, + operation: 'HTTP GET: /route', + resource_host_name: 'test-hs-name', + service_name: 'frontend', + }, + }, + { + data: { + A: 0.92, + operation: 'HTTP GET: /customer', + resource_host_name: 'test-hs-name', + service_name: 'frontend', + }, + }, + { + data: { + A: 0.92, + operation: 'SQL SELECT', + resource_host_name: 'test-hs-name', + service_name: 'mysql', + }, + }, + { + data: { + A: 0.92, + operation: 'HTTP GET /customer', + resource_host_name: 'test-hs-name', + service_name: 'customer', + }, + }, + ], + }, + query: { + builder: { + queryData: [ + { + aggregateAttribute: { + dataType: 'float64', + id: 'signoz_calls_total--float64--Sum--true', + isColumn: true, + isJSON: false, + key: 'signoz_calls_total', + type: 'Sum', + }, + aggregateOperator: 'rate', + dataSource: 'metrics', + disabled: false, + expression: 'A', + filters: { + items: [], + op: 'AND', + }, + functions: [], + groupBy: [ + { + dataType: 'string', + id: 'resource_host_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'resource_host_name', + type: 'tag', + }, + { + dataType: 'string', + id: 'service_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'service_name', + type: 'tag', + }, + { + dataType: 'string', + id: 'operation--string--tag--false', + isColumn: false, + isJSON: false, + key: 'operation', + type: 'tag', + }, + ], + having: [], + legend: '', + limit: null, + orderBy: [], + queryName: 'A', + reduceTo: 'avg', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'rate', + }, + ], + queryFormulas: [], + }, + clickhouse_sql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + id: '1e08128f-c6a3-42ff-8033-4e38d291cf0a', + promql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + queryType: 'builder', + }, + columns: [ + { + dataIndex: 'resource_host_name', + title: 'resource_host_name', + width: 145, + }, + { + dataIndex: 'service_name', + title: 'service_name', + width: 145, + }, + { + dataIndex: 'operation', + title: 'operation', + width: 145, + }, + { + dataIndex: 'A', + title: 'A', + width: 145, + }, + ], + dataSource: [ + { + A: 11.5, + operation: 'GetDriver', + resource_host_name: 'test-hs-name', + service_name: 'redis', + }, + { + A: 10.13, + operation: 'HTTP GET', + resource_host_name: 'test-hs-name', + service_name: 'frontend', + }, + { + A: 9.21, + operation: 'HTTP GET /route', + resource_host_name: 'test-hs-name', + service_name: 'route', + }, + { + A: 9.21, + operation: 'HTTP GET: /route', + resource_host_name: 'test-hs-name', + service_name: 'frontend', + }, + { + A: 0.92, + operation: 'HTTP GET: /customer', + resource_host_name: 'test-hs-name', + service_name: 'frontend', + }, + { + A: 0.92, + operation: 'SQL SELECT', + resource_host_name: 'test-hs-name', + service_name: 'mysql', + }, + { + A: 0.92, + operation: 'HTTP GET /customer', + resource_host_name: 'test-hs-name', + service_name: 'customer', + }, + ], + sticky: true, + searchTerm: '', +}; + +export const WidgetHeaderProps: any = { + title: 'Table - Panel', + widget: { + bucketCount: 30, + bucketWidth: 0, + columnUnits: {}, + description: '', + fillSpans: false, + id: 'add65f0d-7662-4024-af51-da567759235d', + isStacked: false, + mergeAllActiveQueries: false, + nullZeroValues: 'zero', + opacity: '1', + panelTypes: 'table', + query: { + builder: { + queryData: [ + { + aggregateAttribute: { + dataType: 'float64', + id: 'signoz_calls_total--float64--Sum--true', + isColumn: true, + isJSON: false, + key: 'signoz_calls_total', + type: 'Sum', + }, + aggregateOperator: 'rate', + dataSource: 'metrics', + disabled: false, + expression: 'A', + filters: { + items: [], + op: 'AND', + }, + functions: [], + groupBy: [ + { + dataType: 'string', + id: 'resource_host_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'resource_host_name', + type: 'tag', + }, + { + dataType: 'string', + id: 'service_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'service_name', + type: 'tag', + }, + { + dataType: 'string', + id: 'operation--string--tag--false', + isColumn: false, + isJSON: false, + key: 'operation', + type: 'tag', + }, + ], + having: [], + legend: '', + limit: null, + orderBy: [], + queryName: 'A', + reduceTo: 'avg', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'rate', + }, + ], + queryFormulas: [], + }, + clickhouse_sql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + id: '1e08128f-c6a3-42ff-8033-4e38d291cf0a', + promql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + queryType: 'builder', + }, + selectedLogFields: [ + { + dataType: 'string', + name: 'body', + type: '', + }, + { + dataType: 'string', + name: 'timestamp', + type: '', + }, + ], + selectedTracesFields: [ + { + dataType: 'string', + id: 'serviceName--string--tag--true', + isColumn: true, + isJSON: false, + key: 'serviceName', + type: 'tag', + }, + { + dataType: 'string', + id: 'name--string--tag--true', + isColumn: true, + isJSON: false, + key: 'name', + type: 'tag', + }, + { + dataType: 'float64', + id: 'durationNano--float64--tag--true', + isColumn: true, + isJSON: false, + key: 'durationNano', + type: 'tag', + }, + { + dataType: 'string', + id: 'httpMethod--string--tag--true', + isColumn: true, + isJSON: false, + key: 'httpMethod', + type: 'tag', + }, + { + dataType: 'string', + id: 'responseStatusCode--string--tag--true', + isColumn: true, + isJSON: false, + key: 'responseStatusCode', + type: 'tag', + }, + ], + softMax: 0, + softMin: 0, + stackedBarChart: false, + thresholds: [], + timePreferance: 'GLOBAL_TIME', + title: 'Table - Panel', + yAxisUnit: 'none', + }, + parentHover: false, + queryResponse: { + status: 'success', + isLoading: false, + isSuccess: true, + isError: false, + isIdle: false, + data: { + statusCode: 200, + error: null, + message: 'success', + payload: { + status: 'success', + data: { + resultType: '', + result: [ + { + table: { + columns: [ + { + name: 'resource_host_name', + queryName: '', + isValueColumn: false, + }, + { + name: 'service_name', + queryName: '', + isValueColumn: false, + }, + { + name: 'operation', + queryName: '', + isValueColumn: false, + }, + { + name: 'A', + queryName: 'A', + isValueColumn: true, + }, + ], + rows: [ + { + data: { + A: 11.67, + operation: 'GetDriver', + resource_host_name: '4f6ec470feea', + service_name: 'redis', + }, + }, + { + data: { + A: 10.26, + operation: 'HTTP GET', + resource_host_name: '4f6ec470feea', + service_name: 'frontend', + }, + }, + { + data: { + A: 9.33, + operation: 'HTTP GET: /route', + resource_host_name: '4f6ec470feea', + service_name: 'frontend', + }, + }, + { + data: { + A: 9.33, + operation: 'HTTP GET /route', + resource_host_name: '4f6ec470feea', + service_name: 'route', + }, + }, + { + data: { + A: 0.93, + operation: 'FindDriverIDs', + resource_host_name: '4f6ec470feea', + service_name: 'redis', + }, + }, + { + data: { + A: 0.93, + operation: 'HTTP GET: /customer', + resource_host_name: '4f6ec470feea', + service_name: 'frontend', + }, + }, + { + data: { + A: 0.93, + operation: '/driver.DriverService/FindNearest', + resource_host_name: '4f6ec470feea', + service_name: 'driver', + }, + }, + { + data: { + A: 0.93, + operation: '/driver.DriverService/FindNearest', + resource_host_name: '4f6ec470feea', + service_name: 'frontend', + }, + }, + { + data: { + A: 0.93, + operation: 'SQL SELECT', + resource_host_name: '4f6ec470feea', + service_name: 'mysql', + }, + }, + { + data: { + A: 0.93, + operation: 'HTTP GET /customer', + resource_host_name: '4f6ec470feea', + service_name: 'customer', + }, + }, + { + data: { + A: 0.93, + operation: 'HTTP GET /dispatch', + resource_host_name: '4f6ec470feea', + service_name: 'frontend', + }, + }, + { + data: { + A: 0.21, + operation: 'check_request limit', + resource_host_name: '', + service_name: 'demo-app', + }, + }, + { + data: { + A: 0.21, + operation: 'authenticate_check_cache', + resource_host_name: '', + service_name: 'demo-app', + }, + }, + { + data: { + A: 0.21, + operation: 'authenticate_check_db', + resource_host_name: '', + service_name: 'demo-app', + }, + }, + { + data: { + A: 0.21, + operation: 'authenticate', + resource_host_name: '', + service_name: 'demo-app', + }, + }, + { + data: { + A: 0.21, + operation: 'check cart in cache', + resource_host_name: '', + service_name: 'demo-app', + }, + }, + { + data: { + A: 0.2, + operation: 'get_cart', + resource_host_name: '', + service_name: 'demo-app', + }, + }, + { + data: { + A: 0.2, + operation: 'check cart in db', + resource_host_name: '', + service_name: 'demo-app', + }, + }, + { + data: { + A: 0.2, + operation: 'home', + resource_host_name: '', + service_name: 'demo-app', + }, + }, + ], + }, + }, + ], + }, + }, + params: { + start: 1726669030000, + end: 1726670830000, + step: 60, + variables: {}, + formatForWeb: true, + compositeQuery: { + queryType: 'builder', + panelType: 'table', + fillGaps: false, + builderQueries: { + A: { + aggregateAttribute: { + dataType: 'float64', + id: 'signoz_calls_total--float64--Sum--true', + isColumn: true, + isJSON: false, + key: 'signoz_calls_total', + type: 'Sum', + }, + aggregateOperator: 'rate', + dataSource: 'metrics', + disabled: false, + expression: 'A', + filters: { + items: [], + op: 'AND', + }, + functions: [], + groupBy: [ + { + dataType: 'string', + id: 'resource_host_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'resource_host_name', + type: 'tag', + }, + { + dataType: 'string', + id: 'service_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'service_name', + type: 'tag', + }, + { + dataType: 'string', + id: 'operation--string--tag--false', + isColumn: false, + isJSON: false, + key: 'operation', + type: 'tag', + }, + ], + having: [], + legend: '', + limit: null, + orderBy: [], + queryName: 'A', + reduceTo: 'avg', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'rate', + }, + }, + }, + }, + }, + dataUpdatedAt: 1726670830710, + error: null, + errorUpdatedAt: 0, + failureCount: 0, + errorUpdateCount: 0, + isFetched: true, + isFetchedAfterMount: true, + isFetching: false, + isRefetching: false, + isLoadingError: false, + isPlaceholderData: false, + isPreviousData: false, + isRefetchError: false, + isStale: true, + }, + headerMenuList: ['view', 'clone', 'delete', 'edit'], + isWarning: false, + isFetchingResponse: false, + tableProcessedDataRef: { + current: [ + { + resource_host_name: '4f6ec470feea', + service_name: 'redis', + operation: 'GetDriver', + A: 11.67, + }, + { + resource_host_name: '4f6ec470feea', + service_name: 'frontend', + operation: 'HTTP GET', + A: 10.26, + }, + { + resource_host_name: '4f6ec470feea', + service_name: 'frontend', + operation: 'HTTP GET: /route', + A: 9.33, + }, + { + resource_host_name: '4f6ec470feea', + service_name: 'route', + operation: 'HTTP GET /route', + A: 9.33, + }, + { + resource_host_name: '4f6ec470feea', + service_name: 'redis', + operation: 'FindDriverIDs', + A: 0.93, + }, + { + resource_host_name: '4f6ec470feea', + service_name: 'frontend', + operation: 'HTTP GET: /customer', + A: 0.93, + }, + { + resource_host_name: '4f6ec470feea', + service_name: 'driver', + operation: '/driver.DriverService/FindNearest', + A: 0.93, + }, + { + resource_host_name: '4f6ec470feea', + service_name: 'frontend', + operation: '/driver.DriverService/FindNearest', + A: 0.93, + }, + { + resource_host_name: '4f6ec470feea', + service_name: 'mysql', + operation: 'SQL SELECT', + A: 0.93, + }, + { + resource_host_name: '4f6ec470feea', + service_name: 'customer', + operation: 'HTTP GET /customer', + A: 0.93, + }, + { + resource_host_name: '4f6ec470feea', + service_name: 'frontend', + operation: 'HTTP GET /dispatch', + A: 0.93, + }, + { + resource_host_name: '', + service_name: 'demo-app', + operation: 'check_request limit', + A: 0.21, + }, + { + resource_host_name: '', + service_name: 'demo-app', + operation: 'authenticate_check_cache', + A: 0.21, + }, + { + resource_host_name: '', + service_name: 'demo-app', + operation: 'authenticate_check_db', + A: 0.21, + }, + { + resource_host_name: '', + service_name: 'demo-app', + operation: 'authenticate', + A: 0.21, + }, + { + resource_host_name: '', + service_name: 'demo-app', + operation: 'check cart in cache', + A: 0.21, + }, + { + resource_host_name: '', + service_name: 'demo-app', + operation: 'get_cart', + A: 0.2, + }, + { + resource_host_name: '', + service_name: 'demo-app', + operation: 'check cart in db', + A: 0.2, + }, + { + resource_host_name: '', + service_name: 'demo-app', + operation: 'home', + A: 0.2, + }, + ], + }, +}; diff --git a/frontend/src/container/ServiceApplication/ServiceMetrics/ServiceMetricTable.tsx b/frontend/src/container/ServiceApplication/ServiceMetrics/ServiceMetricTable.tsx index 6430cc9c8f..1a3b99d6dd 100644 --- a/frontend/src/container/ServiceApplication/ServiceMetrics/ServiceMetricTable.tsx +++ b/frontend/src/container/ServiceApplication/ServiceMetrics/ServiceMetricTable.tsx @@ -92,9 +92,10 @@ function ServiceMetricTable({ return ( <> {RPS > MAX_RPS_LIMIT && ( - + {getText('rps_over_100')} + email )} diff --git a/frontend/src/container/ServiceApplication/ServiceTraces/ServiceTracesTable.tsx b/frontend/src/container/ServiceApplication/ServiceTraces/ServiceTracesTable.tsx index 6633b7a1aa..42d22e8980 100644 --- a/frontend/src/container/ServiceApplication/ServiceTraces/ServiceTracesTable.tsx +++ b/frontend/src/container/ServiceApplication/ServiceTraces/ServiceTracesTable.tsx @@ -49,10 +49,11 @@ function ServiceTraceTable({ return ( <> {RPS > MAX_RPS_LIMIT && ( - - + + {getText('rps_over_100')} - + email + )} diff --git a/frontend/src/container/SideNav/menuItems.tsx b/frontend/src/container/SideNav/menuItems.tsx index be694227a1..6d24b74c53 100644 --- a/frontend/src/container/SideNav/menuItems.tsx +++ b/frontend/src/container/SideNav/menuItems.tsx @@ -1,7 +1,6 @@ import { RocketOutlined } from '@ant-design/icons'; import ROUTES from 'constants/routes'; import { - AreaChart, BarChart2, BellDot, BugIcon, @@ -114,11 +113,6 @@ const menuItems: SidebarItem[] = [ icon: , isBeta: true, }, - { - key: ROUTES.USAGE_EXPLORER, - label: 'Usage Explorer', - icon: , - }, { key: ROUTES.BILLING, label: 'Billing', diff --git a/frontend/src/container/TracesExplorer/ListView/index.tsx b/frontend/src/container/TracesExplorer/ListView/index.tsx index 810ffb8241..c22623772b 100644 --- a/frontend/src/container/TracesExplorer/ListView/index.tsx +++ b/frontend/src/container/TracesExplorer/ListView/index.tsx @@ -14,9 +14,8 @@ import { Pagination } from 'hooks/queryPagination'; import useDragColumns from 'hooks/useDragColumns'; import { getDraggedColumns } from 'hooks/useDragColumns/utils'; import useUrlQueryData from 'hooks/useUrlQueryData'; -import history from 'lib/history'; import { RowData } from 'lib/query/createTableColumnsFromQuery'; -import { HTMLAttributes, memo, useCallback, useMemo } from 'react'; +import { memo, useCallback, useMemo } from 'react'; import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; import { DataSource } from 'types/common/queryBuilder'; @@ -25,7 +24,7 @@ import { GlobalReducer } from 'types/reducer/globalTime'; import { TracesLoading } from '../TraceLoading/TraceLoading'; import { defaultSelectedColumns, PER_PAGE_OPTIONS } from './configs'; import { Container, ErrorText, tableStyles } from './styles'; -import { getListColumns, getTraceLink, transformDataWithDate } from './utils'; +import { getListColumns, transformDataWithDate } from './utils'; interface ListViewProps { isFilterApplied: boolean; @@ -108,21 +107,6 @@ function ListView({ isFilterApplied }: ListViewProps): JSX.Element { [queryTableData], ); - const handleRow = useCallback( - (record: RowData): HTMLAttributes => ({ - onClick: (event): void => { - event.preventDefault(); - event.stopPropagation(); - if (event.metaKey || event.ctrlKey) { - window.open(getTraceLink(record), '_blank'); - } else { - history.push(getTraceLink(record)); - } - }, - }), - [], - ); - const handleDragColumn = useCallback( (fromIndex: number, toIndex: number) => onDragColumns(columns, fromIndex, toIndex), @@ -169,7 +153,6 @@ function ListView({ isFilterApplied }: ListViewProps): JSX.Element { style={tableStyles} dataSource={transformedQueryTableData} columns={columns} - onRow={handleRow} onDragColumn={handleDragColumn} /> )} diff --git a/frontend/src/container/TracesExplorer/ListView/utils.tsx b/frontend/src/container/TracesExplorer/ListView/utils.tsx index a6201436d1..dc0e3808ae 100644 --- a/frontend/src/container/TracesExplorer/ListView/utils.tsx +++ b/frontend/src/container/TracesExplorer/ListView/utils.tsx @@ -47,11 +47,11 @@ export const getListColumns = ( key: 'date', title: 'Timestamp', width: 145, - render: (item): JSX.Element => { + render: (value, item): JSX.Element => { const date = - typeof item === 'string' - ? dayjs(item).format('YYYY-MM-DD HH:mm:ss.SSS') - : dayjs(item / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS'); + typeof value === 'string' + ? dayjs(value).format('YYYY-MM-DD HH:mm:ss.SSS') + : dayjs(value / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS'); return ( {date} @@ -67,10 +67,10 @@ export const getListColumns = ( dataIndex: key, key: `${key}-${dataType}-${type}`, width: 145, - render: (value): JSX.Element => { + render: (value, item): JSX.Element => { if (value === '') { return ( - + N/A ); @@ -78,7 +78,7 @@ export const getListColumns = ( if (key === 'httpMethod' || key === 'responseStatusCode') { return ( - + {value} @@ -88,14 +88,14 @@ export const getListColumns = ( if (key === 'durationNano') { return ( - + {getMs(value)}ms ); } return ( - + {value} ); diff --git a/frontend/src/container/TracesTableComponent/TracesTableComponent.styles.scss b/frontend/src/container/TracesTableComponent/TracesTableComponent.styles.scss index 74e80f8764..c59bf3c5ad 100644 --- a/frontend/src/container/TracesTableComponent/TracesTableComponent.styles.scss +++ b/frontend/src/container/TracesTableComponent/TracesTableComponent.styles.scss @@ -52,6 +52,8 @@ height: 40px; justify-content: end; padding: 0 8px; + margin-top: 12px; + margin-bottom: 2px; } } diff --git a/frontend/src/hooks/logs/useActiveLog.ts b/frontend/src/hooks/logs/useActiveLog.ts index 0a968c4650..d7cc498f9f 100644 --- a/frontend/src/hooks/logs/useActiveLog.ts +++ b/frontend/src/hooks/logs/useActiveLog.ts @@ -1,6 +1,6 @@ import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys'; import { SOMETHING_WENT_WRONG } from 'constants/api'; -import { QueryBuilderKeys } from 'constants/queryBuilder'; +import { OPERATORS, QueryBuilderKeys } from 'constants/queryBuilder'; import ROUTES from 'constants/routes'; import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; @@ -24,6 +24,16 @@ import { v4 as uuid } from 'uuid'; import { UseActiveLog } from './types'; +export function getOldLogsOperatorFromNew(operator: string): string { + switch (operator) { + case OPERATORS['=']: + return OPERATORS.IN; + case OPERATORS['!=']: + return OPERATORS.NIN; + default: + return operator; + } +} export const useActiveLog = (): UseActiveLog => { const dispatch = useDispatch(); @@ -178,10 +188,11 @@ export const useActiveLog = (): UseActiveLog => { ); const onAddToQueryLogs = useCallback( (fieldKey: string, fieldValue: string, operator: string) => { + const newOperator = getOldLogsOperatorFromNew(operator); const updatedQueryString = getGeneratedFilterQueryString( fieldKey, fieldValue, - operator, + newOperator, queryString, ); diff --git a/frontend/src/hooks/queryBuilder/useFetchKeysAndValues.ts b/frontend/src/hooks/queryBuilder/useFetchKeysAndValues.ts index 6fd42175ad..7b99b9d250 100644 --- a/frontend/src/hooks/queryBuilder/useFetchKeysAndValues.ts +++ b/frontend/src/hooks/queryBuilder/useFetchKeysAndValues.ts @@ -70,7 +70,7 @@ export const useFetchKeysAndValues = ( const queryFiltersWithoutId = useMemo( () => ({ ...query.filters, - items: query.filters.items.map((item) => { + items: query.filters?.items?.map((item) => { const filterWithoutId = cloneDeep(item); unset(filterWithoutId, 'id'); return filterWithoutId; diff --git a/frontend/src/hooks/queryBuilder/useGetCompositeQueryParam.ts b/frontend/src/hooks/queryBuilder/useGetCompositeQueryParam.ts index 442531a15b..efef00022a 100644 --- a/frontend/src/hooks/queryBuilder/useGetCompositeQueryParam.ts +++ b/frontend/src/hooks/queryBuilder/useGetCompositeQueryParam.ts @@ -13,7 +13,11 @@ export const useGetCompositeQueryParam = (): Query | null => { try { if (!compositeQuery) return null; - parsedCompositeQuery = JSON.parse(decodeURIComponent(compositeQuery)); + // MDN reference - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#decoding_query_parameters_from_a_url + // MDN reference to support + characters using encoding - https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams#preserving_plus_signs add later + parsedCompositeQuery = JSON.parse( + decodeURIComponent(compositeQuery.replace(/\+/g, ' ')), + ); } catch (e) { parsedCompositeQuery = null; } diff --git a/frontend/src/index.html.ejs b/frontend/src/index.html.ejs index 8a4e407ec5..f77e50f2b2 100644 --- a/frontend/src/index.html.ejs +++ b/frontend/src/index.html.ejs @@ -114,25 +114,6 @@ })(); - -