From ab950135ffd7cd679ee417b9363cff07558ecdaf Mon Sep 17 00:00:00 2001 From: Yunus M Date: Wed, 14 Feb 2024 22:00:17 +0530 Subject: [PATCH 01/16] fix: disable cloud features for oss (#4551) --- frontend/src/utils/app.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/src/utils/app.ts b/frontend/src/utils/app.ts index d0b859d108..0ab9e6fca7 100644 --- a/frontend/src/utils/app.ts +++ b/frontend/src/utils/app.ts @@ -15,8 +15,6 @@ export function extractDomain(email: string): string { export const isCloudUser = (): boolean => { const { hostname } = window.location; - return true; - return hostname?.endsWith('signoz.cloud'); }; From aa9a3e93494f87822fe09f86348011fc07277da5 Mon Sep 17 00:00:00 2001 From: Yunus M Date: Wed, 14 Feb 2024 22:50:18 +0530 Subject: [PATCH 02/16] fix: remove duplicate settings tab (#4552) --- frontend/src/pages/Settings/utils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/pages/Settings/utils.ts b/frontend/src/pages/Settings/utils.ts index e453ee56a5..5d42f540f9 100644 --- a/frontend/src/pages/Settings/utils.ts +++ b/frontend/src/pages/Settings/utils.ts @@ -24,7 +24,6 @@ export const getRoutes = ( settings.push(...alertChannels(t)); } else { settings.push(...alertChannels(t)); - settings.push(...generalSettings(t)); } settings.push(...generalSettings(t)); From f734142419e928151a0f021d9febf7a2e6db5621 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Wed, 14 Feb 2024 23:33:19 +0530 Subject: [PATCH 03/16] chore: add ExponentialHistogram support for metrics v4 query range (#4525) --- .../otel-collector-config.yaml | 1 + .../otel-collector-config.yaml | 1 + .../app/metrics/v4/delta/time_series_test.go | 34 +++++++++++++++++++ .../app/metrics/v4/delta/timeseries.go | 24 ++++++++++++- .../app/metrics/v4/helpers/clauses.go | 34 ------------------- pkg/query-service/model/v3/v3.go | 11 ++++++ 6 files changed, 70 insertions(+), 35 deletions(-) diff --git a/deploy/docker-swarm/clickhouse-setup/otel-collector-config.yaml b/deploy/docker-swarm/clickhouse-setup/otel-collector-config.yaml index 8f13b6506f..f8a710d535 100644 --- a/deploy/docker-swarm/clickhouse-setup/otel-collector-config.yaml +++ b/deploy/docker-swarm/clickhouse-setup/otel-collector-config.yaml @@ -98,6 +98,7 @@ processors: latency_histogram_buckets: [100us, 1ms, 2ms, 6ms, 10ms, 50ms, 100ms, 250ms, 500ms, 1000ms, 1400ms, 2000ms, 5s, 10s, 20s, 40s, 60s ] dimensions_cache_size: 100000 aggregation_temporality: AGGREGATION_TEMPORALITY_DELTA + enable_exp_histogram: true dimensions: - name: service.namespace default: default diff --git a/deploy/docker/clickhouse-setup/otel-collector-config.yaml b/deploy/docker/clickhouse-setup/otel-collector-config.yaml index 8211364efb..d382a252e5 100644 --- a/deploy/docker/clickhouse-setup/otel-collector-config.yaml +++ b/deploy/docker/clickhouse-setup/otel-collector-config.yaml @@ -101,6 +101,7 @@ processors: latency_histogram_buckets: [100us, 1ms, 2ms, 6ms, 10ms, 50ms, 100ms, 250ms, 500ms, 1000ms, 1400ms, 2000ms, 5s, 10s, 20s, 40s, 60s ] dimensions_cache_size: 100000 aggregation_temporality: AGGREGATION_TEMPORALITY_DELTA + enable_exp_histogram: true dimensions: - name: service.namespace default: default diff --git a/pkg/query-service/app/metrics/v4/delta/time_series_test.go b/pkg/query-service/app/metrics/v4/delta/time_series_test.go index 0af2c91154..29c2340ec4 100644 --- a/pkg/query-service/app/metrics/v4/delta/time_series_test.go +++ b/pkg/query-service/app/metrics/v4/delta/time_series_test.go @@ -212,6 +212,40 @@ func TestPrepareTimeseriesQuery(t *testing.T) { end: 1701796780000, expectedQueryContains: "SELECT service_name, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, sum(value)/60 as value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'service_name') as service_name, fingerprint FROM signoz_metrics.time_series_v4 WHERE metric_name = 'http_requests' AND temporality = 'Delta' AND unix_milli >= 1701792000000 AND unix_milli < 1701796780000 AND like(JSONExtractString(labels, 'service_name'), '%payment_service%')) as filtered_time_series USING fingerprint WHERE metric_name = 'http_requests' AND unix_milli >= 1701794980000 AND unix_milli < 1701796780000 GROUP BY GROUPING SETS ( (service_name, ts), (service_name) ) ORDER BY service_name ASC, ts ASC", }, + { + name: "test time aggregation = rate, space aggregation percentile99, type = ExponentialHistogram", + builderQuery: &v3.BuilderQuery{ + QueryName: "A", + StepInterval: 60, + DataSource: v3.DataSourceMetrics, + AggregateAttribute: v3.AttributeKey{ + Key: "signoz_latency", + DataType: v3.AttributeKeyDataTypeFloat64, + Type: v3.AttributeKeyType(v3.MetricTypeExponentialHistogram), + IsColumn: true, + IsJSON: false, + }, + Temporality: v3.Delta, + Filters: &v3.FilterSet{ + Operator: "AND", + Items: []v3.FilterItem{}, + }, + GroupBy: []v3.AttributeKey{ + { + Key: "service_name", + DataType: v3.AttributeKeyDataTypeString, + Type: v3.AttributeKeyTypeTag, + }, + }, + Expression: "A", + Disabled: false, + TimeAggregation: v3.TimeAggregationRate, + SpaceAggregation: v3.SpaceAggregationPercentile99, + }, + start: 1701794980000, + end: 1701796780000, + expectedQueryContains: "SELECT service_name, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, quantilesDDMerge(0.01, 0.990000)(sketch)[1] as value FROM signoz_metrics.distributed_exp_hist INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'service_name') as service_name, fingerprint FROM signoz_metrics.time_series_v4 WHERE metric_name = 'signoz_latency' AND temporality = 'Delta' AND unix_milli >= 1701792000000 AND unix_milli < 1701796780000) as filtered_time_series USING fingerprint WHERE metric_name = 'signoz_latency' AND unix_milli >= 1701794980000 AND unix_milli < 1701796780000 GROUP BY GROUPING SETS ( (service_name, ts), (service_name) ) ORDER BY service_name ASC, ts ASC", + }, } for _, testCase := range testCases { diff --git a/pkg/query-service/app/metrics/v4/delta/timeseries.go b/pkg/query-service/app/metrics/v4/delta/timeseries.go index 03781dfcd1..365b09c56d 100644 --- a/pkg/query-service/app/metrics/v4/delta/timeseries.go +++ b/pkg/query-service/app/metrics/v4/delta/timeseries.go @@ -9,6 +9,11 @@ import ( "go.signoz.io/signoz/pkg/query-service/utils" ) +// TODO(srikanthccv): support multiple quantiles; see https://github.com/SigNoz/signoz/issues/4016#issuecomment-1838583305 +var ( + sketchFmt = "quantilesDDMerge(0.01, %f)(sketch)[1]" +) + // prepareTimeAggregationSubQuery builds the sub-query to be used for temporal aggregation func prepareTimeAggregationSubQuery(start, end, step int64, mq *v3.BuilderQuery) (string, error) { @@ -84,12 +89,16 @@ func prepareQueryOptimized(start, end, step int64, mq *v3.BuilderQuery) (string, samplesTableFilter := fmt.Sprintf("metric_name = %s AND unix_milli >= %d AND unix_milli < %d", utils.ClickHouseFormattedValue(mq.AggregateAttribute.Key), start, end) + var tableName string = constants.SIGNOZ_SAMPLES_V4_TABLENAME + if mq.AggregateAttribute.Type == v3.AttributeKeyType(v3.MetricTypeExponentialHistogram) { + tableName = "distributed_exp_hist" + } // Select the aggregate value for interval queryTmpl := "SELECT %s" + " toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL %d SECOND) as ts," + " %s as value" + - " FROM " + constants.SIGNOZ_METRIC_DBNAME + "." + constants.SIGNOZ_SAMPLES_V4_TABLENAME + + " FROM " + constants.SIGNOZ_METRIC_DBNAME + "." + tableName + " INNER JOIN" + " (%s) as filtered_time_series" + " USING fingerprint" + @@ -110,6 +119,13 @@ func prepareQueryOptimized(start, end, step int64, mq *v3.BuilderQuery) (string, case v3.SpaceAggregationMax: op := "max(value)" query = fmt.Sprintf(queryTmpl, selectLabels, step, op, timeSeriesSubQuery, groupBy, orderBy) + case v3.SpaceAggregationPercentile50, + v3.SpaceAggregationPercentile75, + v3.SpaceAggregationPercentile90, + v3.SpaceAggregationPercentile95, + v3.SpaceAggregationPercentile99: + op := fmt.Sprintf(sketchFmt, v3.GetPercentileFromOperator(mq.SpaceAggregation)) + query = fmt.Sprintf(queryTmpl, selectLabels, step, op, timeSeriesSubQuery, groupBy, orderBy) } return query, nil } @@ -178,6 +194,9 @@ func PrepareMetricQueryDeltaTimeSeries(start, end, step int64, mq *v3.BuilderQue // 4. time aggregation = max and space aggregation = max // - max of maxs is same as max of all values // +// 5. special case exphist, there is no need for per series/fingerprint aggregation +// we can directly use the quantilesDDMerge function +// // all of this is true only for delta metrics func canShortCircuit(mq *v3.BuilderQuery) bool { if (mq.TimeAggregation == v3.TimeAggregationRate || mq.TimeAggregation == v3.TimeAggregationIncrease) && mq.SpaceAggregation == v3.SpaceAggregationSum { @@ -192,5 +211,8 @@ func canShortCircuit(mq *v3.BuilderQuery) bool { if mq.TimeAggregation == v3.TimeAggregationMax && mq.SpaceAggregation == v3.SpaceAggregationMax { return true } + if mq.AggregateAttribute.Type == v3.AttributeKeyType(v3.MetricTypeExponentialHistogram) && v3.IsPercentileOperator(mq.SpaceAggregation) { + return true + } return false } diff --git a/pkg/query-service/app/metrics/v4/helpers/clauses.go b/pkg/query-service/app/metrics/v4/helpers/clauses.go index 8714df51da..06f4b13cea 100644 --- a/pkg/query-service/app/metrics/v4/helpers/clauses.go +++ b/pkg/query-service/app/metrics/v4/helpers/clauses.go @@ -37,17 +37,6 @@ func GroupByAttributeKeyTags(tags ...v3.AttributeKey) string { return strings.Join(groupTags, ", ") } -func GroupByAttributeKeyTagsWithoutLe(tags ...v3.AttributeKey) string { - groupTags := []string{} - for _, tag := range tags { - if tag.Key != "le" { - groupTags = append(groupTags, tag.Key) - } - } - groupTags = append(groupTags, "ts") - return strings.Join(groupTags, ", ") -} - // OrderByAttributeKeyTags returns a string of comma separated tags for order by clause // if the order is not specified, it defaults to ASC func OrderByAttributeKeyTags(items []v3.OrderBy, tags []v3.AttributeKey) string { @@ -71,29 +60,6 @@ func OrderByAttributeKeyTags(items []v3.OrderBy, tags []v3.AttributeKey) string return strings.Join(orderBy, ", ") } -func OrderByAttributeKeyTagsWithoutLe(items []v3.OrderBy, tags []v3.AttributeKey) string { - var orderBy []string - for _, tag := range tags { - if tag.Key != "le" { - found := false - for _, item := range items { - if item.ColumnName == tag.Key { - found = true - orderBy = append(orderBy, fmt.Sprintf("%s %s", item.ColumnName, item.Order)) - break - } - } - if !found { - orderBy = append(orderBy, fmt.Sprintf("%s ASC", tag.Key)) - } - } - } - - orderBy = append(orderBy, "ts ASC") - - return strings.Join(orderBy, ", ") -} - func SelectLabelsAny(tags []v3.AttributeKey) string { var selectLabelsAny []string for _, tag := range tags { diff --git a/pkg/query-service/model/v3/v3.go b/pkg/query-service/model/v3/v3.go index 010b0b41c1..c01660d6e7 100644 --- a/pkg/query-service/model/v3/v3.go +++ b/pkg/query-service/model/v3/v3.go @@ -489,6 +489,17 @@ func (t TimeAggregation) IsRateOperator() bool { } } +type MetricType string + +const ( + MetricTypeUnspecified MetricType = "" + MetricTypeSum MetricType = "Sum" + MetricTypeGauge MetricType = "Gauge" + MetricTypeHistogram MetricType = "Histogram" + MetricTypeSummary MetricType = "Summary" + MetricTypeExponentialHistogram MetricType = "ExponentialHistogram" +) + type SpaceAggregation string const ( From 633b551e5d0fc21792452a66ea586b2d9b28f806 Mon Sep 17 00:00:00 2001 From: Vikrant Gupta Date: Thu, 15 Feb 2024 14:13:05 +0530 Subject: [PATCH 04/16] fix: qb search not respecting the saved views panel type (#4554) --- frontend/src/container/LogsExplorerViews/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/container/LogsExplorerViews/index.tsx b/frontend/src/container/LogsExplorerViews/index.tsx index c37e621108..bf81cea482 100644 --- a/frontend/src/container/LogsExplorerViews/index.tsx +++ b/frontend/src/container/LogsExplorerViews/index.tsx @@ -395,7 +395,7 @@ function LogsExplorerViews({ handleSetConfig(defaultTo(panelTypes, PANEL_TYPES.LIST), DataSource.LOGS); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [handleSetConfig, panelTypes]); useEffect(() => { const currentParams = data?.params as Omit; From 4e7547983186dd0eb1e64e3b20d518f6bd59bfb9 Mon Sep 17 00:00:00 2001 From: Yunus M Date: Thu, 15 Feb 2024 14:38:21 +0530 Subject: [PATCH 05/16] feat: ui updates - traces explorer (#4555) --- .../LogsContextList.styles.scss | 30 ++++++------ .../LogsContextList/ShowButton.styles.scss | 46 +++++++++---------- .../container/LogsContextList/ShowButton.tsx | 2 +- .../TracesExplorer/Controls/styles.ts | 1 + .../TracesExplorer/TracesExplorer.styles.scss | 8 +++- frontend/src/pages/TracesExplorer/index.tsx | 2 +- .../TracesModulePage.styles.scss | 12 ++++- .../TracesModulePage/TracesModulePage.tsx | 2 +- 8 files changed, 58 insertions(+), 45 deletions(-) diff --git a/frontend/src/container/LogsContextList/LogsContextList.styles.scss b/frontend/src/container/LogsContextList/LogsContextList.styles.scss index 18bb285140..1b160a65ab 100644 --- a/frontend/src/container/LogsContextList/LogsContextList.styles.scss +++ b/frontend/src/container/LogsContextList/LogsContextList.styles.scss @@ -3,34 +3,30 @@ .show-more-button { position: absolute; - z-index: 1; opacity: 1; + z-index: 1; + + cursor: pointer; &.up { - top: 0; + top: -1px; + left: -1px; } &.down { - bottom: 0; + bottom: -1px; + left: -1px; + } + + &.disabled { + cursor: not-allowed; } } .virtuoso-list { &::-webkit-scrollbar { - width: 0.1rem; - height: 0.1rem; - } - } - - &.logs-context-list-asc { - .virtuoso-list { - padding-top: 16px; - } - } - - &.logs-context-list-desc { - .virtuoso-list { - padding-bottom: 16px; + width: 0.3rem; + height: 0.3rem; } } } diff --git a/frontend/src/container/LogsContextList/ShowButton.styles.scss b/frontend/src/container/LogsContextList/ShowButton.styles.scss index ee765f5c08..408847e10f 100644 --- a/frontend/src/container/LogsContextList/ShowButton.styles.scss +++ b/frontend/src/container/LogsContextList/ShowButton.styles.scss @@ -1,31 +1,31 @@ .show-more-button { - background-color: var(--bg-slate-400); - color: var(--bg-vanilla-100); - display: flex; - padding: 4px 8px; - align-items: center; - gap: 3px; - border: none; - margin: 0; + background-color: var(--bg-slate-400); + color: var(--bg-vanilla-100); + display: flex; + padding: 4px 8px; + align-items: center; + gap: 3px; + border: none; + margin: 0; } .show-more-button { - &.disabled { - background-color: var(--bg-slate-200); - color: var(--bg-vanilla-400); - } + &.disabled { + background-color: var(--bg-slate-200); + color: var(--bg-vanilla-400); + } } .lightMode { - .show-more-button { - background-color: var(--bg-vanilla-300); - color: var(--bg-slate-400); - } + .show-more-button { + background-color: var(--bg-vanilla-300); + color: var(--bg-slate-400); + } - .show-more-button { - &.disabled { - background-color: var(--bg-vanilla-300); - color: var(--bg-vanilla-400); - } - } -} \ No newline at end of file + .show-more-button { + &.disabled { + background-color: var(--bg-vanilla-300); + color: var(--bg-vanilla-400); + } + } +} diff --git a/frontend/src/container/LogsContextList/ShowButton.tsx b/frontend/src/container/LogsContextList/ShowButton.tsx index e687a5610d..814d5e8f97 100644 --- a/frontend/src/container/LogsContextList/ShowButton.tsx +++ b/frontend/src/container/LogsContextList/ShowButton.tsx @@ -41,7 +41,7 @@ function ShowButton({ onClick={onClick} icon={getIcons()} className={cx( - 'show-more-button', + 'show-more-button periscope-btn', order === ORDERBY_FILTERS.ASC ? 'up' : 'down', isDisabled && 'disabled', )} diff --git a/frontend/src/container/TracesExplorer/Controls/styles.ts b/frontend/src/container/TracesExplorer/Controls/styles.ts index f91bc43363..be74f2db18 100644 --- a/frontend/src/container/TracesExplorer/Controls/styles.ts +++ b/frontend/src/container/TracesExplorer/Controls/styles.ts @@ -5,4 +5,5 @@ export const Container = styled.div` align-items: center; justify-content: flex-end; gap: 0.5rem; + margin: 4px 0; `; diff --git a/frontend/src/pages/TracesExplorer/TracesExplorer.styles.scss b/frontend/src/pages/TracesExplorer/TracesExplorer.styles.scss index ae985deb74..5ab2bd07b2 100644 --- a/frontend/src/pages/TracesExplorer/TracesExplorer.styles.scss +++ b/frontend/src/pages/TracesExplorer/TracesExplorer.styles.scss @@ -2,6 +2,12 @@ display: flex; flex-direction: row-reverse; align-items: center; - margin: 1rem 0 0.5rem 0; + margin: 8px 16px; gap: 8px; } + +.traces-explorer-views { + .ant-tabs-tabpane { + padding: 0 8px; + } +} diff --git a/frontend/src/pages/TracesExplorer/index.tsx b/frontend/src/pages/TracesExplorer/index.tsx index 22d4b71d2e..3d39aab2cb 100644 --- a/frontend/src/pages/TracesExplorer/index.tsx +++ b/frontend/src/pages/TracesExplorer/index.tsx @@ -192,7 +192,7 @@ function TracesExplorer(): JSX.Element { - + - ; + ); } From 548c53195651e8518e893df76d00f96920be28c9 Mon Sep 17 00:00:00 2001 From: Vikrant Gupta Date: Thu, 15 Feb 2024 16:25:55 +0530 Subject: [PATCH 06/16] fix: dashboard panel light theme (#4556) * fix: dashboard panel light theme * fix: logs pipeline page crashing on opening context for stimulated logs * fix: logs pipeline page crashing on opening context for stimulated logs --- frontend/src/lib/logs/flatLogData.ts | 3 ++- frontend/src/styles.scss | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/frontend/src/lib/logs/flatLogData.ts b/frontend/src/lib/logs/flatLogData.ts index d0bf6cf5dd..dfb59b9c68 100644 --- a/frontend/src/lib/logs/flatLogData.ts +++ b/frontend/src/lib/logs/flatLogData.ts @@ -1,3 +1,4 @@ +import { defaultTo } from 'lodash-es'; import { ILog } from 'types/api/logs/log'; export function FlatLogData(log: ILog): Record { @@ -7,7 +8,7 @@ export function FlatLogData(log: ILog): Record { if (typeof log[key as never] !== 'object') { flattenLogObject[key] = log[key as never]; } else { - Object.keys(log[key as never]).forEach((childKey) => { + Object.keys(defaultTo(log[key as never], {})).forEach((childKey) => { flattenLogObject[childKey] = log[key as never][childKey]; }); } diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss index 2cb3ed419c..05f7c400ab 100644 --- a/frontend/src/styles.scss +++ b/frontend/src/styles.scss @@ -224,3 +224,21 @@ body { margin-left: 8px; margin-right: 8px; } + +.lightMode { + .ant-dropdown-menu { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-100); + box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2); + } + + .ant-dropdown-menu-item { + &:hover { + background-color: var(--bg-vanilla-300) !important; + + &.ant-dropdown-menu-item-danger { + background-color: var(--bg-cherry-500) !important; + } + } + } +} From d11c1eb4392b6a69a2e4c85c3841c91d3858aaa1 Mon Sep 17 00:00:00 2001 From: Vishal Sharma Date: Fri, 16 Feb 2024 12:46:33 +0530 Subject: [PATCH 07/16] feat: api management (#4557) * feat: api management * chore: address review comments and typos * chore: add sort and created by user object on create * chore: replace expiresAt with expiresInDays for request body --- ee/query-service/app/api/api.go | 7 +- ee/query-service/app/api/pat.go | 102 ++++++++++++++++++----- ee/query-service/app/server.go | 29 ++----- ee/query-service/auth/auth.go | 56 +++++++++++++ ee/query-service/dao/interface.go | 4 +- ee/query-service/dao/sqlite/modelDao.go | 71 ++++++++++++++++ ee/query-service/dao/sqlite/pat.go | 102 +++++++++++++++++++++-- ee/query-service/model/pat.go | 36 ++++++-- pkg/query-service/telemetry/telemetry.go | 7 ++ 9 files changed, 351 insertions(+), 63 deletions(-) create mode 100644 ee/query-service/auth/auth.go diff --git a/ee/query-service/app/api/api.go b/ee/query-service/app/api/api.go index 22f3ee67c2..32bb22435f 100644 --- a/ee/query-service/app/api/api.go +++ b/ee/query-service/app/api/api.go @@ -152,9 +152,10 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *baseapp.AuthMiddlew router.HandleFunc("/api/v2/metrics/query_range", am.ViewAccess(ah.queryRangeMetricsV2)).Methods(http.MethodPost) // PAT APIs - router.HandleFunc("/api/v1/pat", am.AdminAccess(ah.createPAT)).Methods(http.MethodPost) - router.HandleFunc("/api/v1/pat", am.AdminAccess(ah.getPATs)).Methods(http.MethodGet) - router.HandleFunc("/api/v1/pat/{id}", am.AdminAccess(ah.deletePAT)).Methods(http.MethodDelete) + router.HandleFunc("/api/v1/pats", am.AdminAccess(ah.createPAT)).Methods(http.MethodPost) + router.HandleFunc("/api/v1/pats", am.AdminAccess(ah.getPATs)).Methods(http.MethodGet) + router.HandleFunc("/api/v1/pats/{id}", am.AdminAccess(ah.updatePAT)).Methods(http.MethodPut) + router.HandleFunc("/api/v1/pats/{id}", am.AdminAccess(ah.revokePAT)).Methods(http.MethodDelete) router.HandleFunc("/api/v1/checkout", am.AdminAccess(ah.checkout)).Methods(http.MethodPost) router.HandleFunc("/api/v1/billing", am.AdminAccess(ah.getBilling)).Methods(http.MethodGet) diff --git a/ee/query-service/app/api/pat.go b/ee/query-service/app/api/pat.go index b0fcf073a4..49ed36f092 100644 --- a/ee/query-service/app/api/pat.go +++ b/ee/query-service/app/api/pat.go @@ -12,6 +12,7 @@ import ( "github.com/gorilla/mux" "go.signoz.io/signoz/ee/query-service/model" "go.signoz.io/signoz/pkg/query-service/auth" + baseconstants "go.signoz.io/signoz/pkg/query-service/constants" basemodel "go.signoz.io/signoz/pkg/query-service/model" "go.uber.org/zap" ) @@ -28,7 +29,7 @@ func generatePATToken() string { func (ah *APIHandler) createPAT(w http.ResponseWriter, r *http.Request) { ctx := context.Background() - req := model.PAT{} + req := model.CreatePATRequestBody{} if err := json.NewDecoder(r.Body).Decode(&req); err != nil { RespondError(w, model.BadRequest(err), nil) return @@ -41,30 +42,87 @@ func (ah *APIHandler) createPAT(w http.ResponseWriter, r *http.Request) { }, nil) return } - - // All the PATs are associated with the user creating the PAT. Hence, the permissions - // associated with the PAT is also equivalent to that of the user. - req.UserID = user.Id - req.CreatedAt = time.Now().Unix() - req.Token = generatePATToken() - - // default expiry is 30 days - if req.ExpiresAt == 0 { - req.ExpiresAt = time.Now().AddDate(0, 0, 30).Unix() + pat := model.PAT{ + Name: req.Name, + Role: req.Role, + ExpiresAt: req.ExpiresInDays, } - // max expiry is 1 year - if req.ExpiresAt > time.Now().AddDate(1, 0, 0).Unix() { - req.ExpiresAt = time.Now().AddDate(1, 0, 0).Unix() + err = validatePATRequest(pat) + if err != nil { + RespondError(w, model.BadRequest(err), nil) + return } - zap.S().Debugf("Got PAT request: %+v", req) + // All the PATs are associated with the user creating the PAT. + pat.UserID = user.Id + pat.CreatedAt = time.Now().Unix() + pat.UpdatedAt = time.Now().Unix() + pat.LastUsed = 0 + pat.Token = generatePATToken() + + if pat.ExpiresAt != 0 { + // convert expiresAt to unix timestamp from days + pat.ExpiresAt = time.Now().Unix() + (pat.ExpiresAt * 24 * 60 * 60) + } + + zap.S().Debugf("Got Create PAT request: %+v", pat) var apierr basemodel.BaseApiError - if req, apierr = ah.AppDao().CreatePAT(ctx, req); apierr != nil { + if pat, apierr = ah.AppDao().CreatePAT(ctx, pat); apierr != nil { RespondError(w, apierr, nil) return } - ah.Respond(w, &req) + ah.Respond(w, &pat) +} + +func validatePATRequest(req model.PAT) error { + if req.Role == "" || (req.Role != baseconstants.ViewerGroup && req.Role != baseconstants.EditorGroup && req.Role != baseconstants.AdminGroup) { + return fmt.Errorf("valid role is required") + } + if req.ExpiresAt < 0 { + return fmt.Errorf("valid expiresAt is required") + } + if req.Name == "" { + return fmt.Errorf("valid name is required") + } + return nil +} + +func (ah *APIHandler) updatePAT(w http.ResponseWriter, r *http.Request) { + ctx := context.Background() + + req := model.PAT{} + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + RespondError(w, model.BadRequest(err), nil) + return + } + + user, err := auth.GetUserFromRequest(r) + if err != nil { + RespondError(w, &model.ApiError{ + Typ: model.ErrorUnauthorized, + Err: err, + }, nil) + return + } + + err = validatePATRequest(req) + if err != nil { + RespondError(w, model.BadRequest(err), nil) + return + } + + req.UpdatedByUserID = user.Id + id := mux.Vars(r)["id"] + req.UpdatedAt = time.Now().Unix() + zap.S().Debugf("Got Update PAT request: %+v", req) + var apierr basemodel.BaseApiError + if apierr = ah.AppDao().UpdatePAT(ctx, req, id); apierr != nil { + RespondError(w, apierr, nil) + return + } + + ah.Respond(w, map[string]string{"data": "pat updated successfully"}) } func (ah *APIHandler) getPATs(w http.ResponseWriter, r *http.Request) { @@ -86,7 +144,7 @@ func (ah *APIHandler) getPATs(w http.ResponseWriter, r *http.Request) { ah.Respond(w, pats) } -func (ah *APIHandler) deletePAT(w http.ResponseWriter, r *http.Request) { +func (ah *APIHandler) revokePAT(w http.ResponseWriter, r *http.Request) { ctx := context.Background() id := mux.Vars(r)["id"] user, err := auth.GetUserFromRequest(r) @@ -105,14 +163,14 @@ func (ah *APIHandler) deletePAT(w http.ResponseWriter, r *http.Request) { if pat.UserID != user.Id { RespondError(w, &model.ApiError{ Typ: model.ErrorUnauthorized, - Err: fmt.Errorf("unauthorized PAT delete request"), + Err: fmt.Errorf("unauthorized PAT revoke request"), }, nil) return } - zap.S().Debugf("Delete PAT with id: %+v", id) - if apierr := ah.AppDao().DeletePAT(ctx, id); apierr != nil { + zap.S().Debugf("Revoke PAT with id: %+v", id) + if apierr := ah.AppDao().RevokePAT(ctx, id, user.Id); apierr != nil { RespondError(w, apierr, nil) return } - ah.Respond(w, map[string]string{"data": "pat deleted successfully"}) + ah.Respond(w, map[string]string{"data": "pat revoked successfully"}) } diff --git a/ee/query-service/app/server.go b/ee/query-service/app/server.go index ea0b0344ad..f8c7633417 100644 --- a/ee/query-service/app/server.go +++ b/ee/query-service/app/server.go @@ -20,10 +20,11 @@ import ( "github.com/soheilhy/cmux" "go.signoz.io/signoz/ee/query-service/app/api" "go.signoz.io/signoz/ee/query-service/app/db" + "go.signoz.io/signoz/ee/query-service/auth" "go.signoz.io/signoz/ee/query-service/constants" "go.signoz.io/signoz/ee/query-service/dao" "go.signoz.io/signoz/ee/query-service/interfaces" - "go.signoz.io/signoz/pkg/query-service/auth" + baseauth "go.signoz.io/signoz/pkg/query-service/auth" baseInterface "go.signoz.io/signoz/pkg/query-service/interfaces" v3 "go.signoz.io/signoz/pkg/query-service/model/v3" @@ -37,7 +38,6 @@ import ( "go.signoz.io/signoz/pkg/query-service/app/logparsingpipeline" "go.signoz.io/signoz/pkg/query-service/app/opamp" opAmpModel "go.signoz.io/signoz/pkg/query-service/app/opamp/model" - baseauth "go.signoz.io/signoz/pkg/query-service/auth" "go.signoz.io/signoz/pkg/query-service/cache" baseconst "go.signoz.io/signoz/pkg/query-service/constants" "go.signoz.io/signoz/pkg/query-service/healthcheck" @@ -304,25 +304,12 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e r := mux.NewRouter() + // add auth middleware getUserFromRequest := func(r *http.Request) (*basemodel.UserPayload, error) { - patToken := r.Header.Get("SIGNOZ-API-KEY") - if len(patToken) > 0 { - zap.S().Debugf("Received a non-zero length PAT token") - ctx := context.Background() - dao := apiHandler.AppDao() - - user, err := dao.GetUserByPAT(ctx, patToken) - if err == nil && user != nil { - zap.S().Debugf("Found valid PAT user: %+v", user) - return user, nil - } - if err != nil { - zap.S().Debugf("Error while getting user for PAT: %+v", err) - } - } - return baseauth.GetUserFromRequest(r) + return auth.GetUserFromRequest(r, apiHandler) } am := baseapp.NewAuthMiddleware(getUserFromRequest) + r.Use(setTimeoutMiddleware) r.Use(s.analyticsMiddleware) r.Use(loggingMiddleware) @@ -439,7 +426,7 @@ func extractQueryRangeV3Data(path string, r *http.Request) (map[string]interface telemetry.GetInstance().AddActiveLogsUser() } data["dataSources"] = dataSources - userEmail, err := auth.GetEmailFromJwt(r.Context()) + userEmail, err := baseauth.GetEmailFromJwt(r.Context()) if err == nil { telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_QUERY_RANGE_V3, data, userEmail, true) } @@ -463,7 +450,7 @@ func getActiveLogs(path string, r *http.Request) { func (s *Server) analyticsMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ctx := auth.AttachJwtToContext(r.Context(), r) + ctx := baseauth.AttachJwtToContext(r.Context(), r) r = r.WithContext(ctx) route := mux.CurrentRoute(r) path, _ := route.GetPathTemplate() @@ -482,7 +469,7 @@ func (s *Server) analyticsMiddleware(next http.Handler) http.Handler { } if _, ok := telemetry.EnabledPaths()[path]; ok { - userEmail, err := auth.GetEmailFromJwt(r.Context()) + userEmail, err := baseauth.GetEmailFromJwt(r.Context()) if err == nil { telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_PATH, data, userEmail) } diff --git a/ee/query-service/auth/auth.go b/ee/query-service/auth/auth.go new file mode 100644 index 0000000000..8c06384549 --- /dev/null +++ b/ee/query-service/auth/auth.go @@ -0,0 +1,56 @@ +package auth + +import ( + "context" + "fmt" + "net/http" + "time" + + "go.signoz.io/signoz/ee/query-service/app/api" + baseauth "go.signoz.io/signoz/pkg/query-service/auth" + basemodel "go.signoz.io/signoz/pkg/query-service/model" + "go.signoz.io/signoz/pkg/query-service/telemetry" + + "go.uber.org/zap" +) + +func GetUserFromRequest(r *http.Request, apiHandler *api.APIHandler) (*basemodel.UserPayload, error) { + patToken := r.Header.Get("SIGNOZ-API-KEY") + if len(patToken) > 0 { + zap.S().Debugf("Received a non-zero length PAT token") + ctx := context.Background() + dao := apiHandler.AppDao() + + pat, err := dao.GetPAT(ctx, patToken) + if err == nil && pat != nil { + zap.S().Debugf("Found valid PAT: %+v", pat) + if pat.ExpiresAt < time.Now().Unix() && pat.ExpiresAt != 0 { + zap.S().Debugf("PAT has expired: %+v", pat) + return nil, fmt.Errorf("PAT has expired") + } + group, apiErr := dao.GetGroupByName(ctx, pat.Role) + if apiErr != nil { + zap.S().Debugf("Error while getting group for PAT: %+v", apiErr) + return nil, apiErr + } + user, err := dao.GetUser(ctx, pat.UserID) + if err != nil { + zap.S().Debugf("Error while getting user for PAT: %+v", err) + return nil, err + } + telemetry.GetInstance().SetPatTokenUser() + dao.UpdatePATLastUsed(ctx, patToken, time.Now().Unix()) + user.User.GroupId = group.Id + user.User.Id = pat.Id + return &basemodel.UserPayload{ + User: user.User, + Role: pat.Role, + }, nil + } + if err != nil { + zap.S().Debugf("Error while getting user for PAT: %+v", err) + return nil, err + } + } + return baseauth.GetUserFromRequest(r) +} diff --git a/ee/query-service/dao/interface.go b/ee/query-service/dao/interface.go index 479ca56edc..78155bc23a 100644 --- a/ee/query-service/dao/interface.go +++ b/ee/query-service/dao/interface.go @@ -34,9 +34,11 @@ type ModelDao interface { GetDomainByEmail(ctx context.Context, email string) (*model.OrgDomain, basemodel.BaseApiError) CreatePAT(ctx context.Context, p model.PAT) (model.PAT, basemodel.BaseApiError) + UpdatePAT(ctx context.Context, p model.PAT, id string) (basemodel.BaseApiError) GetPAT(ctx context.Context, pat string) (*model.PAT, basemodel.BaseApiError) + UpdatePATLastUsed(ctx context.Context, pat string, lastUsed int64) basemodel.BaseApiError GetPATByID(ctx context.Context, id string) (*model.PAT, basemodel.BaseApiError) GetUserByPAT(ctx context.Context, token string) (*basemodel.UserPayload, basemodel.BaseApiError) ListPATs(ctx context.Context, userID string) ([]model.PAT, basemodel.BaseApiError) - DeletePAT(ctx context.Context, id string) basemodel.BaseApiError + RevokePAT(ctx context.Context, id string, userID string) basemodel.BaseApiError } diff --git a/ee/query-service/dao/sqlite/modelDao.go b/ee/query-service/dao/sqlite/modelDao.go index 3c195ea9bf..02b4367da0 100644 --- a/ee/query-service/dao/sqlite/modelDao.go +++ b/ee/query-service/dao/sqlite/modelDao.go @@ -7,6 +7,7 @@ import ( basedao "go.signoz.io/signoz/pkg/query-service/dao" basedsql "go.signoz.io/signoz/pkg/query-service/dao/sqlite" baseint "go.signoz.io/signoz/pkg/query-service/interfaces" + "go.uber.org/zap" ) type modelDao struct { @@ -28,6 +29,41 @@ func (m *modelDao) checkFeature(key string) error { return m.flags.CheckFeature(key) } +func columnExists(db *sqlx.DB, tableName, columnName string) bool { + query := fmt.Sprintf("PRAGMA table_info(%s);", tableName) + rows, err := db.Query(query) + if err != nil { + zap.L().Error("Failed to query table info", zap.Error(err)) + return false + } + defer rows.Close() + + var ( + cid int + name string + ctype string + notnull int + dflt_value *string + pk int + ) + for rows.Next() { + err := rows.Scan(&cid, &name, &ctype, ¬null, &dflt_value, &pk) + if err != nil { + zap.L().Error("Failed to scan table info", zap.Error(err)) + return false + } + if name == columnName { + return true + } + } + err = rows.Err() + if err != nil { + zap.L().Error("Failed to scan table info", zap.Error(err)) + return false + } + return false +} + // InitDB creates and extends base model DB repository func InitDB(dataSourceName string) (*modelDao, error) { dao, err := basedsql.InitDB(dataSourceName) @@ -51,11 +87,16 @@ func InitDB(dataSourceName string) (*modelDao, error) { ); CREATE TABLE IF NOT EXISTS personal_access_tokens ( id INTEGER PRIMARY KEY AUTOINCREMENT, + role TEXT NOT NULL, user_id TEXT NOT NULL, token TEXT NOT NULL UNIQUE, name TEXT NOT NULL, created_at INTEGER NOT NULL, expires_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + last_used INTEGER NOT NULL, + revoked BOOLEAN NOT NULL, + updated_by_user_id TEXT NOT NULL, FOREIGN KEY(user_id) REFERENCES users(id) ); ` @@ -65,6 +106,36 @@ func InitDB(dataSourceName string) (*modelDao, error) { return nil, fmt.Errorf("error in creating tables: %v", err.Error()) } + if !columnExists(m.DB(), "personal_access_tokens", "role") { + _, err = m.DB().Exec("ALTER TABLE personal_access_tokens ADD COLUMN role TEXT NOT NULL DEFAULT 'ADMIN';") + if err != nil { + return nil, fmt.Errorf("error in adding column: %v", err.Error()) + } + } + if !columnExists(m.DB(), "personal_access_tokens", "updated_at") { + _, err = m.DB().Exec("ALTER TABLE personal_access_tokens ADD COLUMN updated_at INTEGER NOT NULL DEFAULT 0;") + if err != nil { + return nil, fmt.Errorf("error in adding column: %v", err.Error()) + } + } + if !columnExists(m.DB(), "personal_access_tokens", "last_used") { + _, err = m.DB().Exec("ALTER TABLE personal_access_tokens ADD COLUMN last_used INTEGER NOT NULL DEFAULT 0;") + if err != nil { + return nil, fmt.Errorf("error in adding column: %v", err.Error()) + } + } + if !columnExists(m.DB(), "personal_access_tokens", "revoked") { + _, err = m.DB().Exec("ALTER TABLE personal_access_tokens ADD COLUMN revoked BOOLEAN NOT NULL DEFAULT FALSE;") + if err != nil { + return nil, fmt.Errorf("error in adding column: %v", err.Error()) + } + } + if !columnExists(m.DB(), "personal_access_tokens", "updated_by_user_id") { + _, err = m.DB().Exec("ALTER TABLE personal_access_tokens ADD COLUMN updated_by_user_id TEXT NOT NULL DEFAULT '';") + if err != nil { + return nil, fmt.Errorf("error in adding column: %v", err.Error()) + } + } return m, nil } diff --git a/ee/query-service/dao/sqlite/pat.go b/ee/query-service/dao/sqlite/pat.go index 5bd1b78a62..a1752ea238 100644 --- a/ee/query-service/dao/sqlite/pat.go +++ b/ee/query-service/dao/sqlite/pat.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "strconv" + "time" "go.signoz.io/signoz/ee/query-service/model" basemodel "go.signoz.io/signoz/pkg/query-service/model" @@ -12,12 +13,16 @@ import ( func (m *modelDao) CreatePAT(ctx context.Context, p model.PAT) (model.PAT, basemodel.BaseApiError) { result, err := m.DB().ExecContext(ctx, - "INSERT INTO personal_access_tokens (user_id, token, name, created_at, expires_at) VALUES ($1, $2, $3, $4, $5)", + "INSERT INTO personal_access_tokens (user_id, token, role, name, created_at, expires_at, updated_at, updated_by_user_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", p.UserID, p.Token, + p.Role, p.Name, p.CreatedAt, - p.ExpiresAt) + p.ExpiresAt, + p.UpdatedAt, + p.UpdatedByUserID, + ) if err != nil { zap.S().Errorf("Failed to insert PAT in db, err: %v", zap.Error(err)) return model.PAT{}, model.InternalError(fmt.Errorf("PAT insertion failed")) @@ -28,24 +33,102 @@ func (m *modelDao) CreatePAT(ctx context.Context, p model.PAT) (model.PAT, basem return model.PAT{}, model.InternalError(fmt.Errorf("PAT insertion failed")) } p.Id = strconv.Itoa(int(id)) + createdByUser, _ := m.GetUser(ctx, p.UserID) + if createdByUser == nil { + p.CreatedByUser = model.User{ + NotFound: true, + } + } else { + p.CreatedByUser = model.User{ + Id: createdByUser.Id, + Name: createdByUser.Name, + Email: createdByUser.Email, + CreatedAt: createdByUser.CreatedAt, + ProfilePictureURL: createdByUser.ProfilePictureURL, + NotFound: false, + } + } return p, nil } +func (m *modelDao) UpdatePAT(ctx context.Context, p model.PAT, id string) basemodel.BaseApiError { + _, err := m.DB().ExecContext(ctx, + "UPDATE personal_access_tokens SET role=$1, name=$2, updated_at=$3, updated_by_user_id=$4 WHERE id=$5 and revoked=false;", + p.Role, + p.Name, + p.UpdatedAt, + p.UpdatedByUserID, + id) + if err != nil { + zap.S().Errorf("Failed to update PAT in db, err: %v", zap.Error(err)) + return model.InternalError(fmt.Errorf("PAT update failed")) + } + return nil +} + +func (m *modelDao) UpdatePATLastUsed(ctx context.Context, token string, lastUsed int64) basemodel.BaseApiError { + _, err := m.DB().ExecContext(ctx, + "UPDATE personal_access_tokens SET last_used=$1 WHERE token=$2 and revoked=false;", + lastUsed, + token) + if err != nil { + zap.S().Errorf("Failed to update PAT last used in db, err: %v", zap.Error(err)) + return model.InternalError(fmt.Errorf("PAT last used update failed")) + } + return nil +} + func (m *modelDao) ListPATs(ctx context.Context, userID string) ([]model.PAT, basemodel.BaseApiError) { pats := []model.PAT{} - if err := m.DB().Select(&pats, `SELECT * FROM personal_access_tokens WHERE user_id=?;`, userID); err != nil { + if err := m.DB().Select(&pats, `SELECT * FROM personal_access_tokens WHERE user_id=? and revoked=false ORDER by updated_at DESC;`, userID); err != nil { zap.S().Errorf("Failed to fetch PATs for user: %s, err: %v", userID, zap.Error(err)) return nil, model.InternalError(fmt.Errorf("failed to fetch PATs")) } + for i := range pats { + createdByUser, _ := m.GetUser(ctx, pats[i].UserID) + if createdByUser == nil { + pats[i].CreatedByUser = model.User{ + NotFound: true, + } + } else { + pats[i].CreatedByUser = model.User{ + Id: createdByUser.Id, + Name: createdByUser.Name, + Email: createdByUser.Email, + CreatedAt: createdByUser.CreatedAt, + ProfilePictureURL: createdByUser.ProfilePictureURL, + NotFound: false, + } + } + + updatedByUser, _ := m.GetUser(ctx, pats[i].UpdatedByUserID) + if updatedByUser == nil { + pats[i].UpdatedByUser = model.User{ + NotFound: true, + } + } else { + pats[i].UpdatedByUser = model.User{ + Id: updatedByUser.Id, + Name: updatedByUser.Name, + Email: updatedByUser.Email, + CreatedAt: updatedByUser.CreatedAt, + ProfilePictureURL: updatedByUser.ProfilePictureURL, + NotFound: false, + } + } + } return pats, nil } -func (m *modelDao) DeletePAT(ctx context.Context, id string) basemodel.BaseApiError { - _, err := m.DB().ExecContext(ctx, `DELETE from personal_access_tokens where id=?;`, id) +func (m *modelDao) RevokePAT(ctx context.Context, id string, userID string) basemodel.BaseApiError { + updatedAt := time.Now().Unix() + _, err := m.DB().ExecContext(ctx, + "UPDATE personal_access_tokens SET revoked=true, updated_by_user_id = $1, updated_at=$2 WHERE id=$3", + userID, updatedAt, id) if err != nil { - zap.S().Errorf("Failed to delete PAT, err: %v", zap.Error(err)) - return model.InternalError(fmt.Errorf("failed to delete PAT")) + zap.S().Errorf("Failed to revoke PAT in db, err: %v", zap.Error(err)) + return model.InternalError(fmt.Errorf("PAT revoke failed")) } return nil } @@ -53,7 +136,7 @@ func (m *modelDao) DeletePAT(ctx context.Context, id string) basemodel.BaseApiEr func (m *modelDao) GetPAT(ctx context.Context, token string) (*model.PAT, basemodel.BaseApiError) { pats := []model.PAT{} - if err := m.DB().Select(&pats, `SELECT * FROM personal_access_tokens WHERE token=?;`, token); err != nil { + if err := m.DB().Select(&pats, `SELECT * FROM personal_access_tokens WHERE token=? and revoked=false;`, token); err != nil { return nil, model.InternalError(fmt.Errorf("failed to fetch PAT")) } @@ -70,7 +153,7 @@ func (m *modelDao) GetPAT(ctx context.Context, token string) (*model.PAT, basemo func (m *modelDao) GetPATByID(ctx context.Context, id string) (*model.PAT, basemodel.BaseApiError) { pats := []model.PAT{} - if err := m.DB().Select(&pats, `SELECT * FROM personal_access_tokens WHERE id=?;`, id); err != nil { + if err := m.DB().Select(&pats, `SELECT * FROM personal_access_tokens WHERE id=? and revoked=false;`, id); err != nil { return nil, model.InternalError(fmt.Errorf("failed to fetch PAT")) } @@ -84,6 +167,7 @@ func (m *modelDao) GetPATByID(ctx context.Context, id string) (*model.PAT, basem return &pats[0], nil } +// deprecated func (m *modelDao) GetUserByPAT(ctx context.Context, token string) (*basemodel.UserPayload, basemodel.BaseApiError) { users := []basemodel.UserPayload{} diff --git a/ee/query-service/model/pat.go b/ee/query-service/model/pat.go index f320d0be7c..ef683a08bf 100644 --- a/ee/query-service/model/pat.go +++ b/ee/query-service/model/pat.go @@ -1,10 +1,32 @@ package model -type PAT struct { - Id string `json:"id" db:"id"` - UserID string `json:"userId" db:"user_id"` - Token string `json:"token" db:"token"` - Name string `json:"name" db:"name"` - CreatedAt int64 `json:"createdAt" db:"created_at"` - ExpiresAt int64 `json:"expiresAt" db:"expires_at"` +type User struct { + Id string `json:"id" db:"id"` + Name string `json:"name" db:"name"` + Email string `json:"email" db:"email"` + CreatedAt int64 `json:"createdAt" db:"created_at"` + ProfilePictureURL string `json:"profilePictureURL" db:"profile_picture_url"` + NotFound bool `json:"notFound"` +} + +type CreatePATRequestBody struct { + Name string `json:"name"` + Role string `json:"role"` + ExpiresInDays int64 `json:"expiresInDays"` +} + +type PAT struct { + Id string `json:"id" db:"id"` + UserID string `json:"userId" db:"user_id"` + CreatedByUser User `json:"createdByUser"` + UpdatedByUser User `json:"updatedByUser"` + Token string `json:"token" db:"token"` + Role string `json:"role" db:"role"` + Name string `json:"name" db:"name"` + CreatedAt int64 `json:"createdAt" db:"created_at"` + ExpiresAt int64 `json:"expiresAt" db:"expires_at"` + UpdatedAt int64 `json:"updatedAt" db:"updated_at"` + LastUsed int64 `json:"lastUsed" db:"last_used"` + Revoked bool `json:"revoked" db:"revoked"` + UpdatedByUserID string `json:"updatedByUserId" db:"updated_by_user_id"` } diff --git a/pkg/query-service/telemetry/telemetry.go b/pkg/query-service/telemetry/telemetry.go index 55b9a8a72b..a840760ac2 100644 --- a/pkg/query-service/telemetry/telemetry.go +++ b/pkg/query-service/telemetry/telemetry.go @@ -152,6 +152,7 @@ type Telemetry struct { maxRandInt int rateLimits map[string]int8 activeUser map[string]int8 + patTokenUser bool countUsers int8 mutex sync.RWMutex } @@ -243,7 +244,9 @@ func createTelemetry() { "metricsTTLStatus": metricsTTL.Status, "tracesTTLStatus": traceTTL.Status, "logsTTLStatus": logsTTL.Status, + "patUser": telemetry.patTokenUser, } + telemetry.patTokenUser = false for key, value := range tsInfo { data[key] = value } @@ -346,6 +349,10 @@ func (a *Telemetry) SetUserEmail(email string) { a.userEmail = email } +func (a *Telemetry) SetPatTokenUser() { + a.patTokenUser = true +} + func (a *Telemetry) GetUserEmail() string { return a.userEmail } From b10f17de7801047c5ca037adbba52ef3fa774091 Mon Sep 17 00:00:00 2001 From: Vikrant Gupta Date: Fri, 16 Feb 2024 18:22:33 +0530 Subject: [PATCH 08/16] chore: Added jest cases for logs explorer page (#4553) * chore: base file for logs explorer jest test cases * chore: added base setup for logs explorer jest fixing the uplot/d3-interpolate/antd-config errors * chore: added test for rendering of logs explorer page without API calls * chore: added test for rendering of logs with API call * chore: used virutoso mock to render items on the screen * chore: used virutoso mock to render items on the screen * chore: update dummy data --- .../src/container/LogsExplorerList/index.tsx | 4 +- .../src/container/LogsExplorerViews/index.tsx | 3 + .../OptionsMenu/AddColumnField/index.tsx | 3 +- .../ToolbarActions/LeftToolbarActions.tsx | 6 +- .../TracesExplorer/ListView/utils.tsx | 3 +- .../__mockdata__/logs_query_range.ts | 45 ++++++ .../__tests__/LogsExplorer.test.tsx | 147 ++++++++++++++++++ 7 files changed, 203 insertions(+), 8 deletions(-) create mode 100644 frontend/src/mocks-server/__mockdata__/logs_query_range.ts create mode 100644 frontend/src/pages/LogsExplorer/__tests__/LogsExplorer.test.tsx diff --git a/frontend/src/container/LogsExplorerList/index.tsx b/frontend/src/container/LogsExplorerList/index.tsx index c864aa355a..c4bba5291b 100644 --- a/frontend/src/container/LogsExplorerList/index.tsx +++ b/frontend/src/container/LogsExplorerList/index.tsx @@ -173,7 +173,9 @@ function LogsExplorerList({ {!isLoading && !isError && logs.length > 0 && ( <> - {renderContent} + + {renderContent} + handleModeChange(PANEL_TYPES.LIST)} + data-testid="logs-list-view" > List view @@ -551,6 +552,7 @@ function LogsExplorerViews({ : 'tab' } onClick={(): void => handleModeChange(PANEL_TYPES.TIME_SERIES)} + data-testid="time-series-view" > Time series @@ -561,6 +563,7 @@ function LogsExplorerViews({ selectedPanelType === PANEL_TYPES.TABLE ? 'selected_view tab' : 'tab' } onClick={(): void => handleModeChange(PANEL_TYPES.TABLE)} + data-testid="table-view" > Table diff --git a/frontend/src/container/OptionsMenu/AddColumnField/index.tsx b/frontend/src/container/OptionsMenu/AddColumnField/index.tsx index 5b5382a016..e6f082e500 100644 --- a/frontend/src/container/OptionsMenu/AddColumnField/index.tsx +++ b/frontend/src/container/OptionsMenu/AddColumnField/index.tsx @@ -1,6 +1,5 @@ import { SearchOutlined } from '@ant-design/icons'; -import { Input, Spin } from 'antd'; -import Typography from 'antd/es/typography/Typography'; +import { Input, Spin, Typography } from 'antd'; import { useIsDarkMode } from 'hooks/useDarkMode'; import { useTranslation } from 'react-i18next'; diff --git a/frontend/src/container/QueryBuilder/components/ToolbarActions/LeftToolbarActions.tsx b/frontend/src/container/QueryBuilder/components/ToolbarActions/LeftToolbarActions.tsx index 7485c47563..fc88c7f59a 100644 --- a/frontend/src/container/QueryBuilder/components/ToolbarActions/LeftToolbarActions.tsx +++ b/frontend/src/container/QueryBuilder/components/ToolbarActions/LeftToolbarActions.tsx @@ -39,7 +39,7 @@ export default function LeftToolbarActions({ )} onClick={(): void => onChangeSelectedView(SELECTED_VIEWS.SEARCH)} > - + @@ -52,7 +52,7 @@ export default function LeftToolbarActions({ )} onClick={(): void => onChangeSelectedView(SELECTED_VIEWS.QUERY_BUILDER)} > - + @@ -66,7 +66,7 @@ export default function LeftToolbarActions({ )} onClick={(): void => onChangeSelectedView(SELECTED_VIEWS.CLICKHOUSE)} > - + )} diff --git a/frontend/src/container/TracesExplorer/ListView/utils.tsx b/frontend/src/container/TracesExplorer/ListView/utils.tsx index a94e26d216..6a28acfba9 100644 --- a/frontend/src/container/TracesExplorer/ListView/utils.tsx +++ b/frontend/src/container/TracesExplorer/ListView/utils.tsx @@ -1,6 +1,5 @@ -import { Tag } from 'antd'; +import { Tag, Typography } from 'antd'; import { ColumnsType } from 'antd/es/table'; -import Typography from 'antd/es/typography/Typography'; import ROUTES from 'constants/routes'; import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util'; import { formUrlParams } from 'container/TraceDetail/utils'; diff --git a/frontend/src/mocks-server/__mockdata__/logs_query_range.ts b/frontend/src/mocks-server/__mockdata__/logs_query_range.ts new file mode 100644 index 0000000000..3b67e48945 --- /dev/null +++ b/frontend/src/mocks-server/__mockdata__/logs_query_range.ts @@ -0,0 +1,45 @@ +export const logsQueryRangeSuccessResponse = { + status: 'success', + data: { + resultType: '', + result: [ + { + queryName: 'A', + series: null, + list: [ + { + timestamp: '2024-02-15T21:20:22Z', + data: { + attributes_bool: {}, + attributes_float64: {}, + attributes_int64: {}, + attributes_string: { + container_id: 'container_id', + container_name: 'container_name', + driver: 'driver', + eta: '2m0s', + location: 'frontend', + log_level: 'INFO', + message: 'Dispatch successful', + service: 'frontend', + span_id: 'span_id', + trace_id: 'span_id', + }, + body: + '2024-02-15T21:20:22.035Z\tINFO\tfrontend\tDispatch successful\t{"service": "frontend", "trace_id": "span_id", "span_id": "span_id", "driver": "driver", "eta": "2m0s"}', + id: 'id', + resources_string: { + 'container.name': 'container_name', + }, + severity_number: 0, + severity_text: '', + span_id: '', + trace_flags: 0, + trace_id: '', + }, + }, + ], + }, + ], + }, +}; diff --git a/frontend/src/pages/LogsExplorer/__tests__/LogsExplorer.test.tsx b/frontend/src/pages/LogsExplorer/__tests__/LogsExplorer.test.tsx new file mode 100644 index 0000000000..072bfe98ca --- /dev/null +++ b/frontend/src/pages/LogsExplorer/__tests__/LogsExplorer.test.tsx @@ -0,0 +1,147 @@ +import { render, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { logsQueryRangeSuccessResponse } from 'mocks-server/__mockdata__/logs_query_range'; +import { server } from 'mocks-server/server'; +import { rest } from 'msw'; +import { QueryBuilderProvider } from 'providers/QueryBuilder'; +import MockQueryClientProvider from 'providers/test/MockQueryClientProvider'; +import { I18nextProvider } from 'react-i18next'; +import { Provider } from 'react-redux'; +import { MemoryRouter } from 'react-router-dom'; +// https://virtuoso.dev/mocking-in-tests/ +import { VirtuosoMockContext } from 'react-virtuoso'; +import i18n from 'ReactI18'; +import store from 'store'; + +import LogsExplorer from '..'; + +const queryRangeURL = 'http://localhost/api/v3/query_range'; +// mocking the graph components in this test as this should be handled separately +jest.mock( + 'container/TimeSeriesView/TimeSeriesView', + () => + // eslint-disable-next-line func-names, @typescript-eslint/explicit-function-return-type, react/display-name + function () { + return
Time Series Chart
; + }, +); +jest.mock( + 'container/LogsExplorerChart', + () => + // eslint-disable-next-line func-names, @typescript-eslint/explicit-function-return-type, react/display-name + function () { + return
Histogram Chart
; + }, +); + +jest.mock('constants/panelTypes', () => ({ + AVAILABLE_EXPORT_PANEL_TYPES: ['graph', 'table'], +})); + +jest.mock('d3-interpolate', () => ({ + interpolate: jest.fn(), +})); + +describe('Logs Explorer Tests', () => { + test('Logs Explorer default view test without data', async () => { + const { + getByText, + getByRole, + queryByText, + getByTestId, + queryByTestId, + } = render( + + + + + + + + + + + , + ); + + // check the presence of histogram chart + expect(getByText('Histogram Chart')).toBeInTheDocument(); + + // toggle the chart and check it gets removed from the DOM + const histogramToggle = getByRole('switch'); + await userEvent.click(histogramToggle); + expect(queryByText('Histogram Chart')).not.toBeInTheDocument(); + + // check the presence of search bar and query builder and absence of clickhouse + const searchView = getByTestId('search-view'); + expect(searchView).toBeInTheDocument(); + const queryBuilderView = getByTestId('query-builder-view'); + expect(queryBuilderView).toBeInTheDocument(); + const clickhouseView = queryByTestId('clickhouse-view'); + expect(clickhouseView).not.toBeInTheDocument(); + + // check the presence of List View / Time Series View / Table View + const listView = getByTestId('logs-list-view'); + const timeSeriesView = getByTestId('time-series-view'); + const tableView = getByTestId('table-view'); + expect(listView).toBeInTheDocument(); + expect(timeSeriesView).toBeInTheDocument(); + expect(tableView).toBeInTheDocument(); + + // check the presence of old logs explorer CTA + const oldLogsCTA = getByText('Switch to Old Logs Explorer'); + expect(oldLogsCTA).toBeInTheDocument(); + }); + + test('Logs Explorer Page should render with data', async () => { + // mocking the query range API to return the logs + server.use( + rest.post(queryRangeURL, (req, res, ctx) => + res(ctx.status(200), ctx.json(logsQueryRangeSuccessResponse)), + ), + ); + const { queryByText, queryByTestId } = render( + + + + + + + + + + + + + , + ); + + // check for loading state to be not present + await waitFor(() => + expect( + queryByText( + `Just a bit of patience, just a little bit’s enough ⎯ we’re getting your logs!`, + ), + ).not.toBeInTheDocument(), + ); + + // check for no data state to not be present + await waitFor(() => + expect(queryByText('No logs yet.')).not.toBeInTheDocument(), + ); + + // check for the data container loaded + await waitFor(() => + expect(queryByTestId('logs-list-virtuoso')).toBeInTheDocument(), + ); + + // check for data being present in the UI + expect( + queryByText( + '2024-02-15T21:20:22.035Z INFO frontend Dispatch successful {"service": "frontend", "trace_id": "span_id", "span_id": "span_id", "driver": "driver", "eta": "2m0s"}', + ), + ).toBeInTheDocument(); + }); +}); From 0c59953cb5ae27e0280311e0a8dee3d4de874569 Mon Sep 17 00:00:00 2001 From: Yunus M Date: Sat, 17 Feb 2024 14:59:49 +0530 Subject: [PATCH 09/16] feat: open left nav items in new tab on cmd ctrl click (#4561) --- frontend/src/AppRoutes/Private.tsx | 4 +- .../container/LogDetailedView/TableView.tsx | 25 ++-- .../src/container/LogsError/LogsError.tsx | 17 ++- frontend/src/container/NoLogs/NoLogs.tsx | 8 +- .../SideNav/NavItem/NavItem.styles.scss | 1 + .../src/container/SideNav/NavItem/NavItem.tsx | 7 +- frontend/src/container/SideNav/SideNav.tsx | 117 ++++++++++++------ 7 files changed, 128 insertions(+), 51 deletions(-) diff --git a/frontend/src/AppRoutes/Private.tsx b/frontend/src/AppRoutes/Private.tsx index aafaa932af..e409e3e3e6 100644 --- a/frontend/src/AppRoutes/Private.tsx +++ b/frontend/src/AppRoutes/Private.tsx @@ -20,7 +20,7 @@ import { UPDATE_USER_IS_FETCH } from 'types/actions/app'; import AppReducer from 'types/reducer/app'; import { routePermission } from 'utils/permission'; -import routes from './routes'; +import routes, { LIST_LICENSES } from './routes'; import afterLogin from './utils'; function PrivateRoute({ children }: PrivateRouteProps): JSX.Element { @@ -29,7 +29,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element { const mapRoutes = useMemo( () => new Map( - routes.map((e) => { + [...routes, LIST_LICENSES].map((e) => { const currentPath = matchPath(pathname, { path: e.path, }); diff --git a/frontend/src/container/LogDetailedView/TableView.tsx b/frontend/src/container/LogDetailedView/TableView.tsx index 29224e65ae..21774291f5 100644 --- a/frontend/src/container/LogDetailedView/TableView.tsx +++ b/frontend/src/container/LogDetailedView/TableView.tsx @@ -100,7 +100,10 @@ function TableView({ value: JSON.stringify(flattenLogData[key]), })); - const onTraceHandler = (record: DataType) => (): void => { + const onTraceHandler = ( + record: DataType, + event: React.MouseEvent, + ) => (): void => { if (flattenLogData === null) return; const traceId = flattenLogData[record.field]; @@ -119,7 +122,12 @@ function TableView({ const route = spanId ? `${basePath}?spanId=${spanId}` : basePath; - history.push(route); + if (event.ctrlKey || event.metaKey) { + // open the trace in new tab + window.open(route, '_blank'); + } else { + history.push(route); + } } }; @@ -148,17 +156,20 @@ function TableView({ {traceId && ( -
, + ): void => { + onTraceHandler(record, event); + }} > -
+
)} diff --git a/frontend/src/container/LogsError/LogsError.tsx b/frontend/src/container/LogsError/LogsError.tsx index 0b3c9f501d..6143674c6b 100644 --- a/frontend/src/container/LogsError/LogsError.tsx +++ b/frontend/src/container/LogsError/LogsError.tsx @@ -1,9 +1,20 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ import './LogsError.styles.scss'; import { Typography } from 'antd'; +import history from 'lib/history'; import { ArrowRight } from 'lucide-react'; +import { isCloudUser } from 'utils/app'; export default function LogsError(): JSX.Element { + const handleContactSupport = (): void => { + if (isCloudUser()) { + history.push('/support'); + } else { + window.open('https://signoz.io/slack', '_blank'); + } + }; return (
@@ -16,10 +27,12 @@ export default function LogsError(): JSX.Element { Aw snap :/ Something went wrong. Please try again or contact support. -
+ +
Contact Support + -
+
); diff --git a/frontend/src/container/NoLogs/NoLogs.tsx b/frontend/src/container/NoLogs/NoLogs.tsx index df934b7bcc..1274bdb20a 100644 --- a/frontend/src/container/NoLogs/NoLogs.tsx +++ b/frontend/src/container/NoLogs/NoLogs.tsx @@ -9,13 +9,17 @@ export default function NoLogs(): JSX.Element {
eyes emoji - No logs yet.{' '} + No logs yet. When we receive logs, they would show up here - + Sending Logs to SigNoz
diff --git a/frontend/src/container/SideNav/NavItem/NavItem.styles.scss b/frontend/src/container/SideNav/NavItem/NavItem.styles.scss index f182a1df6d..fe5952d400 100644 --- a/frontend/src/container/SideNav/NavItem/NavItem.styles.scss +++ b/frontend/src/container/SideNav/NavItem/NavItem.styles.scss @@ -7,6 +7,7 @@ height: 36px; margin-bottom: 4px; + cursor: pointer; &.active { .nav-item-active-marker { diff --git a/frontend/src/container/SideNav/NavItem/NavItem.tsx b/frontend/src/container/SideNav/NavItem/NavItem.tsx index 0ec6127da1..4171f2f4f5 100644 --- a/frontend/src/container/SideNav/NavItem/NavItem.tsx +++ b/frontend/src/container/SideNav/NavItem/NavItem.tsx @@ -16,13 +16,16 @@ export default function NavItem({ isCollapsed: boolean; item: SidebarItem; isActive: boolean; - onClick: () => void; + onClick: (event: React.MouseEvent) => void; }): JSX.Element { const { label, icon } = item; return ( -
+
onClick(event)} + >
{icon}
diff --git a/frontend/src/container/SideNav/SideNav.tsx b/frontend/src/container/SideNav/SideNav.tsx index a98d9eacd9..665a406710 100644 --- a/frontend/src/container/SideNav/SideNav.tsx +++ b/frontend/src/container/SideNav/SideNav.tsx @@ -19,7 +19,7 @@ import { RocketIcon, UserCircle, } from 'lucide-react'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { MouseEvent, useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; import { useLocation } from 'react-router-dom'; @@ -40,7 +40,7 @@ import defaultMenuItems, { trySignozCloudMenuItem, } from './menuItems'; import NavItem from './NavItem/NavItem'; -import { SecondaryMenuItemKey } from './sideNav.types'; +import { SecondaryMenuItemKey, SidebarItem } from './sideNav.types'; import { getActiveMenuKeyFromPath } from './sideNav.utils'; interface UserManagementMenuItems { @@ -88,10 +88,6 @@ function SideNav({ window.open('https://signoz.io/slack', '_blank'); }; - const onClickVersionHandler = (): void => { - history.push(ROUTES.VERSION); - }; - const isLatestVersion = checkVersionState(currentVersion, latestVersion); const [inviteMembers] = useComponentPermission(['invite_members'], role); @@ -164,23 +160,49 @@ function SideNav({ ); }; - const onClickShortcuts = (): void => { - history.push(`/shortcuts`); + const isCtrlMetaKey = (e: MouseEvent): boolean => e.ctrlKey || e.metaKey; + + const openInNewTab = (path: string): void => { + window.open(path, '_blank'); }; - const onClickGetStarted = (): void => { - history.push(`/get-started`); + const onClickShortcuts = (e: MouseEvent): void => { + if (isCtrlMetaKey(e)) { + openInNewTab('/shortcuts'); + } else { + history.push(`/shortcuts`); + } + }; + + const onClickGetStarted = (event: MouseEvent): void => { + if (isCtrlMetaKey(event)) { + openInNewTab('/get-started'); + } else { + history.push(`/get-started`); + } + }; + + const onClickVersionHandler = (event: MouseEvent): void => { + if (isCtrlMetaKey(event)) { + openInNewTab(ROUTES.VERSION); + } else { + history.push(ROUTES.VERSION); + } }; const onClickHandler = useCallback( - (key: string) => { + (key: string, event: MouseEvent | null) => { const params = new URLSearchParams(search); const availableParams = routeConfig[key]; const queryString = getQueryString(availableParams || [], params); if (pathname !== key) { - history.push(`${key}?${queryString.join('&')}`); + if (event && isCtrlMetaKey(event)) { + openInNewTab(`${key}?${queryString.join('&')}`); + } else { + history.push(`${key}?${queryString.join('&')}`); + } } }, [pathname, search], @@ -220,16 +242,19 @@ function SideNav({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentVersion, latestVersion]); - const handleUserManagentMenuItemClick = (key: string): void => { + const handleUserManagentMenuItemClick = ( + key: string, + event: MouseEvent, + ): void => { switch (key) { case SecondaryMenuItemKey.Slack: onClickSlackHandler(); break; case SecondaryMenuItemKey.Version: - onClickVersionHandler(); + onClickVersionHandler(event); break; default: - onClickHandler(key); + onClickHandler(key, event); break; } }; @@ -255,29 +280,41 @@ function SideNav({ ? ROUTES.ORG_SETTINGS : ROUTES.SETTINGS; + const handleMenuItemClick = (event: MouseEvent, item: SidebarItem): void => { + if (item.key === ROUTES.SETTINGS) { + if (isCtrlMetaKey(event)) { + openInNewTab(settingsRoute); + } else { + history.push(settingsRoute); + } + } else if (item) { + onClickHandler(item?.key as string, event); + } + }; + useEffect(() => { registerShortcut(GlobalShortcuts.SidebarCollapse, onCollapse); registerShortcut(GlobalShortcuts.NavigateToServices, () => - onClickHandler(ROUTES.APPLICATION), + onClickHandler(ROUTES.APPLICATION, null), ); registerShortcut(GlobalShortcuts.NavigateToTraces, () => - onClickHandler(ROUTES.TRACE), + onClickHandler(ROUTES.TRACE, null), ); registerShortcut(GlobalShortcuts.NavigateToLogs, () => - onClickHandler(ROUTES.LOGS), + onClickHandler(ROUTES.LOGS, null), ); registerShortcut(GlobalShortcuts.NavigateToDashboards, () => - onClickHandler(ROUTES.ALL_DASHBOARD), + onClickHandler(ROUTES.ALL_DASHBOARD, null), ); registerShortcut(GlobalShortcuts.NavigateToAlerts, () => - onClickHandler(ROUTES.LIST_ALL_ALERT), + onClickHandler(ROUTES.LIST_ALL_ALERT, null), ); registerShortcut(GlobalShortcuts.NavigateToExceptions, () => - onClickHandler(ROUTES.ALL_ERROR), + onClickHandler(ROUTES.ALL_ERROR, null), ); return (): void => { @@ -297,9 +334,9 @@ function SideNav({
{ + onClick={(event: MouseEvent): void => { // Current home page - onClickHandler(ROUTES.APPLICATION); + onClickHandler(ROUTES.APPLICATION, event); }} > SigNoz @@ -314,7 +351,12 @@ function SideNav({ {isCloudUserVal && (
-
+
+ ), + children: ( +
+ {APIKey?.createdByUser && ( + + Creator + + + {APIKey?.createdByUser?.name?.substring(0, 1)} + + + {APIKey.createdByUser?.name} + +
{APIKey.createdByUser?.email}
+ +
+ )} + + Created on + + {createdOn} + + + {updatedOn && ( + + Updated on + + {updatedOn} + + + )} + + + Expires on + + {expiresOn} + + +
+ ), + }, + ]; + + return ( +
+ + +
+
+ + Last used + {formattedDateAndTime} +
+ {expiresIn <= EXPIRATION_WITHIN_SEVEN_DAYS && ( +
+ Expires in {expiresIn} Days +
+ )} +
+
+ ); + }, + }, + ]; + + return ( +
+
+
+ API Keys + + Create and manage access keys for the SigNoz API + +
+ +
+ } + value={searchValue} + onChange={handleSearch} + /> + + +
+ + + `${range[0]}-${range[1]} of ${total} API Keys`, + }} + /> + + + {/* Delete Key Modal */} + Delete key} + open={isDeleteModalOpen} + closable + afterClose={handleModalClose} + onCancel={hideDeleteViewModal} + destroyOnClose + footer={[ + , + , + ]} + > + + {t('delete_confirm_message', { + keyName: activeAPIKey?.name, + })} + + + + {/* Edit Key Modal */} + } + > + Cancel + , + , + ]} + > +
+ + + + + + + + +
+ Admin +
+
+ +
+ Editor +
+
+ +
+ Viewer +
+
+
+
+
+ +
+ + {/* Create New Key Modal */} + } + > + Copy key and close + , + ] + : [ + , + , + ] + } + > + {!showNewAPIKeyDetails && ( +
+ + + + + + + + +
+ Admin +
+
+ +
+ Editor +
+
+ +
+ Viewer +
+
+
+
+
+ +
+ + {!query.builder.queryData[0].limit && ( +
+ +
+ )} + + + + ); +} + +export type LogsPanelComponentProps = { + selectedLogsFields: Widgets['selectedLogFields']; + query: Query; + selectedTime?: timePreferance; +}; + +LogsPanelComponent.defaultProps = { + selectedTime: undefined, +}; + +export default LogsPanelComponent; diff --git a/frontend/src/container/LogsPanelTable/utils.tsx b/frontend/src/container/LogsPanelTable/utils.tsx new file mode 100644 index 0000000000..46701e763b --- /dev/null +++ b/frontend/src/container/LogsPanelTable/utils.tsx @@ -0,0 +1,38 @@ +import { ColumnsType } from 'antd/es/table'; +import { Typography } from 'antd/lib'; +// import Typography from 'antd/es/typography/Typography'; +import { RowData } from 'lib/query/createTableColumnsFromQuery'; +import { ReactNode } from 'react'; +import { Widgets } from 'types/api/dashboard/getAll'; +import { IField } from 'types/api/logs/fields'; + +export const getLogPanelColumnsList = ( + selectedLogFields: Widgets['selectedLogFields'], +): ColumnsType => { + const initialColumns: ColumnsType = []; + + const columns: ColumnsType = + selectedLogFields?.map((field: IField) => { + const { name } = field; + return { + title: name, + dataIndex: name, + key: name, + width: name === 'body' ? 350 : 100, + render: (value: ReactNode): JSX.Element => { + if (name === 'body') { + return ( + + {value} + + ); + } + + return {value}; + }, + responsive: ['md'], + }; + }) || []; + + return [...initialColumns, ...columns]; +}; diff --git a/frontend/src/container/MetricsApplication/MetricsApplication.factory.ts b/frontend/src/container/MetricsApplication/MetricsApplication.factory.ts index 20becbb810..b974972d70 100644 --- a/frontend/src/container/MetricsApplication/MetricsApplication.factory.ts +++ b/frontend/src/container/MetricsApplication/MetricsApplication.factory.ts @@ -22,4 +22,6 @@ export const getWidgetQueryBuilder = ({ yAxisUnit, softMax: null, softMin: null, + selectedLogFields: [], + selectedTracesFields: [], }); diff --git a/frontend/src/container/NewDashboard/ComponentsSlider/constants.ts b/frontend/src/container/NewDashboard/ComponentsSlider/constants.ts new file mode 100644 index 0000000000..44512e3a00 --- /dev/null +++ b/frontend/src/container/NewDashboard/ComponentsSlider/constants.ts @@ -0,0 +1,88 @@ +import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; +import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { Query } from 'types/api/queryBuilder/queryBuilderData'; +import { LogsAggregatorOperator } from 'types/common/queryBuilder'; + +export const PANEL_TYPES_INITIAL_QUERY = { + [PANEL_TYPES.TIME_SERIES]: initialQueriesMap.metrics, + [PANEL_TYPES.VALUE]: initialQueriesMap.metrics, + [PANEL_TYPES.TABLE]: initialQueriesMap.metrics, + [PANEL_TYPES.LIST]: initialQueriesMap.logs, + [PANEL_TYPES.TRACE]: initialQueriesMap.traces, + [PANEL_TYPES.EMPTY_WIDGET]: initialQueriesMap.metrics, +}; + +export const listViewInitialLogQuery: Query = { + ...initialQueriesMap.logs, + builder: { + ...initialQueriesMap.logs.builder, + queryData: [ + { + ...initialQueriesMap.logs.builder.queryData[0], + aggregateOperator: LogsAggregatorOperator.NOOP, + orderBy: [{ columnName: 'timestamp', order: 'desc' }], + offset: 0, + pageSize: 100, + }, + ], + }, +}; + +export const listViewInitialTraceQuery = { + // it should be the above commented query + ...initialQueriesMap.traces, + builder: { + ...initialQueriesMap.traces.builder, + queryData: [ + { + ...initialQueriesMap.traces.builder.queryData[0], + aggregateOperator: LogsAggregatorOperator.NOOP, + orderBy: [{ columnName: 'timestamp', order: 'desc' }], + offset: 0, + pageSize: 10, + selectColumns: [ + { + key: 'serviceName', + dataType: 'string', + type: 'tag', + isColumn: true, + isJSON: false, + id: 'serviceName--string--tag--true', + }, + { + key: 'name', + dataType: 'string', + type: 'tag', + isColumn: true, + isJSON: false, + id: 'name--string--tag--true', + }, + { + key: 'durationNano', + dataType: 'float64', + type: 'tag', + isColumn: true, + isJSON: false, + id: 'durationNano--float64--tag--true', + }, + { + key: 'httpMethod', + dataType: 'string', + type: 'tag', + isColumn: true, + isJSON: false, + id: 'httpMethod--string--tag--true', + }, + { + key: 'responseStatusCode', + dataType: 'string', + type: 'tag', + isColumn: true, + isJSON: false, + id: 'responseStatusCode--string--tag--true', + }, + ] as BaseAutocompleteData[], + }, + ], + }, +}; diff --git a/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx b/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx index d355edfd1a..80f1e745da 100644 --- a/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx +++ b/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx @@ -1,6 +1,6 @@ import { SOMETHING_WENT_WRONG } from 'constants/api'; import { QueryParams } from 'constants/query'; -import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; +import { PANEL_TYPES } from 'constants/queryBuilder'; import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; import { useIsDarkMode } from 'hooks/useDarkMode'; import { useNotifications } from 'hooks/useNotifications'; @@ -8,8 +8,14 @@ import createQueryParams from 'lib/createQueryParams'; import history from 'lib/history'; import { useDashboard } from 'providers/Dashboard/Dashboard'; import { CSSProperties } from 'react'; +import { LogsAggregatorOperator } from 'types/common/queryBuilder'; import { v4 as uuid } from 'uuid'; +import { + listViewInitialLogQuery, + listViewInitialTraceQuery, + PANEL_TYPES_INITIAL_QUERY, +} from './constants'; import menuItems from './menuItems'; import { Card, Container, Text } from './styles'; @@ -28,6 +34,7 @@ function DashboardGraphSlider(): JSX.Element { const updateDashboardMutation = useUpdateDashboard(); + // eslint-disable-next-line sonarjs/cognitive-complexity const onClickHandler = (name: PANEL_TYPES) => (): void => { const id = uuid(); @@ -61,10 +68,28 @@ function DashboardGraphSlider(): JSX.Element { nullZeroValues: '', opacity: '', panelTypes: name, - query: initialQueriesMap.metrics, + query: + name === PANEL_TYPES.LIST + ? listViewInitialLogQuery + : PANEL_TYPES_INITIAL_QUERY[name], timePreferance: 'GLOBAL_TIME', softMax: null, softMin: null, + selectedLogFields: [ + { + dataType: 'string', + type: '', + name: 'body', + }, + { + dataType: 'string', + type: '', + name: 'timestamp', + }, + ], + selectedTracesFields: [ + ...listViewInitialTraceQuery.builder.queryData[0].selectColumns, + ], }, ], }, @@ -73,16 +98,43 @@ function DashboardGraphSlider(): JSX.Element { onSuccess: (data) => { if (data.payload) { handleToggleDashboardSlider(false); + const queryParamsLog = { + graphType: name, + widgetId: id, + [QueryParams.compositeQuery]: JSON.stringify({ + ...PANEL_TYPES_INITIAL_QUERY[name], + builder: { + ...PANEL_TYPES_INITIAL_QUERY[name].builder, + queryData: [ + { + ...PANEL_TYPES_INITIAL_QUERY[name].builder.queryData[0], + aggregateOperator: LogsAggregatorOperator.NOOP, + orderBy: [{ columnName: 'timestamp', order: 'desc' }], + offset: 0, + pageSize: 100, + }, + ], + }, + }), + }; const queryParams = { graphType: name, widgetId: id, - [QueryParams.compositeQuery]: JSON.stringify(initialQueriesMap.metrics), + [QueryParams.compositeQuery]: JSON.stringify( + PANEL_TYPES_INITIAL_QUERY[name], + ), }; - history.push( - `${history.location.pathname}/new?${createQueryParams(queryParams)}`, - ); + if (name === PANEL_TYPES.LIST) { + history.push( + `${history.location.pathname}/new?${createQueryParams(queryParamsLog)}`, + ); + } else { + history.push( + `${history.location.pathname}/new?${createQueryParams(queryParams)}`, + ); + } } }, onError: () => { diff --git a/frontend/src/container/NewDashboard/ComponentsSlider/menuItems.ts b/frontend/src/container/NewDashboard/ComponentsSlider/menuItems.ts index 560da8deef..1aaa3a71ea 100644 --- a/frontend/src/container/NewDashboard/ComponentsSlider/menuItems.ts +++ b/frontend/src/container/NewDashboard/ComponentsSlider/menuItems.ts @@ -1,3 +1,4 @@ +import List from 'assets/Dashboard/List'; import TableIcon from 'assets/Dashboard/Table'; import TimeSeriesIcon from 'assets/Dashboard/TimeSeries'; import ValueIcon from 'assets/Dashboard/Value'; @@ -16,6 +17,7 @@ const Items: ItemsProps[] = [ display: 'Value', }, { name: PANEL_TYPES.TABLE, Icon: TableIcon, display: 'Table' }, + { name: PANEL_TYPES.LIST, Icon: List, display: 'List' }, ]; interface ItemsProps { diff --git a/frontend/src/container/NewWidget/LeftContainer/ExplorerColumnsRenderer.styles.scss b/frontend/src/container/NewWidget/LeftContainer/ExplorerColumnsRenderer.styles.scss new file mode 100644 index 0000000000..f9652e859a --- /dev/null +++ b/frontend/src/container/NewWidget/LeftContainer/ExplorerColumnsRenderer.styles.scss @@ -0,0 +1,184 @@ +.explorer-columns-renderer { + margin-top: 10px; + + .title { + display: flex; + align-items: center; + gap: 4px; + } + + .ant-typography { + color: var(rgba(255, 255, 255, 0.85)); + font-family: "Inter"; + font-size: 13px; + font-style: normal; + font-weight: 400; + line-height: 22px; + letter-spacing: 0.5px; + } + + .ant-divider { + margin: 8px 0 !important; + border: 0.5px solid var(--bg-slate-400); + } + + .explorer-columns-contents { + display: flex; + justify-content: space-between; + align-items: center; + + .explorer-columns { + display: flex; + align-items: center; + gap: 12px; + overflow-x: scroll; + min-width: 90%; + + .explorer-columns-list { + display: flex !important; + } + + .explorer-column-card { + display: flex; + align-items: center; + justify-content: space-between; + padding: 4px; + min-width: 200px; + border-radius: 2px; + border: 1px solid var(--colorBorder, rgba(118, 136, 201, 0.12)); + background: var(--bg-slate-500); + cursor: unset; + + .explorer-column-title { + display: flex; + align-items: center; + gap: 8px; + font-family: Inter; + font-size: 12px; + cursor: grab; + } + + .lucide-trash2 { + cursor: pointer !important; + } + + } + } + + .explorer-columns::-webkit-scrollbar { + height: 0px; /* Height of the scrollbar */ + } + + .action-btn { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + padding: 0px 16px; + border-radius: 2px; + background: var(--bg-robin-400); + } + } +} + +.explorer-columns-search { + border: 1px solid rgba(118, 136, 201, 0.12); + border-radius: 6px; + padding: 0px; + background:#141414; + > input { + height: 32px; + padding: 0 6px; + } + + +} + +.explorer-columns-dropdown { + height: 200px; + background-color: var(--bg-slate-500); + overflow: hidden !important; + .ant-dropdown-menu { + padding: 0; + + .ant-dropdown-menu-item { + padding: 4px; + .ant-checkbox-wrapper { + padding: 2px 8px !important; + } + + .attribute-columns { + display: flex; + flex-direction: column; + height: 160px; + overflow: scroll; + } + + .attribute-columns::-webkit-scrollbar { + width: 3px; /* Width of the scrollbar */ + } + + .attribute-columns::-webkit-scrollbar-track { + background: var(--bg-slate-500); /* Color of the track */ + } + + .attribute-columns::-webkit-scrollbar-thumb { + background: var(--bg-vanilla-400); /* Color of the thumb */ + border-radius: 4px; /* Roundness of the thumb */ + } + + .attribute-columns::-webkit-scrollbar-thumb:hover { + background: var(--bg-vanilla-300); /* Color of the thumb on hover */ + } + } + } +} + +.lightMode { + .explorer-columns-renderer { + + .ant-divider { + border: 0.5px solid var(--bg-vanilla-300); + } + + .explorer-columns { + .explorer-column-card { + border: 1px solid var(--colorBorder, rgba(118, 136, 201, 0.12)); + background: var(--bg-vanilla-200); + } + } + + .explorer-columns-search { + border: 1px solid rgba(118, 136, 201, 0.12); + } + } + + .explorer-columns-dropdown { + background-color: var(--bg-vanilla-100); + + .ant-dropdown-menu-item { + .attribute-columns { + &::-webkit-scrollbar { + width: 3px; /* Width of the scrollbar */ + } + + &::-webkit-scrollbar-track { + background: var(--bg-vanilla-200); /* Color of the track */ + } + + &::-webkit-scrollbar-thumb { + background: var(--bg-vanilla-400); /* Color of the thumb */ + } + + &::-webkit-scrollbar-thumb:hover { + background: var(--bg-vanilla-300); /* Color of the thumb on hover */ + } + } + } + } + + .explorer-columns-search { + background: var(--bg-vanilla-100); + } +} diff --git a/frontend/src/container/NewWidget/LeftContainer/ExplorerColumnsRenderer.tsx b/frontend/src/container/NewWidget/LeftContainer/ExplorerColumnsRenderer.tsx new file mode 100644 index 0000000000..a9a8d9ceb2 --- /dev/null +++ b/frontend/src/container/NewWidget/LeftContainer/ExplorerColumnsRenderer.tsx @@ -0,0 +1,328 @@ +/* eslint-disable sonarjs/cognitive-complexity */ +/* eslint-disable react/jsx-props-no-spreading */ +import './ExplorerColumnsRenderer.styles.scss'; + +import { Color } from '@signozhq/design-tokens'; +import { + Button, + Checkbox, + Divider, + Dropdown, + Input, + Tooltip, + Typography, +} from 'antd'; +import { MenuProps } from 'antd/lib'; +import Spinner from 'components/Spinner'; +import { SOMETHING_WENT_WRONG } from 'constants/api'; +import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys'; +import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; +import { useIsDarkMode } from 'hooks/useDarkMode'; +import { + AlertCircle, + GripVertical, + PlusCircle, + Search, + Trash2, +} from 'lucide-react'; +import { useState } from 'react'; +import { + DragDropContext, + Draggable, + Droppable, + DropResult, +} from 'react-beautiful-dnd'; +import { DataSource } from 'types/common/queryBuilder'; + +import { WidgetGraphProps } from '../types'; + +type LogColumnsRendererProps = { + setSelectedLogFields: WidgetGraphProps['setSelectedLogFields']; + selectedLogFields: WidgetGraphProps['selectedLogFields']; + selectedTracesFields: WidgetGraphProps['selectedTracesFields']; + setSelectedTracesFields: WidgetGraphProps['setSelectedTracesFields']; +}; + +function ExplorerColumnsRenderer({ + selectedLogFields, + setSelectedLogFields, + selectedTracesFields, + setSelectedTracesFields, +}: LogColumnsRendererProps): JSX.Element { + const { currentQuery } = useQueryBuilder(); + const [searchText, setSearchText] = useState(''); + const [open, setOpen] = useState(false); + + const initialDataSource = currentQuery.builder.queryData[0].dataSource; + + const { data, isLoading, isError } = useGetAggregateKeys( + { + aggregateAttribute: '', + dataSource: currentQuery.builder.queryData[0].dataSource, + aggregateOperator: currentQuery.builder.queryData[0].aggregateOperator, + searchText: '', + tagType: '', + }, + { + queryKey: [ + currentQuery.builder.queryData[0].dataSource, + currentQuery.builder.queryData[0].aggregateOperator, + ], + }, + ); + + const isAttributeKeySelected = (key: string): boolean => { + if (initialDataSource === DataSource.LOGS && selectedLogFields) { + return selectedLogFields.some((field) => field.name === key); + } + if (initialDataSource === DataSource.TRACES && selectedTracesFields) { + return selectedTracesFields.some((field) => field.key === key); + } + return false; + }; + + const handleCheckboxChange = (key: string): void => { + if ( + initialDataSource === DataSource.LOGS && + setSelectedLogFields !== undefined + ) { + if (selectedLogFields) { + if (isAttributeKeySelected(key)) { + setSelectedLogFields( + selectedLogFields.filter((field) => field.name !== key), + ); + } else { + setSelectedLogFields([ + ...selectedLogFields, + { dataType: 'string', name: key, type: '' }, + ]); + } + } else { + setSelectedLogFields([{ dataType: 'string', name: key, type: '' }]); + } + } else if ( + initialDataSource === DataSource.TRACES && + setSelectedTracesFields !== undefined + ) { + const selectedField = data?.payload?.attributeKeys?.find( + (attributeKey) => attributeKey.key === key, + ); + if (selectedTracesFields) { + if (isAttributeKeySelected(key)) { + setSelectedTracesFields( + selectedTracesFields.filter((field) => field.key !== key), + ); + } else if (selectedField) { + setSelectedTracesFields([...selectedTracesFields, selectedField]); + } + } else if (selectedField) setSelectedTracesFields([selectedField]); + } + setOpen(false); + }; + + const handleSearchChange = (e: React.ChangeEvent): void => { + setSearchText(e.target.value); + }; + + const items: MenuProps['items'] = [ + { + key: 'search', + label: ( + } + /> + ), + }, + { + key: 'columns', + label: ( +
+ {data?.payload?.attributeKeys + ?.filter((attributeKey) => + attributeKey.key.toLowerCase().includes(searchText.toLowerCase()), + ) + ?.map((attributeKey) => ( + handleCheckboxChange(attributeKey.key)} + style={{ padding: 0 }} + key={attributeKey.key} + > + {attributeKey.key} + + ))} +
+ ), + }, + ]; + + const removeSelectedLogField = (name: string): void => { + if ( + initialDataSource === DataSource.LOGS && + setSelectedLogFields && + selectedLogFields + ) { + setSelectedLogFields( + selectedLogFields.filter((field) => field.name !== name), + ); + } + if ( + initialDataSource === DataSource.TRACES && + setSelectedTracesFields && + selectedTracesFields + ) { + setSelectedTracesFields( + selectedTracesFields.filter((field) => field.key !== name), + ); + } + }; + + const onDragEnd = (result: DropResult): void => { + if (!result.destination) { + return; + } + + if ( + initialDataSource === DataSource.LOGS && + selectedLogFields && + setSelectedLogFields + ) { + const items = [...selectedLogFields]; + const [reorderedItem] = items.splice(result.source.index, 1); + items.splice(result.destination.index, 0, reorderedItem); + + setSelectedLogFields(items); + } + if ( + initialDataSource === DataSource.TRACES && + selectedTracesFields && + setSelectedTracesFields + ) { + const items = [...selectedTracesFields]; + const [reorderedItem] = items.splice(result.source.index, 1); + items.splice(result.destination.index, 0, reorderedItem); + + setSelectedTracesFields(items); + } + }; + + const toggleDropdown = (): void => { + setOpen(!open); + if (!open) { + setSearchText(''); + } + }; + + const isDarkMode = useIsDarkMode(); + + if (isLoading) { + return ; + } + + return ( +
+
+ Columns + {isError && ( + + + + )} +
+ + {!isError && ( +
+ + + {(provided): JSX.Element => ( +
+ {initialDataSource === DataSource.LOGS && + selectedLogFields && + selectedLogFields.map((field, index) => ( + // eslint-disable-next-line react/no-array-index-key + + {(dragProvided): JSX.Element => ( +
+
+ + {field.name} +
+ removeSelectedLogField(field.name)} + /> +
+ )} +
+ ))} + {initialDataSource === DataSource.TRACES && + selectedTracesFields && + selectedTracesFields.map((field, index) => ( + // eslint-disable-next-line react/no-array-index-key + + {(dragProvided): JSX.Element => ( +
+
+ + {field.key} +
+ removeSelectedLogField(field.key)} + /> +
+ )} +
+ ))} +
+ )} +
+
+
+ +
+
+ )} +
+ ); +} + +export default ExplorerColumnsRenderer; diff --git a/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx b/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx index 9fe20bd8bc..4c66c70b83 100644 --- a/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx @@ -18,7 +18,7 @@ import { getPreviousWidgets, getSelectedWidgetIndex, } from 'providers/Dashboard/util'; -import { useCallback, useMemo, useState } from 'react'; +import { useCallback, useMemo } from 'react'; import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; import { Widgets } from 'types/api/dashboard/getAll'; @@ -35,7 +35,6 @@ function QuerySection({ selectedTime, }: QueryProps): JSX.Element { const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder(); - const [currentTab, setCurrentTab] = useState(currentQuery.queryType); const urlQuery = useUrlQuery(); const { minTime, maxTime } = useSelector( @@ -100,7 +99,6 @@ function QuerySection({ ], }, }); - redirectWithQueryBuilderData(updatedQuery); }, [ @@ -114,11 +112,13 @@ function QuerySection({ ); const handleQueryCategoryChange = (qCategory: string): void => { - const currentQueryType = qCategory as EQueryType; - setCurrentTab(qCategory as EQueryType); + const currentQueryType = qCategory; featureResponse.refetch().then(() => { - handleStageQuery({ ...currentQuery, queryType: currentQueryType }); + handleStageQuery({ + ...currentQuery, + queryType: currentQueryType as EQueryType, + }); }); }; @@ -134,6 +134,27 @@ function QuerySection({ return config; }, []); + const listItems = [ + { + key: EQueryType.QUERY_BUILDER, + label: ( + + + + ), + tab: Query Builder, + children: ( + + ), + }, + ]; + const items = [ { key: EQueryType.QUERY_BUILDER, @@ -180,8 +201,12 @@ function QuerySection({ @@ -197,7 +222,7 @@ function QuerySection({ } - items={items} + items={selectedGraph === PANEL_TYPES.LIST ? listItems : items} /> ); diff --git a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/PlotTag.tsx b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/PlotTag.tsx index 219d4e011b..99da2e517e 100644 --- a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/PlotTag.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/PlotTag.tsx @@ -10,7 +10,7 @@ interface IPlotTagProps { } function PlotTag({ queryType, panelType }: IPlotTagProps): JSX.Element | null { - if (queryType === undefined) { + if (queryType === undefined || panelType === PANEL_TYPES.LIST) { return null; } diff --git a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraphContainer.tsx b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraphContainer.tsx index c3d47880d3..2472d94092 100644 --- a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraphContainer.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraphContainer.tsx @@ -1,5 +1,6 @@ import { Card, Typography } from 'antd'; import Spinner from 'components/Spinner'; +import { PANEL_TYPES } from 'constants/queryBuilder'; import { WidgetGraphProps } from 'container/NewWidget/types'; import { useGetWidgetQueryRange } from 'hooks/queryBuilder/useGetWidgetQueryRange'; import useUrlQuery from 'hooks/useUrlQuery'; @@ -16,6 +17,8 @@ function WidgetGraphContainer({ fillSpans = false, softMax, softMin, + selectedLogFields, + selectedTracesFields, }: WidgetGraphProps): JSX.Element { const { selectedDashboard } = useDashboard(); @@ -46,7 +49,21 @@ function WidgetGraphContainer({ if (getWidgetQueryRange.isLoading) { return ; } - if (getWidgetQueryRange.data?.payload.data.result.length === 0) { + + if ( + selectedGraph !== PANEL_TYPES.LIST && + getWidgetQueryRange.data?.payload.data.result.length === 0 + ) { + return ( + + No Data + + ); + } + if ( + selectedGraph === PANEL_TYPES.LIST && + getWidgetQueryRange.data?.payload.data.newResult.data.result.length === 0 + ) { return ( No Data @@ -63,6 +80,9 @@ function WidgetGraphContainer({ fillSpans={fillSpans} softMax={softMax} softMin={softMin} + selectedLogFields={selectedLogFields} + selectedTracesFields={selectedTracesFields} + selectedTime={selectedTime} /> ); } diff --git a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraphs.tsx b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraphs.tsx index f4a8ed1b22..ccd0a91ea3 100644 --- a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraphs.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraphs.tsx @@ -1,6 +1,7 @@ import { QueryParams } from 'constants/query'; import GridPanelSwitch from 'container/GridPanelSwitch'; import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types'; +import { timePreferance } from 'container/NewWidget/RightContainer/timeItems'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useIsDarkMode } from 'hooks/useDarkMode'; import { useResizeObserver } from 'hooks/useDimensions'; @@ -30,8 +31,11 @@ function WidgetGraph({ fillSpans, softMax, softMin, + selectedLogFields, + selectedTracesFields, + selectedTime, }: WidgetGraphProps): JSX.Element { - const { stagedQuery } = useQueryBuilder(); + const { stagedQuery, currentQuery } = useQueryBuilder(); const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector< AppState, @@ -156,6 +160,10 @@ function WidgetGraph({ } query={stagedQuery || selectedWidget.query} thresholds={thresholds} + selectedLogFields={selectedLogFields} + selectedTracesFields={selectedTracesFields} + dataSource={currentQuery.builder.queryData[0].dataSource} + selectedTime={selectedTime} /> ); @@ -172,6 +180,9 @@ interface WidgetGraphProps { >; softMax: number | null; softMin: number | null; + selectedLogFields: Widgets['selectedLogFields']; + selectedTracesFields: Widgets['selectedTracesFields']; + selectedTime: timePreferance; } export default WidgetGraph; diff --git a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/index.tsx b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/index.tsx index 56846c8dec..1f306a41a2 100644 --- a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/index.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/index.tsx @@ -19,6 +19,8 @@ function WidgetGraph({ fillSpans, softMax, softMin, + selectedLogFields, + selectedTracesFields, }: WidgetGraphProps): JSX.Element { const { currentQuery } = useQueryBuilder(); const { selectedDashboard } = useDashboard(); @@ -57,6 +59,8 @@ function WidgetGraph({ fillSpans={fillSpans} softMax={softMax} softMin={softMin} + selectedLogFields={selectedLogFields} + selectedTracesFields={selectedTracesFields} /> ); diff --git a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/styles.ts b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/styles.ts index b1bf36b588..a5d030e27c 100644 --- a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/styles.ts +++ b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/styles.ts @@ -13,8 +13,10 @@ export const Container = styled(Card)` .ant-card-body { padding: ${({ $panelType }): string => - $panelType === PANEL_TYPES.TABLE ? '0 0' : '1.5rem 0'}; - height: 57vh; + $panelType === PANEL_TYPES.TABLE || $panelType === PANEL_TYPES.LIST + ? '0 0' + : '1.5rem 0'}; + height: 60vh; display: flex; flex-direction: column; } diff --git a/frontend/src/container/NewWidget/LeftContainer/index.tsx b/frontend/src/container/NewWidget/LeftContainer/index.tsx index 0a78a084d8..5dd429d83f 100644 --- a/frontend/src/container/NewWidget/LeftContainer/index.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/index.tsx @@ -1,6 +1,8 @@ +import { PANEL_TYPES } from 'constants/queryBuilder'; import { memo } from 'react'; import { WidgetGraphProps } from '../types'; +import ExplorerColumnsRenderer from './ExplorerColumnsRenderer'; import QuerySection from './QuerySection'; import { QueryContainer } from './styles'; import WidgetGraph from './WidgetGraph'; @@ -13,6 +15,10 @@ function LeftContainer({ fillSpans, softMax, softMin, + selectedLogFields, + setSelectedLogFields, + selectedTracesFields, + setSelectedTracesFields, }: WidgetGraphProps): JSX.Element { return ( <> @@ -24,9 +30,19 @@ function LeftContainer({ fillSpans={fillSpans} softMax={softMax} softMin={softMin} + selectedLogFields={selectedLogFields} + selectedTracesFields={selectedTracesFields} /> + {selectedGraph === PANEL_TYPES.LIST && ( + + )} ); diff --git a/frontend/src/container/NewWidget/RightContainer/constants.ts b/frontend/src/container/NewWidget/RightContainer/constants.ts index 0030f6927b..a6663ac75c 100644 --- a/frontend/src/container/NewWidget/RightContainer/constants.ts +++ b/frontend/src/container/NewWidget/RightContainer/constants.ts @@ -47,3 +47,41 @@ export const panelTypeVsDragAndDrop: { [key in PANEL_TYPES]: boolean } = { [PANEL_TYPES.TRACE]: false, [PANEL_TYPES.EMPTY_WIDGET]: false, } as const; + +export const panelTypeVsFillSpan: { [key in PANEL_TYPES]: boolean } = { + [PANEL_TYPES.TIME_SERIES]: true, + [PANEL_TYPES.VALUE]: false, + [PANEL_TYPES.TABLE]: false, + [PANEL_TYPES.LIST]: false, + [PANEL_TYPES.TRACE]: false, + [PANEL_TYPES.EMPTY_WIDGET]: false, +} as const; + +export const panelTypeVsYAxisUnit: { [key in PANEL_TYPES]: boolean } = { + [PANEL_TYPES.TIME_SERIES]: true, + [PANEL_TYPES.VALUE]: true, + [PANEL_TYPES.TABLE]: true, + [PANEL_TYPES.LIST]: false, + [PANEL_TYPES.TRACE]: false, + [PANEL_TYPES.EMPTY_WIDGET]: false, +} as const; + +export const panelTypeVsCreateAlert: { [key in PANEL_TYPES]: boolean } = { + [PANEL_TYPES.TIME_SERIES]: true, + [PANEL_TYPES.VALUE]: true, + [PANEL_TYPES.TABLE]: false, + [PANEL_TYPES.LIST]: false, + [PANEL_TYPES.TRACE]: false, + [PANEL_TYPES.EMPTY_WIDGET]: false, +} as const; + +export const panelTypeVsPanelTimePreferences: { + [key in PANEL_TYPES]: boolean; +} = { + [PANEL_TYPES.TIME_SERIES]: true, + [PANEL_TYPES.VALUE]: true, + [PANEL_TYPES.TABLE]: true, + [PANEL_TYPES.LIST]: false, + [PANEL_TYPES.TRACE]: false, + [PANEL_TYPES.EMPTY_WIDGET]: false, +} as const; diff --git a/frontend/src/container/NewWidget/RightContainer/index.tsx b/frontend/src/container/NewWidget/RightContainer/index.tsx index 5ab1a965fc..60e90c866e 100644 --- a/frontend/src/container/NewWidget/RightContainer/index.tsx +++ b/frontend/src/container/NewWidget/RightContainer/index.tsx @@ -17,7 +17,14 @@ import useCreateAlerts from 'hooks/queryBuilder/useCreateAlerts'; import { Dispatch, SetStateAction, useCallback } from 'react'; import { Widgets } from 'types/api/dashboard/getAll'; -import { panelTypeVsSoftMinMax, panelTypeVsThreshold } from './constants'; +import { + panelTypeVsCreateAlert, + panelTypeVsFillSpan, + panelTypeVsPanelTimePreferences, + panelTypeVsSoftMinMax, + panelTypeVsThreshold, + panelTypeVsYAxisUnit, +} from './constants'; import { Container, Title } from './styles'; import ThresholdSelector from './Threshold/ThresholdSelector'; import { ThresholdProps } from './Threshold/types'; @@ -62,6 +69,11 @@ function RightContainer({ const allowThreshold = panelTypeVsThreshold[selectedGraph]; const allowSoftMinMax = panelTypeVsSoftMinMax[selectedGraph]; + const allowFillSpans = panelTypeVsFillSpan[selectedGraph]; + const allowYAxisUnit = panelTypeVsYAxisUnit[selectedGraph]; + const allowCreateAlerts = panelTypeVsCreateAlert[selectedGraph]; + const allowPanelTimePreference = + panelTypeVsPanelTimePreferences[selectedGraph]; const softMinHandler = useCallback( (value: number | null) => { @@ -117,32 +129,40 @@ function RightContainer({ } /> - - Fill gaps + {allowFillSpans && ( + + Fill gaps - setIsFillSpans(checked)} - /> - + setIsFillSpans(checked)} + /> + + )} - Panel Time Preference + {allowPanelTimePreference && ( + Panel Time Preference + )} - + {allowPanelTimePreference && ( + + )} - + {allowYAxisUnit && ( + + )} - {selectedWidget?.panelTypes !== PANEL_TYPES.TABLE && ( + {allowCreateAlerts && ( diff --git a/frontend/src/container/NewWidget/index.tsx b/frontend/src/container/NewWidget/index.tsx index 5192644a5e..f4c62ae601 100644 --- a/frontend/src/container/NewWidget/index.tsx +++ b/frontend/src/container/NewWidget/index.tsx @@ -24,6 +24,7 @@ import { useSelector } from 'react-redux'; import { generatePath, useLocation, useParams } from 'react-router-dom'; import { AppState } from 'store/reducers'; import { Dashboard, Widgets } from 'types/api/dashboard/getAll'; +import { IField } from 'types/api/logs/fields'; import { EQueryType } from 'types/common/dashboard'; import { DataSource } from 'types/common/queryBuilder'; import AppReducer from 'types/reducer/app'; @@ -110,6 +111,14 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element { : selectedWidget?.softMin || 0, ); + const [selectedLogFields, setSelectedLogFields] = useState( + selectedWidget?.selectedLogFields || null, + ); + + const [selectedTracesFields, setSelectedTracesFields] = useState( + selectedWidget?.selectedTracesFields || null, + ); + const [softMax, setSoftMax] = useState( selectedWidget?.softMax === null || selectedWidget?.softMax === undefined ? null @@ -189,10 +198,13 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element { title, yAxisUnit, panelTypes: graphType, + query: currentQuery, thresholds, softMin, softMax, fillSpans: isFillSpans, + selectedLogFields, + selectedTracesFields, }, ...afterWidgets, ], @@ -226,10 +238,13 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element { title, yAxisUnit, graphType, + currentQuery, thresholds, softMin, softMax, isFillSpans, + selectedLogFields, + selectedTracesFields, afterWidgets, updateDashboardMutation, setSelectedDashboard, @@ -336,6 +351,10 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element { fillSpans={isFillSpans} softMax={softMax} softMin={softMin} + selectedLogFields={selectedLogFields} + setSelectedLogFields={setSelectedLogFields} + selectedTracesFields={selectedTracesFields} + setSelectedTracesFields={setSelectedTracesFields} /> diff --git a/frontend/src/container/NewWidget/types.ts b/frontend/src/container/NewWidget/types.ts index 21d2268d76..31cdf0c87c 100644 --- a/frontend/src/container/NewWidget/types.ts +++ b/frontend/src/container/NewWidget/types.ts @@ -1,4 +1,5 @@ import { PANEL_TYPES } from 'constants/queryBuilder'; +import { Dispatch, SetStateAction } from 'react'; import { Widgets } from 'types/api/dashboard/getAll'; import { ThresholdProps } from './RightContainer/Threshold/types'; @@ -15,4 +16,10 @@ export interface WidgetGraphProps extends NewWidgetProps { thresholds: ThresholdProps[]; softMin: number | null; softMax: number | null; + selectedLogFields: Widgets['selectedLogFields']; + setSelectedLogFields?: Dispatch>; + selectedTracesFields: Widgets['selectedTracesFields']; + setSelectedTracesFields?: Dispatch< + SetStateAction + >; } diff --git a/frontend/src/container/QueryBuilder/QueryBuilder.interfaces.ts b/frontend/src/container/QueryBuilder/QueryBuilder.interfaces.ts index 0923395296..ef18d8ce39 100644 --- a/frontend/src/container/QueryBuilder/QueryBuilder.interfaces.ts +++ b/frontend/src/container/QueryBuilder/QueryBuilder.interfaces.ts @@ -26,4 +26,5 @@ export type QueryBuilderProps = { actions?: ReactNode; filterConfigs?: Partial; queryComponents?: { renderOrderBy?: (props: OrderByFilterProps) => ReactNode }; + isListViewPanel?: boolean; }; diff --git a/frontend/src/container/QueryBuilder/QueryBuilder.tsx b/frontend/src/container/QueryBuilder/QueryBuilder.tsx index 39875a7c90..f67533e427 100644 --- a/frontend/src/container/QueryBuilder/QueryBuilder.tsx +++ b/frontend/src/container/QueryBuilder/QueryBuilder.tsx @@ -1,7 +1,12 @@ import './QueryBuilder.styles.scss'; import { Button, Col, Divider, Row, Tooltip } from 'antd'; -import { MAX_FORMULAS, MAX_QUERIES } from 'constants/queryBuilder'; +import { + MAX_FORMULAS, + MAX_QUERIES, + OPERATORS, + PANEL_TYPES, +} from 'constants/queryBuilder'; // ** Hooks import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { DatabaseZap, Sigma } from 'lucide-react'; @@ -19,6 +24,7 @@ export const QueryBuilder = memo(function QueryBuilder({ panelType: newPanelType, filterConfigs = {}, queryComponents, + isListViewPanel = false, }: QueryBuilderProps): JSX.Element { const { currentQuery, @@ -84,6 +90,33 @@ export const QueryBuilder = memo(function QueryBuilder({ } }; + const listViewLogFilterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => { + const config: QueryBuilderProps['filterConfigs'] = { + stepInterval: { isHidden: true, isDisabled: true }, + having: { isHidden: true, isDisabled: true }, + filters: { + customKey: 'body', + customOp: OPERATORS.CONTAINS, + }, + }; + + return config; + }, []); + + const listViewTracesFilterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => { + const config: QueryBuilderProps['filterConfigs'] = { + stepInterval: { isHidden: true, isDisabled: true }, + having: { isHidden: true, isDisabled: true }, + limit: { isHidden: true, isDisabled: true }, + filters: { + customKey: 'body', + customOp: OPERATORS.CONTAINS, + }, + }; + + return config; + }, []); + return ( -
- - - - + {!isListViewPanel && ( +
+ + + + - - - - -
+ + + +
+
+ )}
@@ -119,49 +154,66 @@ export const QueryBuilder = memo(function QueryBuilder({ className="query-builder-queries-formula-container" ref={containerRef} > - {currentQuery.builder.queryData.map((query, index) => ( - - - - ))} - {currentQuery.builder.queryFormulas.map((formula, index) => { - const isAllMetricDataSource = currentQuery.builder.queryData.every( - (query) => query.dataSource === DataSource.METRICS, - ); - - const query = - currentQuery.builder.queryData[index] || - currentQuery.builder.queryData[0]; - - return ( + {panelType === PANEL_TYPES.LIST && isListViewPanel && ( + + )} + {!isListViewPanel && + currentQuery.builder.queryData.map((query, index) => ( - - ); - })} + ))} + {!isListViewPanel && + currentQuery.builder.queryFormulas.map((formula, index) => { + const isAllMetricDataSource = currentQuery.builder.queryData.every( + (query) => query.dataSource === DataSource.METRICS, + ); + + const query = + currentQuery.builder.queryData[index] || + currentQuery.builder.queryData[0]; + + return ( + + + + ); + })} @@ -171,29 +223,31 @@ export const QueryBuilder = memo(function QueryBuilder({ - - {currentQuery.builder.queryData.map((query) => ( - - ))} + {!isListViewPanel && ( + + {currentQuery.builder.queryData.map((query) => ( + + ))} - {currentQuery.builder.queryFormulas.map((formula) => ( - - ))} - + {currentQuery.builder.queryFormulas.map((formula) => ( + + ))} + + )} ); }); diff --git a/frontend/src/container/QueryBuilder/components/DataSourceDropdown/DataSourceDropdown.interfaces.ts b/frontend/src/container/QueryBuilder/components/DataSourceDropdown/DataSourceDropdown.interfaces.ts index 0a34d52c6f..acbe586220 100644 --- a/frontend/src/container/QueryBuilder/components/DataSourceDropdown/DataSourceDropdown.interfaces.ts +++ b/frontend/src/container/QueryBuilder/components/DataSourceDropdown/DataSourceDropdown.interfaces.ts @@ -3,4 +3,5 @@ import { DataSource } from 'types/common/queryBuilder'; export type QueryLabelProps = { onChange: (value: DataSource) => void; + isListViewPanel?: boolean; } & Omit; diff --git a/frontend/src/container/QueryBuilder/components/DataSourceDropdown/DataSourceDropdown.tsx b/frontend/src/container/QueryBuilder/components/DataSourceDropdown/DataSourceDropdown.tsx index 52b3d030cb..81efc3b9d7 100644 --- a/frontend/src/container/QueryBuilder/components/DataSourceDropdown/DataSourceDropdown.tsx +++ b/frontend/src/container/QueryBuilder/components/DataSourceDropdown/DataSourceDropdown.tsx @@ -10,18 +10,22 @@ import { QueryLabelProps } from './DataSourceDropdown.interfaces'; const dataSourceMap = [DataSource.LOGS, DataSource.METRICS, DataSource.TRACES]; +const exploreDataSourceMap = [DataSource.LOGS, DataSource.TRACES]; + export const DataSourceDropdown = memo(function DataSourceDropdown( props: QueryLabelProps, ): JSX.Element { - const { onChange, value, style } = props; + const { onChange, value, style, isListViewPanel = false } = props; - const dataSourceOptions: SelectOption< - DataSource, - string - >[] = dataSourceMap.map((source) => ({ - label: transformToUpperCase(source), - value: source, - })); + const dataSourceOptions: SelectOption[] = isListViewPanel + ? exploreDataSourceMap.map((source) => ({ + label: transformToUpperCase(source), + value: source, + })) + : dataSourceMap.map((source) => ({ + label: transformToUpperCase(source), + value: source, + })); return ( void; + isListViewPanel?: boolean; }; export type OrderByFilterValue = { diff --git a/frontend/src/container/QueryBuilder/filters/OrderByFilter/OrderByFilter.tsx b/frontend/src/container/QueryBuilder/filters/OrderByFilter/OrderByFilter.tsx index 2f707ec7a1..dc6fb964fa 100644 --- a/frontend/src/container/QueryBuilder/filters/OrderByFilter/OrderByFilter.tsx +++ b/frontend/src/container/QueryBuilder/filters/OrderByFilter/OrderByFilter.tsx @@ -11,6 +11,7 @@ import { useOrderByFilter } from './useOrderByFilter'; export function OrderByFilter({ query, onChange, + isListViewPanel = false, }: OrderByFilterProps): JSX.Element { const { debouncedSearchText, @@ -30,7 +31,7 @@ export function OrderByFilter({ searchText: debouncedSearchText, }, { - enabled: !!query.aggregateAttribute.key, + enabled: !!query.aggregateAttribute.key || isListViewPanel, keepPreviousData: true, }, ); diff --git a/frontend/src/container/TracesExplorer/Controls/styles.ts b/frontend/src/container/TracesExplorer/Controls/styles.ts index be74f2db18..d9810d1f4c 100644 --- a/frontend/src/container/TracesExplorer/Controls/styles.ts +++ b/frontend/src/container/TracesExplorer/Controls/styles.ts @@ -4,6 +4,5 @@ export const Container = styled.div` display: flex; align-items: center; justify-content: flex-end; - gap: 0.5rem; - margin: 4px 0; + gap: 0.3rem; `; diff --git a/frontend/src/container/TracesExplorer/ListView/index.tsx b/frontend/src/container/TracesExplorer/ListView/index.tsx index c9dd78df2f..4f18bb3a27 100644 --- a/frontend/src/container/TracesExplorer/ListView/index.tsx +++ b/frontend/src/container/TracesExplorer/ListView/index.tsx @@ -4,6 +4,7 @@ import { QueryParams } from 'constants/query'; import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import { useOptionsMenu } from 'container/OptionsMenu'; +import TraceExplorerControls from 'container/TracesExplorer/Controls'; import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { Pagination } from 'hooks/queryPagination'; @@ -18,7 +19,6 @@ import { AppState } from 'store/reducers'; import { DataSource } from 'types/common/queryBuilder'; import { GlobalReducer } from 'types/reducer/globalTime'; -import TraceExplorerControls from '../Controls'; import { defaultSelectedColumns, PER_PAGE_OPTIONS } from './configs'; import { Container, ErrorText, tableStyles } from './styles'; import { getListColumns, getTraceLink, transformDataWithDate } from './utils'; diff --git a/frontend/src/container/TracesExplorer/ListView/styles.ts b/frontend/src/container/TracesExplorer/ListView/styles.ts index 292b04b1f9..9410a9b90e 100644 --- a/frontend/src/container/TracesExplorer/ListView/styles.ts +++ b/frontend/src/container/TracesExplorer/ListView/styles.ts @@ -3,7 +3,7 @@ import { CSSProperties } from 'react'; import styled from 'styled-components'; export const tableStyles: CSSProperties = { - cursor: 'pointer', + cursor: 'unset', }; export const Container = styled.div` diff --git a/frontend/src/container/TracesExplorer/ListView/utils.tsx b/frontend/src/container/TracesExplorer/ListView/utils.tsx index 6a28acfba9..6a8ef4fc4c 100644 --- a/frontend/src/container/TracesExplorer/ListView/utils.tsx +++ b/frontend/src/container/TracesExplorer/ListView/utils.tsx @@ -3,14 +3,11 @@ import { ColumnsType } from 'antd/es/table'; import ROUTES from 'constants/routes'; import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util'; import { formUrlParams } from 'container/TraceDetail/utils'; -import dayjs from 'dayjs'; import { RowData } from 'lib/query/createTableColumnsFromQuery'; import { ILog } from 'types/api/logs/log'; import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { QueryDataV3 } from 'types/api/widgets/getQuery'; -import { DateText } from './styles'; - export const transformDataWithDate = ( data: QueryDataV3[], ): Omit[] => @@ -27,28 +24,14 @@ export const getTraceLink = (record: RowData): string => export const getListColumns = ( selectedColumns: BaseAutocompleteData[], ): ColumnsType => { - const initialColumns: ColumnsType = [ - { - title: 'date', - dataIndex: 'date', - key: 'date', - width: 145, - render: (date: string): JSX.Element => { - const day = dayjs(date); - return ( - - {day.format('YYYY/MM/DD HH:mm:ss')} - - ); - }, - }, - ]; + const initialColumns: ColumnsType = []; const columns: ColumnsType = selectedColumns.map(({ dataType, key, type }) => ({ title: key, dataIndex: key, key: `${key}-${dataType}-${type}`, + width: 145, render: (value): JSX.Element => { if (value === '') { return N/A; @@ -68,6 +51,7 @@ export const getListColumns = ( return {value}; }, + responsive: ['md'], })) || []; return [...initialColumns, ...columns]; diff --git a/frontend/src/container/TracesTableComponent/TracesTableComponent.styles.scss b/frontend/src/container/TracesTableComponent/TracesTableComponent.styles.scss new file mode 100644 index 0000000000..e1ff9ba437 --- /dev/null +++ b/frontend/src/container/TracesTableComponent/TracesTableComponent.styles.scss @@ -0,0 +1,65 @@ +.traces-table { + position: relative; + display: flex; + flex-direction: column; + height: 100%; + + .resize-table { + height: calc(90% - 5px); + overflow: scroll; + + .ant-table-wrapper .ant-table-tbody >tr >td { + border: none; + background-color: transparent; + color: var(--bg-vanilla-100); + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 18px; + padding: 10px 8px; + font-family: Inter; + cursor: pointer; + } + + .ant-table-wrapper .ant-table-thead > tr > th { + font-family: Inter; + color: var(--bg-vanilla-100); + background-color: transparent; + border: none; + border-bottom: 0.5px solid var(--bg-slate-400); + font-size: 14px; + font-style: normal; + font-weight: 600; + line-height: 22px; + letter-spacing: 0.5px; + padding: 8px; + } + + .ant-table-wrapper .ant-table-thead > tr > th::before { + display: none; + } + } + + .controller { + position: absolute; + bottom: 5px; + right: 10px; + } +} + +.lightMode { + .traces-table { + .resize-table { + .ant-table-wrapper .ant-table-tbody >tr >td { + background-color: var(--bg-vanilla-100); + color: var(--bg-ink-500); + border-color: rgba(0, 0, 0, 0.06); + } + .ant-table-wrapper .ant-table-thead > tr > th { + background-color: var(--bg-vanilla-300); + color: var(--bg-ink-500); + border-color: rgba(0, 0, 0, 0.06); + } + } + } +} \ No newline at end of file diff --git a/frontend/src/container/TracesTableComponent/TracesTableComponent.tsx b/frontend/src/container/TracesTableComponent/TracesTableComponent.tsx new file mode 100644 index 0000000000..a59303780c --- /dev/null +++ b/frontend/src/container/TracesTableComponent/TracesTableComponent.tsx @@ -0,0 +1,170 @@ +import './TracesTableComponent.styles.scss'; + +import { Table } from 'antd'; +// import { ResizeTable } from 'components/ResizeTable'; +import { SOMETHING_WENT_WRONG } from 'constants/api'; +import { PANEL_TYPES } from 'constants/queryBuilder'; +import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; +import Controls from 'container/Controls'; +import { timePreferance } from 'container/NewWidget/RightContainer/timeItems'; +import { PER_PAGE_OPTIONS } from 'container/TracesExplorer/ListView/configs'; +import { tableStyles } from 'container/TracesExplorer/ListView/styles'; +import { + getListColumns, + getTraceLink, + transformDataWithDate, +} from 'container/TracesExplorer/ListView/utils'; +import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange'; +import { Pagination } from 'hooks/queryPagination'; +import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables'; +import history from 'lib/history'; +import { RowData } from 'lib/query/createTableColumnsFromQuery'; +import { useDashboard } from 'providers/Dashboard/Dashboard'; +import { HTMLAttributes, useCallback, useMemo, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; +import { Widgets } from 'types/api/dashboard/getAll'; +import { Query } from 'types/api/queryBuilder/queryBuilderData'; +import { GlobalReducer } from 'types/reducer/globalTime'; + +function TracesTableComponent({ + selectedTracesFields, + query, + selectedTime, +}: TracesTableComponentProps): JSX.Element { + const { selectedTime: globalSelectedTime, maxTime, minTime } = useSelector< + AppState, + GlobalReducer + >((state) => state.globalTime); + + const [pagination, setPagination] = useState({ + offset: 0, + limit: 10, + }); + + const { selectedDashboard } = useDashboard(); + + const { data, isFetching, isError } = useGetQueryRange( + { + query, + graphType: PANEL_TYPES.LIST, + selectedTime: selectedTime?.enum || 'GLOBAL_TIME', + globalSelectedInterval: globalSelectedTime, + params: { + dataSource: 'traces', + }, + tableParams: { + pagination, + selectColumns: selectedTracesFields, + }, + variables: getDashboardVariables(selectedDashboard?.data.variables), + }, + { + queryKey: [ + REACT_QUERY_KEY.GET_QUERY_RANGE, + globalSelectedTime, + maxTime, + minTime, + query, + pagination, + selectedTracesFields?.length, + selectedTime?.enum, + selectedDashboard?.data.variables, + ], + enabled: !!query && !!selectedTracesFields?.length, + }, + ); + + const columns = getListColumns(selectedTracesFields || []); + + const dataLength = + data?.payload?.data?.newResult?.data?.result[0]?.list?.length; + const totalCount = useMemo(() => dataLength || 0, [dataLength]); + + const queryTableDataResult = data?.payload.data.newResult.data.result; + const queryTableData = useMemo(() => queryTableDataResult || [], [ + queryTableDataResult, + ]); + + const transformedQueryTableData = useMemo( + () => ((transformDataWithDate(queryTableData) || []) as unknown) as RowData[], + [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)); + } + }, + }), + [], + ); + + if (isError) { + return
{SOMETHING_WENT_WRONG}
; + } + + return ( +
+
+
+ +
+ { + setPagination({ + ...pagination, + offset: pagination.offset - pagination.limit, + }); + }} + handleNavigateNext={(): void => { + setPagination({ + ...pagination, + offset: pagination.offset + pagination.limit, + }); + }} + handleCountItemsPerPageChange={(value): void => { + setPagination({ + ...pagination, + limit: value, + offset: 0, + }); + }} + /> +
+ + ); +} + +export type TracesTableComponentProps = { + selectedTracesFields: Widgets['selectedTracesFields']; + query: Query; + selectedTime?: timePreferance; +}; + +TracesTableComponent.defaultProps = { + selectedTime: undefined, +}; + +export default TracesTableComponent; diff --git a/frontend/src/hooks/dashboard/utils.ts b/frontend/src/hooks/dashboard/utils.ts index 4dfb8ce9c3..a66204ae62 100644 --- a/frontend/src/hooks/dashboard/utils.ts +++ b/frontend/src/hooks/dashboard/utils.ts @@ -35,6 +35,8 @@ export const addEmptyWidgetInDashboardJSONWithQuery = ( panelTypes: panelTypes || PANEL_TYPES.TIME_SERIES, softMax: null, softMin: null, + selectedLogFields: [], + selectedTracesFields: [], }, ], }, diff --git a/frontend/src/hooks/queryBuilder/useQueryBuilderOperations.ts b/frontend/src/hooks/queryBuilder/useQueryBuilderOperations.ts index 8e883852cb..799640da4e 100644 --- a/frontend/src/hooks/queryBuilder/useQueryBuilderOperations.ts +++ b/frontend/src/hooks/queryBuilder/useQueryBuilderOperations.ts @@ -6,6 +6,10 @@ import { mapOfQueryFilters, PANEL_TYPES, } from 'constants/queryBuilder'; +import { + listViewInitialLogQuery, + listViewInitialTraceQuery, +} from 'container/NewDashboard/ComponentsSlider/constants'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { getOperatorsBySourceAndPanelType } from 'lib/newQueryBuilder/getOperatorsBySourceAndPanelType'; import { findDataTypeOfOperator } from 'lib/query/findDataTypeOfOperator'; @@ -29,6 +33,7 @@ export const useQueryOperations: UseQueryOperations = ({ index, filterConfigs, formula, + isListViewPanel = false, }) => { const { handleSetQueryData, @@ -37,6 +42,7 @@ export const useQueryOperations: UseQueryOperations = ({ panelType, initialDataSource, currentQuery, + redirectWithQueryBuilderData, } = useQueryBuilder(); const [operators, setOperators] = useState[]>([]); @@ -125,6 +131,14 @@ export const useQueryOperations: UseQueryOperations = ({ const handleChangeDataSource = useCallback( (nextSource: DataSource): void => { + if (isListViewPanel) { + if (nextSource === DataSource.LOGS) { + redirectWithQueryBuilderData(listViewInitialLogQuery); + } else if (nextSource === DataSource.TRACES) { + redirectWithQueryBuilderData(listViewInitialTraceQuery); + } + } + const newOperators = getOperatorsBySourceAndPanelType({ dataSource: nextSource, panelType: panelType || PANEL_TYPES.TIME_SERIES, @@ -146,7 +160,14 @@ export const useQueryOperations: UseQueryOperations = ({ setOperators(newOperators); handleSetQueryData(index, newQuery); }, - [index, query, panelType, handleSetQueryData], + [ + isListViewPanel, + panelType, + query, + handleSetQueryData, + index, + redirectWithQueryBuilderData, + ], ); const handleDeleteQuery = useCallback(() => { diff --git a/frontend/src/hooks/useLogsData.ts b/frontend/src/hooks/useLogsData.ts new file mode 100644 index 0000000000..6105c03cbf --- /dev/null +++ b/frontend/src/hooks/useLogsData.ts @@ -0,0 +1,196 @@ +import { QueryParams } from 'constants/query'; +import { + initialQueryBuilderFormValues, + PANEL_TYPES, +} from 'constants/queryBuilder'; +import { DEFAULT_PER_PAGE_VALUE } from 'container/Controls/config'; +import { getPaginationQueryData } from 'lib/newQueryBuilder/getPaginationQueryData'; +import { useEffect, useMemo, useState } from 'react'; +import { ILog } from 'types/api/logs/log'; +import { + IBuilderQuery, + OrderByPayload, + Query, + TagFilter, +} from 'types/api/queryBuilder/queryBuilderData'; +import { QueryDataV3 } from 'types/api/widgets/getQuery'; + +import { LogTimeRange } from './logs/types'; +import { useCopyLogLink } from './logs/useCopyLogLink'; +import { useGetExplorerQueryRange } from './queryBuilder/useGetExplorerQueryRange'; +import useUrlQueryData from './useUrlQueryData'; + +export const useLogsData = ({ + result, + panelType, + stagedQuery, +}: { + result: QueryDataV3[] | undefined; + panelType: PANEL_TYPES; + stagedQuery: Query | null; +}): { + logs: ILog[]; + handleEndReached: (index: number) => void; + isFetching: boolean; +} => { + const [logs, setLogs] = useState([]); + const [page, setPage] = useState(1); + const [requestData, setRequestData] = useState(null); + const [shouldLoadMoreLogs, setShouldLoadMoreLogs] = useState(false); + + const { queryData: pageSize } = useUrlQueryData( + QueryParams.pageSize, + DEFAULT_PER_PAGE_VALUE, + ); + + const listQuery = useMemo(() => { + if (!stagedQuery || stagedQuery?.builder?.queryData?.length < 1) return null; + + return stagedQuery.builder?.queryData.find((item) => !item.disabled) || null; + }, [stagedQuery]); + + const isLimit: boolean = useMemo(() => { + if (!listQuery) return false; + if (!listQuery.limit) return false; + + return logs.length >= listQuery.limit; + }, [logs.length, listQuery]); + + const orderByTimestamp: OrderByPayload | null = useMemo(() => { + const timestampOrderBy = listQuery?.orderBy.find( + (item) => item.columnName === 'timestamp', + ); + + return timestampOrderBy || null; + }, [listQuery]); + + useEffect(() => { + if (panelType !== PANEL_TYPES.LIST) return; + const currentData = result || []; + if (currentData.length > 0 && currentData[0].list) { + const currentLogs: ILog[] = currentData[0].list.map((item) => ({ + ...item.data, + timestamp: item.timestamp, + })); + const newLogs = [...currentLogs]; + + setLogs(newLogs); + } else { + setLogs([]); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [result]); + + const getRequestData = ( + query: Query | null, + params: { + page: number; + log: ILog | null; + pageSize: number; + filters: TagFilter; + }, + ): Query | null => { + if (!query) return null; + + const paginateData = getPaginationQueryData({ + filters: params.filters, + listItemId: params.log ? params.log.id : null, + orderByTimestamp, + page: params.page, + pageSize: params.pageSize, + }); + + const queryData: IBuilderQuery[] = + query.builder.queryData.length > 1 + ? query.builder.queryData + : [ + { + ...(listQuery || initialQueryBuilderFormValues), + ...paginateData, + }, + ]; + + const data: Query = { + ...query, + builder: { + ...query.builder, + queryData, + }, + }; + + return data; + }; + + const { activeLogId, timeRange, onTimeRangeChange } = useCopyLogLink(); + + const { data, isFetching } = useGetExplorerQueryRange( + requestData, + panelType, + { + keepPreviousData: true, + enabled: !isLimit && !!requestData, + }, + { + ...(timeRange && + activeLogId && + !logs.length && { + start: timeRange.start, + end: timeRange.end, + }), + }, + shouldLoadMoreLogs, + ); + + useEffect(() => { + const currentParams = data?.params as Omit; + const currentData = data?.payload.data.newResult.data.result || []; + if (currentData.length > 0 && currentData[0].list) { + const currentLogs: ILog[] = currentData[0].list.map((item) => ({ + ...item.data, + timestamp: item.timestamp, + })); + const newLogs = [...logs, ...currentLogs]; + + setLogs(newLogs); + onTimeRangeChange({ + start: currentParams?.start, + end: timeRange?.end || currentParams?.end, + pageSize: newLogs.length, + }); + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [data]); + + const handleEndReached = (index: number): void => { + if (!listQuery) return; + + if (isLimit) return; + if (logs.length < pageSize) return; + + const { limit, filters } = listQuery; + + const lastLog = logs[index]; + + const nextLogsLength = logs.length + pageSize; + + const nextPageSize = + limit && nextLogsLength >= limit ? limit - logs.length : pageSize; + + if (!stagedQuery) return; + + const newRequestData = getRequestData(stagedQuery, { + filters, + page: page + 1, + log: orderByTimestamp ? lastLog : null, + pageSize: nextPageSize, + }); + + setPage((prevPage) => prevPage + 1); + + setRequestData(newRequestData); + setShouldLoadMoreLogs(true); + }; + + return { logs, handleEndReached, isFetching }; +}; diff --git a/frontend/src/types/api/dashboard/getAll.ts b/frontend/src/types/api/dashboard/getAll.ts index bb302c152b..2111d3d57b 100644 --- a/frontend/src/types/api/dashboard/getAll.ts +++ b/frontend/src/types/api/dashboard/getAll.ts @@ -5,6 +5,9 @@ import { ReactNode } from 'react'; import { Layout } from 'react-grid-layout'; import { Query } from 'types/api/queryBuilder/queryBuilderData'; +import { IField } from '../logs/fields'; +import { BaseAutocompleteData } from '../queryBuilder/queryAutocompleteResponse'; + export type PayloadProps = Dashboard[]; export const VariableQueryTypeArr = ['QUERY', 'TEXTBOX', 'CUSTOM'] as const; @@ -76,6 +79,8 @@ export interface IBaseWidget { softMin: number | null; softMax: number | null; fillSpans?: boolean; + selectedLogFields: IField[] | null; + selectedTracesFields: BaseAutocompleteData[] | null; } export interface Widgets extends IBaseWidget { query: Query; diff --git a/frontend/src/types/common/operations.types.ts b/frontend/src/types/common/operations.types.ts index b1a21ce385..58fd4533b9 100644 --- a/frontend/src/types/common/operations.types.ts +++ b/frontend/src/types/common/operations.types.ts @@ -12,6 +12,7 @@ import { SelectOption } from './select'; type UseQueryOperationsParams = Pick & Pick & { formula?: IBuilderFormula; + isListViewPanel?: boolean; }; export type HandleChangeQueryData = < diff --git a/frontend/yarn.lock b/frontend/yarn.lock index fd1b081eb0..8e1c80fad2 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -4222,6 +4222,13 @@ dependencies: "@types/react" "*" +"@types/react-beautiful-dnd@13.1.8": + version "13.1.8" + resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.8.tgz#f52d3ea07e1e19159d6c3c4a48c8da3d855e60b4" + integrity sha512-E3TyFsro9pQuK4r8S/OL6G99eq7p8v29sX0PM7oT8Z+PJfZvSQTx4zTQbUJ+QZXioAF0e7TGBEcA1XhYhCweyQ== + dependencies: + "@types/react" "*" + "@types/react-dom@18.0.10", "@types/react-dom@^18.0.0": version "18.0.10" resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.10.tgz" @@ -6809,7 +6816,7 @@ critters@^0.0.16: cross-env@^7.0.3: version "7.0.3" - resolved "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== dependencies: cross-spawn "^7.0.1" @@ -6846,6 +6853,13 @@ crypto-random-string@^2.0.0: resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== +css-box-model@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1" + integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw== + dependencies: + tiny-invariant "^1.0.6" + css-color-keywords@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz" @@ -11697,6 +11711,11 @@ memfs@^3.4.3: dependencies: fs-monkey "^1.0.3" +memoize-one@^5.1.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" + integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== + memorystream@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2" @@ -13749,6 +13768,11 @@ quickselect@^2.0.0: resolved "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz" integrity sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw== +raf-schd@^4.0.2: + version "4.0.3" + resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a" + integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ== + randombytes@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz" @@ -14179,6 +14203,19 @@ react-addons-update@15.6.3: dependencies: object-assign "^4.1.0" +react-beautiful-dnd@13.1.1: + version "13.1.1" + resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz#b0f3087a5840920abf8bb2325f1ffa46d8c4d0a2" + integrity sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ== + dependencies: + "@babel/runtime" "^7.9.2" + css-box-model "^1.2.0" + memoize-one "^5.1.1" + raf-schd "^4.0.2" + react-redux "^7.2.0" + redux "^4.0.4" + use-memo-one "^1.1.1" + react-dnd-html5-backend@16.0.1: version "16.0.1" resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz#87faef15845d512a23b3c08d29ecfd34871688b6" @@ -14361,7 +14398,7 @@ react-query@3.39.3: broadcast-channel "^3.4.1" match-sorter "^6.0.2" -react-redux@^7.2.2: +react-redux@^7.2.0, react-redux@^7.2.2: version "7.2.9" resolved "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz" integrity sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ== @@ -14556,7 +14593,7 @@ redux-thunk@^2.3.0: resolved "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz" integrity sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q== -redux@^4.0.0, redux@^4.0.5, redux@^4.2.0: +redux@^4.0.0, redux@^4.0.4, redux@^4.0.5, redux@^4.2.0: version "4.2.1" resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197" integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w== @@ -16162,7 +16199,7 @@ timestamp-nano@^1.0.0: resolved "https://registry.npmjs.org/timestamp-nano/-/timestamp-nano-1.0.1.tgz" integrity sha512-4oGOVZWTu5sl89PtCDnhQBSt7/vL1zVEwAfxH1p49JhTosxzVQWYBYFRFZ8nJmo0G6f824iyP/44BFAwIoKvIA== -tiny-invariant@^1.0.2: +tiny-invariant@^1.0.2, tiny-invariant@^1.0.6: version "1.3.1" resolved "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz" integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw== @@ -16731,6 +16768,11 @@ use-isomorphic-layout-effect@^1.1.2: resolved "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz" integrity sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA== +use-memo-one@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.3.tgz#2fd2e43a2169eabc7496960ace8c79efef975e99" + integrity sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ== + use-sync-external-store@^1.0.0: version "1.2.0" resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz" From 0cb60e1c105a11f680b875172927eac0275569f0 Mon Sep 17 00:00:00 2001 From: Vishal Sharma Date: Wed, 21 Feb 2024 14:49:33 +0530 Subject: [PATCH 14/16] chore: update heartbeat, language event and add serviceName event (#4571) * chore: update heartbeat, language event and add serviceName event * chore: update tagsInfo --- .../app/clickhouseReader/reader.go | 44 ++++++++++++++----- pkg/query-service/interfaces/interface.go | 11 +++-- pkg/query-service/model/response.go | 1 + pkg/query-service/telemetry/telemetry.go | 28 +++++++++--- 4 files changed, 63 insertions(+), 21 deletions(-) diff --git a/pkg/query-service/app/clickhouseReader/reader.go b/pkg/query-service/app/clickhouseReader/reader.go index e07ef55b3a..1834aa0ff0 100644 --- a/pkg/query-service/app/clickhouseReader/reader.go +++ b/pkg/query-service/app/clickhouseReader/reader.go @@ -3273,17 +3273,27 @@ func (r *ClickHouseReader) GetTotalSpans(ctx context.Context) (uint64, error) { return totalSpans, nil } -func (r *ClickHouseReader) GetSpansInLastHeartBeatInterval(ctx context.Context) (uint64, error) { +func (r *ClickHouseReader) GetSpansInLastHeartBeatInterval(ctx context.Context, interval time.Duration) (uint64, error) { var spansInLastHeartBeatInterval uint64 - queryStr := fmt.Sprintf("SELECT count() from %s.%s where timestamp > toUnixTimestamp(now()-toIntervalMinute(%d));", signozTraceDBName, signozSpansTable, 30) + queryStr := fmt.Sprintf("SELECT count() from %s.%s where timestamp > toUnixTimestamp(now()-toIntervalMinute(%d));", signozTraceDBName, signozSpansTable, int(interval.Minutes())) r.db.QueryRow(ctx, queryStr).Scan(&spansInLastHeartBeatInterval) return spansInLastHeartBeatInterval, nil } +func (r *ClickHouseReader) GetTotalLogs(ctx context.Context) (uint64, error) { + + var totalLogs uint64 + + queryStr := fmt.Sprintf("SELECT count() from %s.%s;", r.logsDB, r.logsTable) + r.db.QueryRow(ctx, queryStr).Scan(&totalLogs) + + return totalLogs, nil +} + func (r *ClickHouseReader) FetchTemporality(ctx context.Context, metricNames []string) (map[string]map[v3.Temporality]bool, error) { metricNameToTemporality := make(map[string]map[v3.Temporality]bool) @@ -3312,9 +3322,7 @@ func (r *ClickHouseReader) FetchTemporality(ctx context.Context, metricNames []s func (r *ClickHouseReader) GetTimeSeriesInfo(ctx context.Context) (map[string]interface{}, error) { - queryStr := fmt.Sprintf("SELECT count() as count from %s.%s group by metric_name order by count desc;", signozMetricDBName, signozTSTableName) - - // r.db.Select(ctx, &tsByMetricName, queryStr) + queryStr := fmt.Sprintf("SELECT count() as count from %s.%s where metric_name not like 'signoz_%%' group by metric_name order by count desc;", signozMetricDBName, signozTSTableName) rows, _ := r.db.Query(ctx, queryStr) @@ -3343,11 +3351,21 @@ func (r *ClickHouseReader) GetTimeSeriesInfo(ctx context.Context) (map[string]in return timeSeriesData, nil } -func (r *ClickHouseReader) GetSamplesInfoInLastHeartBeatInterval(ctx context.Context) (uint64, error) { +func (r *ClickHouseReader) GetSamplesInfoInLastHeartBeatInterval(ctx context.Context, interval time.Duration) (uint64, error) { var totalSamples uint64 - queryStr := fmt.Sprintf("select count() from %s.%s where timestamp_ms > toUnixTimestamp(now()-toIntervalMinute(%d))*1000;", signozMetricDBName, signozSampleTableName, 30) + queryStr := fmt.Sprintf("select count() from %s.%s where metric_name not like 'signoz_%%' and timestamp_ms > toUnixTimestamp(now()-toIntervalMinute(%d))*1000;", signozMetricDBName, signozSampleTableName, int(interval.Minutes())) + + r.db.QueryRow(ctx, queryStr).Scan(&totalSamples) + + return totalSamples, nil +} + +func (r *ClickHouseReader) GetTotalSamples(ctx context.Context) (uint64, error) { + var totalSamples uint64 + + queryStr := fmt.Sprintf("select count() from %s.%s where metric_name not like 'signoz_%%';", signozMetricDBName, signozSampleTableName) r.db.QueryRow(ctx, queryStr).Scan(&totalSamples) @@ -3367,23 +3385,23 @@ func (r *ClickHouseReader) GetDistributedInfoInLastHeartBeatInterval(ctx context return nil, nil } -func (r *ClickHouseReader) GetLogsInfoInLastHeartBeatInterval(ctx context.Context) (uint64, error) { +func (r *ClickHouseReader) GetLogsInfoInLastHeartBeatInterval(ctx context.Context, interval time.Duration) (uint64, error) { var totalLogLines uint64 - queryStr := fmt.Sprintf("select count() from %s.%s where timestamp > toUnixTimestamp(now()-toIntervalMinute(%d))*1000000000;", r.logsDB, r.logsTable, 30) + queryStr := fmt.Sprintf("select count() from %s.%s where timestamp > toUnixTimestamp(now()-toIntervalMinute(%d))*1000000000;", r.logsDB, r.logsTable, int(interval.Minutes())) err := r.db.QueryRow(ctx, queryStr).Scan(&totalLogLines) return totalLogLines, err } -func (r *ClickHouseReader) GetTagsInfoInLastHeartBeatInterval(ctx context.Context) (*model.TagsInfo, error) { +func (r *ClickHouseReader) GetTagsInfoInLastHeartBeatInterval(ctx context.Context, interval time.Duration) (*model.TagsInfo, error) { queryStr := fmt.Sprintf(`select serviceName, stringTagMap['deployment.environment'] as env, stringTagMap['telemetry.sdk.language'] as language from %s.%s where timestamp > toUnixTimestamp(now()-toIntervalMinute(%d)) - group by serviceName, env, language;`, r.TraceDB, r.indexTable, 1) + group by serviceName, env, language;`, r.TraceDB, r.indexTable, int(interval.Minutes())) tagTelemetryDataList := []model.TagTelemetryData{} err := r.db.Select(ctx, &tagTelemetryDataList, queryStr) @@ -3396,6 +3414,7 @@ func (r *ClickHouseReader) GetTagsInfoInLastHeartBeatInterval(ctx context.Contex tagsInfo := model.TagsInfo{ Languages: make(map[string]interface{}), + Services: make(map[string]interface{}), } for _, tagTelemetryData := range tagTelemetryDataList { @@ -3409,6 +3428,9 @@ func (r *ClickHouseReader) GetTagsInfoInLastHeartBeatInterval(ctx context.Contex if len(tagTelemetryData.Language) != 0 { tagsInfo.Languages[tagTelemetryData.Language] = struct{}{} } + if len(tagTelemetryData.ServiceName) != 0 { + tagsInfo.Services[tagTelemetryData.ServiceName] = struct{}{} + } } diff --git a/pkg/query-service/interfaces/interface.go b/pkg/query-service/interfaces/interface.go index e15b1db67e..9d0d65c39c 100644 --- a/pkg/query-service/interfaces/interface.go +++ b/pkg/query-service/interfaces/interface.go @@ -2,6 +2,7 @@ package interfaces import ( "context" + "time" "github.com/ClickHouse/clickhouse-go/v2" "github.com/prometheus/prometheus/promql" @@ -74,11 +75,13 @@ type Reader interface { GetDashboardsInfo(ctx context.Context) (*model.DashboardsInfo, error) GetAlertsInfo(ctx context.Context) (*model.AlertsInfo, error) GetTotalSpans(ctx context.Context) (uint64, error) - GetSpansInLastHeartBeatInterval(ctx context.Context) (uint64, error) + GetTotalLogs(ctx context.Context) (uint64, error) + GetTotalSamples(ctx context.Context) (uint64, error) + GetSpansInLastHeartBeatInterval(ctx context.Context, interval time.Duration) (uint64, error) GetTimeSeriesInfo(ctx context.Context) (map[string]interface{}, error) - GetSamplesInfoInLastHeartBeatInterval(ctx context.Context) (uint64, error) - GetLogsInfoInLastHeartBeatInterval(ctx context.Context) (uint64, error) - GetTagsInfoInLastHeartBeatInterval(ctx context.Context) (*model.TagsInfo, error) + GetSamplesInfoInLastHeartBeatInterval(ctx context.Context, interval time.Duration) (uint64, error) + GetLogsInfoInLastHeartBeatInterval(ctx context.Context, interval time.Duration) (uint64, error) + GetTagsInfoInLastHeartBeatInterval(ctx context.Context, interval time.Duration) (*model.TagsInfo, error) GetDistributedInfoInLastHeartBeatInterval(ctx context.Context) (map[string]interface{}, error) // Logs GetLogFields(ctx context.Context) (*model.GetFieldsResponse, *model.ApiError) diff --git a/pkg/query-service/model/response.go b/pkg/query-service/model/response.go index 6d5e65d732..ae99473720 100644 --- a/pkg/query-service/model/response.go +++ b/pkg/query-service/model/response.go @@ -613,6 +613,7 @@ type DashboardVar struct { type TagsInfo struct { Languages map[string]interface{} `json:"languages"` Env string `json:"env"` + Services map[string]interface{} `json:"services"` } type AlertsInfo struct { diff --git a/pkg/query-service/telemetry/telemetry.go b/pkg/query-service/telemetry/telemetry.go index a840760ac2..ea93d75a0a 100644 --- a/pkg/query-service/telemetry/telemetry.go +++ b/pkg/query-service/telemetry/telemetry.go @@ -35,6 +35,7 @@ const ( TELEMETRY_LICENSE_ACT_FAILED = "License Activation Failed" TELEMETRY_EVENT_ENVIRONMENT = "Environment" TELEMETRY_EVENT_LANGUAGE = "Language" + TELEMETRY_EVENT_SERVICE = "ServiceName" TELEMETRY_EVENT_LOGS_FILTERS = "Logs Filters" TELEMETRY_EVENT_DISTRIBUTED = "Distributed" TELEMETRY_EVENT_QUERY_RANGE_V3 = "Query Range V3 Metadata" @@ -51,6 +52,7 @@ var SAAS_EVENTS_LIST = map[string]struct{}{ TELEMETRY_EVENT_ACTIVE_USER: {}, TELEMETRY_EVENT_HEART_BEAT: {}, TELEMETRY_EVENT_LANGUAGE: {}, + TELEMETRY_EVENT_SERVICE: {}, TELEMETRY_EVENT_ENVIRONMENT: {}, TELEMETRY_EVENT_USER_INVITATION_SENT: {}, TELEMETRY_EVENT_USER_INVITATION_ACCEPTED: {}, @@ -201,11 +203,17 @@ func createTelemetry() { select { case <-activeUserTicker.C: if telemetry.activeUser["logs"] != 0 { - getLogsInfoInLastHeartBeatInterval, err := telemetry.reader.GetLogsInfoInLastHeartBeatInterval(context.Background()) + getLogsInfoInLastHeartBeatInterval, err := telemetry.reader.GetLogsInfoInLastHeartBeatInterval(context.Background(), ACTIVE_USER_DURATION) if err != nil && getLogsInfoInLastHeartBeatInterval == 0 { telemetry.activeUser["logs"] = 0 } } + if telemetry.activeUser["metrics"] != 0 { + getSamplesInfoInLastHeartBeatInterval, err := telemetry.reader.GetSamplesInfoInLastHeartBeatInterval(context.Background(), ACTIVE_USER_DURATION) + if err != nil && getSamplesInfoInLastHeartBeatInterval == 0 { + telemetry.activeUser["metrics"] = 0 + } + } if (telemetry.activeUser["traces"] != 0) || (telemetry.activeUser["metrics"] != 0) || (telemetry.activeUser["logs"] != 0) { telemetry.activeUser["any"] = 1 } @@ -214,7 +222,7 @@ func createTelemetry() { case <-ticker.C: - tagsInfo, _ := telemetry.reader.GetTagsInfoInLastHeartBeatInterval(context.Background()) + tagsInfo, _ := telemetry.reader.GetTagsInfoInLastHeartBeatInterval(context.Background(), HEART_BEAT_DURATION) if len(tagsInfo.Env) != 0 { telemetry.SendEvent(TELEMETRY_EVENT_ENVIRONMENT, map[string]interface{}{"value": tagsInfo.Env}, "") @@ -224,12 +232,18 @@ func createTelemetry() { telemetry.SendEvent(TELEMETRY_EVENT_LANGUAGE, map[string]interface{}{"language": language}, "") } + for service, _ := range tagsInfo.Services { + telemetry.SendEvent(TELEMETRY_EVENT_SERVICE, map[string]interface{}{"serviceName": service}, "") + } + totalSpans, _ := telemetry.reader.GetTotalSpans(context.Background()) - spansInLastHeartBeatInterval, _ := telemetry.reader.GetSpansInLastHeartBeatInterval(context.Background()) - getSamplesInfoInLastHeartBeatInterval, _ := telemetry.reader.GetSamplesInfoInLastHeartBeatInterval(context.Background()) + totalLogs, _ := telemetry.reader.GetTotalLogs(context.Background()) + spansInLastHeartBeatInterval, _ := telemetry.reader.GetSpansInLastHeartBeatInterval(context.Background(), HEART_BEAT_DURATION) + getSamplesInfoInLastHeartBeatInterval, _ := telemetry.reader.GetSamplesInfoInLastHeartBeatInterval(context.Background(), HEART_BEAT_DURATION) + totalSamples, _ := telemetry.reader.GetTotalSamples(context.Background()) tsInfo, _ := telemetry.reader.GetTimeSeriesInfo(context.Background()) - getLogsInfoInLastHeartBeatInterval, _ := telemetry.reader.GetLogsInfoInLastHeartBeatInterval(context.Background()) + getLogsInfoInLastHeartBeatInterval, _ := telemetry.reader.GetLogsInfoInLastHeartBeatInterval(context.Background(), HEART_BEAT_DURATION) traceTTL, _ := telemetry.reader.GetTTL(context.Background(), &model.GetTTLParams{Type: constants.TraceTTL}) metricsTTL, _ := telemetry.reader.GetTTL(context.Background(), &model.GetTTLParams{Type: constants.MetricsTTL}) @@ -238,7 +252,9 @@ func createTelemetry() { data := map[string]interface{}{ "totalSpans": totalSpans, "spansInLastHeartBeatInterval": spansInLastHeartBeatInterval, + "totalSamples": totalSamples, "getSamplesInfoInLastHeartBeatInterval": getSamplesInfoInLastHeartBeatInterval, + "totalLogs": totalLogs, "getLogsInfoInLastHeartBeatInterval": getLogsInfoInLastHeartBeatInterval, "countUsers": telemetry.countUsers, "metricsTTLStatus": metricsTTL.Status, @@ -269,7 +285,7 @@ func createTelemetry() { "tracesBasedAlerts": alertsInfo.TracesBasedAlerts, } // send event only if there are dashboards or alerts - if dashboardsInfo.TotalDashboards > 0 || alertsInfo.TotalAlerts > 0 { + if dashboardsInfo.TotalDashboards > 0 || alertsInfo.TotalAlerts > 0 { telemetry.SendEvent(TELEMETRY_EVENT_DASHBOARDS_ALERTS, dashboardsAlertsData, "") } } else { From 7bca847f113da8fc529c97485f976d363a156fe8 Mon Sep 17 00:00:00 2001 From: Yunus M Date: Wed, 21 Feb 2024 15:38:18 +0530 Subject: [PATCH 15/16] fix: show expired token label (#4581) * fix: show expired token label * fix: handle no expiry --------- Co-authored-by: Vishal Sharma --- frontend/src/container/APIKeys/APIKeys.tsx | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/frontend/src/container/APIKeys/APIKeys.tsx b/frontend/src/container/APIKeys/APIKeys.tsx index 0540e86954..c24bad7009 100644 --- a/frontend/src/container/APIKeys/APIKeys.tsx +++ b/frontend/src/container/APIKeys/APIKeys.tsx @@ -26,6 +26,7 @@ import updateAPIKeyApi from 'api/APIKeys/updateAPIKey'; import axios, { AxiosError } from 'axios'; import cx from 'classnames'; import { SOMETHING_WENT_WRONG } from 'constants/api'; +import dayjs from 'dayjs'; import { useGetAllAPIKeys } from 'hooks/APIKeys/useGetAllAPIKeys'; import { useNotifications } from 'hooks/useNotifications'; import { @@ -320,10 +321,20 @@ function APIKeys(): JSX.Element { return differenceInSeconds / (60 * 60 * 24); }; + const isExpiredToken = (expiryTimestamp: number): boolean => { + if (expiryTimestamp === 0) { + return false; + } + const currentTime = dayjs(); + const tokenExpiresAt = dayjs.unix(expiryTimestamp); + return tokenExpiresAt.isBefore(currentTime); + }; + const columns: TableProps['columns'] = [ { title: 'API Key', key: 'api-key', + // eslint-disable-next-line sonarjs/cognitive-complexity render: (APIKey: APIKeyProps): JSX.Element => { const formattedDateAndTime = APIKey && APIKey?.lastUsed && APIKey?.lastUsed !== 0 @@ -337,6 +348,8 @@ function APIKeys(): JSX.Element { ? Number.POSITIVE_INFINITY : getDateDifference(APIKey?.createdAt, APIKey?.expiresAt); + const isExpired = isExpiredToken(APIKey.expiresAt); + const expiresOn = !APIKey.expiresAt || APIKey.expiresAt === 0 ? 'No Expiry' @@ -473,7 +486,8 @@ function APIKeys(): JSX.Element { Last used {formattedDateAndTime} - {expiresIn <= EXPIRATION_WITHIN_SEVEN_DAYS && ( + + {!isExpired && expiresIn <= EXPIRATION_WITHIN_SEVEN_DAYS && (
Expires in {expiresIn} Days
)} + + {isExpired && ( +
+ Expired +
+ )} ); From 493aef0241a4cdd0d19d9d92fbe4dd076cf2183e Mon Sep 17 00:00:00 2001 From: Prashant Shahi Date: Wed, 21 Feb 2024 16:22:48 +0545 Subject: [PATCH 16/16] =?UTF-8?q?chore(signoz):=20=F0=9F=93=8C=20pin=20ver?= =?UTF-8?q?sions:=20SigNoz=200.39.1,=20SigNoz=20OtelCollector=200.88.13?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Prashant Shahi --- deploy/docker-swarm/clickhouse-setup/docker-compose.yaml | 8 ++++---- deploy/docker/clickhouse-setup/docker-compose-core.yaml | 4 ++-- deploy/docker/clickhouse-setup/docker-compose.yaml | 8 ++++---- go.mod | 2 +- go.sum | 6 ++---- pkg/query-service/tests/test-deploy/docker-compose.yaml | 4 ++-- 6 files changed, 15 insertions(+), 17 deletions(-) diff --git a/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml b/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml index edc5ead22c..38581568dc 100644 --- a/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml +++ b/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml @@ -146,7 +146,7 @@ services: condition: on-failure query-service: - image: signoz/query-service:0.39.0 + image: signoz/query-service:0.39.1 command: [ "-config=/root/config/prometheus.yml", @@ -186,7 +186,7 @@ services: <<: *db-depend frontend: - image: signoz/frontend:0.39.0 + image: signoz/frontend:0.39.1 deploy: restart_policy: condition: on-failure @@ -199,7 +199,7 @@ services: - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf otel-collector: - image: signoz/signoz-otel-collector:0.88.12 + image: signoz/signoz-otel-collector:0.88.13 command: [ "--config=/etc/otel-collector-config.yaml", @@ -237,7 +237,7 @@ services: - query-service otel-collector-migrator: - image: signoz/signoz-schema-migrator:0.88.12 + image: signoz/signoz-schema-migrator:0.88.13 deploy: restart_policy: condition: on-failure diff --git a/deploy/docker/clickhouse-setup/docker-compose-core.yaml b/deploy/docker/clickhouse-setup/docker-compose-core.yaml index 61e03804f4..525fa5175d 100644 --- a/deploy/docker/clickhouse-setup/docker-compose-core.yaml +++ b/deploy/docker/clickhouse-setup/docker-compose-core.yaml @@ -66,7 +66,7 @@ services: - --storage.path=/data otel-collector-migrator: - image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.12} + image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.13} container_name: otel-migrator command: - "--dsn=tcp://clickhouse:9000" @@ -81,7 +81,7 @@ services: # Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md` otel-collector: container_name: signoz-otel-collector - image: signoz/signoz-otel-collector:0.88.12 + image: signoz/signoz-otel-collector:0.88.13 command: [ "--config=/etc/otel-collector-config.yaml", diff --git a/deploy/docker/clickhouse-setup/docker-compose.yaml b/deploy/docker/clickhouse-setup/docker-compose.yaml index cb77c4c024..b0d11fbaf5 100644 --- a/deploy/docker/clickhouse-setup/docker-compose.yaml +++ b/deploy/docker/clickhouse-setup/docker-compose.yaml @@ -164,7 +164,7 @@ services: # Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md` query-service: - image: signoz/query-service:${DOCKER_TAG:-0.39.0} + image: signoz/query-service:${DOCKER_TAG:-0.39.1} container_name: signoz-query-service command: [ @@ -203,7 +203,7 @@ services: <<: *db-depend frontend: - image: signoz/frontend:${DOCKER_TAG:-0.39.0} + image: signoz/frontend:${DOCKER_TAG:-0.39.1} container_name: signoz-frontend restart: on-failure depends_on: @@ -215,7 +215,7 @@ services: - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf otel-collector-migrator: - image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.12} + image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.13} container_name: otel-migrator command: - "--dsn=tcp://clickhouse:9000" @@ -229,7 +229,7 @@ services: otel-collector: - image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.12} + image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.13} container_name: signoz-otel-collector command: [ diff --git a/go.mod b/go.mod index ec4d7506ff..4b24c57239 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.21.3 require ( github.com/ClickHouse/clickhouse-go/v2 v2.15.0 github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd - github.com/SigNoz/signoz-otel-collector v0.88.12 + github.com/SigNoz/signoz-otel-collector v0.88.13 github.com/SigNoz/zap_otlp/zap_otlp_encoder v0.0.0-20230822164844-1b861a431974 github.com/SigNoz/zap_otlp/zap_otlp_sync v0.0.0-20230822164844-1b861a431974 github.com/antonmedv/expr v1.15.3 diff --git a/go.sum b/go.sum index ae300b0f17..ced65a3169 100644 --- a/go.sum +++ b/go.sum @@ -94,14 +94,12 @@ github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migc github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/SigNoz/govaluate v0.0.0-20220522085550-d19c08c206cb h1:bneLSKPf9YUSFmafKx32bynV6QrzViL/s+ZDvQxH1E4= -github.com/SigNoz/govaluate v0.0.0-20220522085550-d19c08c206cb/go.mod h1:JznGDNg9x1cujDKa22RaQOimOvvEfy3nxzDGd8XDgmA= github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd h1:Bk43AsDYe0fhkbj57eGXx8H3ZJ4zhmQXBnrW523ktj8= github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd/go.mod h1:nxRcH/OEdM8QxzH37xkGzomr1O0JpYBRS6pwjsWW6Pc= github.com/SigNoz/prometheus v1.9.78 h1:bB3yuDrRzi/Mv00kWayR9DZbyjTuGfendSqISyDcXiY= github.com/SigNoz/prometheus v1.9.78/go.mod h1:MffmFu2qFILQrOHehx3D0XjYtaZMVfI+Ppeiv98x4Ww= -github.com/SigNoz/signoz-otel-collector v0.88.12 h1:UwkVi1o2NY9gRgCLBtWVKr+UDxb4FaTs63Sb20qgf8w= -github.com/SigNoz/signoz-otel-collector v0.88.12/go.mod h1:RH9OEjni6tkh9RgN/meSPxv3kykjcFscqMwJgbUAXmo= +github.com/SigNoz/signoz-otel-collector v0.88.13 h1:VAVXokL28Hqxo6xyzlCrFS1na/bd1cgqFAVOe1lJjUE= +github.com/SigNoz/signoz-otel-collector v0.88.13/go.mod h1:RH9OEjni6tkh9RgN/meSPxv3kykjcFscqMwJgbUAXmo= github.com/SigNoz/zap_otlp v0.1.0 h1:T7rRcFN87GavY8lDGZj0Z3Xv6OhJA6Pj3I9dNPmqvRc= github.com/SigNoz/zap_otlp v0.1.0/go.mod h1:lcHvbDbRgvDnPxo9lDlaL1JK2PyOyouP/C3ynnYIvyo= github.com/SigNoz/zap_otlp/zap_otlp_encoder v0.0.0-20230822164844-1b861a431974 h1:PKVgdf83Yw+lZJbFtNGBgqXiXNf3+kOXW2qZ7Ms7OaY= diff --git a/pkg/query-service/tests/test-deploy/docker-compose.yaml b/pkg/query-service/tests/test-deploy/docker-compose.yaml index 7c9b50199f..d88945b312 100644 --- a/pkg/query-service/tests/test-deploy/docker-compose.yaml +++ b/pkg/query-service/tests/test-deploy/docker-compose.yaml @@ -192,7 +192,7 @@ services: <<: *db-depend otel-collector-migrator: - image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.12} + image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.13} container_name: otel-migrator command: - "--dsn=tcp://clickhouse:9000" @@ -205,7 +205,7 @@ services: # condition: service_healthy otel-collector: - image: signoz/signoz-otel-collector:0.88.12 + image: signoz/signoz-otel-collector:0.88.13 container_name: signoz-otel-collector command: [