diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 573be5f290..ae8840a453 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -5,6 +5,6 @@ /frontend/ @YounixM /frontend/src/container/MetricsApplication @srikanthccv /frontend/src/container/NewWidget/RightContainer/types.ts @srikanthccv -/deploy/ @prashant-shahi -/sample-apps/ @prashant-shahi -.github @prashant-shahi +/deploy/ @SigNoz/devops +/sample-apps/ @SigNoz/devops +.github @SigNoz/devops diff --git a/.gitignore b/.gitignore index 8fe54dcf3d..2bd9238255 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,6 @@ e2e/.auth # go vendor/ **/main/** + +# git-town +.git-branches.toml diff --git a/deploy/docker-swarm/clickhouse-setup/clickhouse-config.xml b/deploy/docker-swarm/clickhouse-setup/clickhouse-config.xml index dd2b1bdf5b..4e8dc00b30 100644 --- a/deploy/docker-swarm/clickhouse-setup/clickhouse-config.xml +++ b/deploy/docker-swarm/clickhouse-setup/clickhouse-config.xml @@ -649,12 +649,12 @@ See https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/replication/#creating-replicated-tables --> - + diff --git a/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml b/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml index e9cdbb31c0..007de0fc9b 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.52.0 + image: signoz/query-service:0.53.0 command: [ "-config=/root/config/prometheus.yml", @@ -186,7 +186,7 @@ services: <<: *db-depend frontend: - image: signoz/frontend:0.52.0 + image: signoz/frontend:0.53.0 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.102.4 + image: signoz/signoz-otel-collector:0.102.7 command: [ "--config=/etc/otel-collector-config.yaml", @@ -238,7 +238,7 @@ services: - query-service otel-collector-migrator: - image: signoz/signoz-schema-migrator:0.102.4 + image: signoz/signoz-schema-migrator:0.102.7 deploy: restart_policy: condition: on-failure diff --git a/deploy/docker/clickhouse-setup/clickhouse-config.xml b/deploy/docker/clickhouse-setup/clickhouse-config.xml index f8213b6521..027f53f951 100644 --- a/deploy/docker/clickhouse-setup/clickhouse-config.xml +++ b/deploy/docker/clickhouse-setup/clickhouse-config.xml @@ -649,12 +649,12 @@ See https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/replication/#creating-replicated-tables --> - + diff --git a/deploy/docker/clickhouse-setup/docker-compose-core.yaml b/deploy/docker/clickhouse-setup/docker-compose-core.yaml index 6b53443e79..973e1ae602 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.102.4} + image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.7} 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.102.4 + image: signoz/signoz-otel-collector:0.102.7 command: [ "--config=/etc/otel-collector-config.yaml", diff --git a/deploy/docker/clickhouse-setup/docker-compose.testing.yaml b/deploy/docker/clickhouse-setup/docker-compose.testing.yaml index 8fe7c6b4ee..8d3c54be0a 100644 --- a/deploy/docker/clickhouse-setup/docker-compose.testing.yaml +++ b/deploy/docker/clickhouse-setup/docker-compose.testing.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.52.0} + image: signoz/query-service:${DOCKER_TAG:-0.53.0} container_name: signoz-query-service command: [ @@ -204,7 +204,7 @@ services: <<: *db-depend frontend: - image: signoz/frontend:${DOCKER_TAG:-0.52.0} + image: signoz/frontend:${DOCKER_TAG:-0.53.0} container_name: signoz-frontend restart: on-failure depends_on: @@ -216,7 +216,7 @@ services: - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf otel-collector-migrator: - image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.4} + image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.7} container_name: otel-migrator command: - "--dsn=tcp://clickhouse:9000" @@ -230,7 +230,7 @@ services: otel-collector: - image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.4} + image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.7} container_name: signoz-otel-collector command: [ diff --git a/deploy/docker/clickhouse-setup/docker-compose.yaml b/deploy/docker/clickhouse-setup/docker-compose.yaml index 3d70f0d0f9..d20728c769 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.52.0} + image: signoz/query-service:${DOCKER_TAG:-0.53.0} container_name: signoz-query-service command: [ @@ -203,7 +203,7 @@ services: <<: *db-depend frontend: - image: signoz/frontend:${DOCKER_TAG:-0.52.0} + image: signoz/frontend:${DOCKER_TAG:-0.53.0} 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.102.4} + image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.7} container_name: otel-migrator command: - "--dsn=tcp://clickhouse:9000" @@ -229,7 +229,7 @@ services: otel-collector: - image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.4} + image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.7} container_name: signoz-otel-collector command: [ diff --git a/deploy/docker/common/nginx-config.conf b/deploy/docker/common/nginx-config.conf index f7943e21aa..c87960d7b2 100644 --- a/deploy/docker/common/nginx-config.conf +++ b/deploy/docker/common/nginx-config.conf @@ -1,3 +1,8 @@ +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + server { listen 3301; server_name _; @@ -42,6 +47,14 @@ server { proxy_read_timeout 600s; } + location /ws { + proxy_pass http://query-service:8080/ws; + proxy_http_version 1.1; + proxy_set_header Upgrade "websocket"; + proxy_set_header Connection "upgrade"; + proxy_read_timeout 86400; + } + # redirect server error pages to the static page /50x.html # error_page 500 502 503 504 /50x.html; diff --git a/ee/query-service/app/server.go b/ee/query-service/app/server.go index 348cdbddd2..5d645673e4 100644 --- a/ee/query-service/app/server.go +++ b/ee/query-service/app/server.go @@ -359,6 +359,8 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e apiHandler.RegisterIntegrationRoutes(r, am) apiHandler.RegisterQueryRangeV3Routes(r, am) apiHandler.RegisterQueryRangeV4Routes(r, am) + apiHandler.RegisterWebSocketPaths(r, am) + apiHandler.RegisterMessagingQueuesRoutes(r, am) c := cors.New(cors.Options{ AllowedOrigins: []string{"*"}, @@ -375,6 +377,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e }, nil } +// TODO(remove): Implemented at pkg/http/middleware/logging.go // loggingMiddleware is used for logging public api calls func loggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -386,6 +389,7 @@ func loggingMiddleware(next http.Handler) http.Handler { }) } +// TODO(remove): Implemented at pkg/http/middleware/logging.go // loggingMiddlewarePrivate is used for logging private api calls // from internal services like alert manager func loggingMiddlewarePrivate(next http.Handler) http.Handler { @@ -398,27 +402,32 @@ func loggingMiddlewarePrivate(next http.Handler) http.Handler { }) } +// TODO(remove): Implemented at pkg/http/middleware/logging.go type loggingResponseWriter struct { http.ResponseWriter statusCode int } +// TODO(remove): Implemented at pkg/http/middleware/logging.go func NewLoggingResponseWriter(w http.ResponseWriter) *loggingResponseWriter { // WriteHeader(int) is not called if our response implicitly returns 200 OK, so // we default to that status code. return &loggingResponseWriter{w, http.StatusOK} } +// TODO(remove): Implemented at pkg/http/middleware/logging.go func (lrw *loggingResponseWriter) WriteHeader(code int) { lrw.statusCode = code lrw.ResponseWriter.WriteHeader(code) } +// TODO(remove): Implemented at pkg/http/middleware/logging.go // Flush implements the http.Flush interface. func (lrw *loggingResponseWriter) Flush() { lrw.ResponseWriter.(http.Flusher).Flush() } +// TODO(remove): Implemented at pkg/http/middleware/logging.go // Support websockets func (lrw *loggingResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { h, ok := lrw.ResponseWriter.(http.Hijacker) @@ -564,6 +573,7 @@ func (s *Server) analyticsMiddleware(next http.Handler) http.Handler { }) } +// TODO(remove): Implemented at pkg/http/middleware/timeout.go func setTimeoutMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() diff --git a/ee/query-service/model/plans.go b/ee/query-service/model/plans.go index 135d276cad..9b696c013f 100644 --- a/ee/query-service/model/plans.go +++ b/ee/query-service/model/plans.go @@ -12,6 +12,7 @@ const DisableUpsell = "DISABLE_UPSELL" const Onboarding = "ONBOARDING" const ChatSupport = "CHAT_SUPPORT" const Gateway = "GATEWAY" +const PremiumSupport = "PREMIUM_SUPPORT" var BasicPlan = basemodel.FeatureSet{ basemodel.Feature{ @@ -119,6 +120,13 @@ var BasicPlan = basemodel.FeatureSet{ UsageLimit: -1, Route: "", }, + basemodel.Feature{ + Name: PremiumSupport, + Active: false, + Usage: 0, + UsageLimit: -1, + Route: "", + }, } var ProPlan = basemodel.FeatureSet{ @@ -220,6 +228,13 @@ var ProPlan = basemodel.FeatureSet{ UsageLimit: -1, Route: "", }, + basemodel.Feature{ + Name: PremiumSupport, + Active: true, + Usage: 0, + UsageLimit: -1, + Route: "", + }, } var EnterprisePlan = basemodel.FeatureSet{ @@ -335,4 +350,11 @@ var EnterprisePlan = basemodel.FeatureSet{ UsageLimit: -1, Route: "", }, + basemodel.Feature{ + Name: PremiumSupport, + Active: true, + Usage: 0, + UsageLimit: -1, + Route: "", + }, } diff --git a/frontend/public/Icons/groupBy.svg b/frontend/public/Icons/groupBy.svg new file mode 100644 index 0000000000..e668ef176a --- /dev/null +++ b/frontend/public/Icons/groupBy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/Icons/solid-x-circle.svg b/frontend/public/Icons/solid-x-circle.svg new file mode 100644 index 0000000000..3f189e3865 --- /dev/null +++ b/frontend/public/Icons/solid-x-circle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/locales/en/dashboard.json b/frontend/public/locales/en/dashboard.json index d2e90237a9..26e8f289a9 100644 --- a/frontend/public/locales/en/dashboard.json +++ b/frontend/public/locales/en/dashboard.json @@ -1,6 +1,7 @@ { "create_dashboard": "Create Dashboard", "import_json": "Import Dashboard JSON", + "view_template": "View templates", "import_grafana_json": "Import Grafana JSON", "copy_to_clipboard": "Copy To ClipBoard", "download_json": "Download JSON", diff --git a/frontend/public/locales/en/titles.json b/frontend/public/locales/en/titles.json index f77bf0e85a..4aa2b65dc0 100644 --- a/frontend/public/locales/en/titles.json +++ b/frontend/public/locales/en/titles.json @@ -49,5 +49,6 @@ "TRACES_SAVE_VIEWS": "SigNoz | Traces Saved Views", "DEFAULT": "Open source Observability Platform | SigNoz", "SHORTCUTS": "SigNoz | Shortcuts", - "INTEGRATIONS": "SigNoz | Integrations" + "INTEGRATIONS": "SigNoz | Integrations", + "MESSAGING_QUEUES": "SigNoz | Messaging Queues" } diff --git a/frontend/src/AppRoutes/pageComponents.ts b/frontend/src/AppRoutes/pageComponents.ts index 9275e7d6f6..bce075cef3 100644 --- a/frontend/src/AppRoutes/pageComponents.ts +++ b/frontend/src/AppRoutes/pageComponents.ts @@ -204,3 +204,15 @@ export const InstalledIntegrations = Loadable( /* webpackChunkName: "InstalledIntegrations" */ 'pages/IntegrationsModulePage' ), ); + +export const MessagingQueues = Loadable( + () => + import(/* webpackChunkName: "MessagingQueues" */ 'pages/MessagingQueues'), +); + +export const MQDetailPage = Loadable( + () => + import( + /* webpackChunkName: "MQDetailPage" */ 'pages/MessagingQueues/MQDetailPage' + ), +); diff --git a/frontend/src/AppRoutes/routes.ts b/frontend/src/AppRoutes/routes.ts index 4fd421ffba..98fdbed392 100644 --- a/frontend/src/AppRoutes/routes.ts +++ b/frontend/src/AppRoutes/routes.ts @@ -23,6 +23,8 @@ import { LogsExplorer, LogsIndexToFields, LogsSaveViews, + MessagingQueues, + MQDetailPage, MySettings, NewDashboardPage, OldLogsExplorer, @@ -351,6 +353,20 @@ const routes: AppRoutes[] = [ isPrivate: true, key: 'INTEGRATIONS', }, + { + path: ROUTES.MESSAGING_QUEUES, + exact: true, + component: MessagingQueues, + key: 'MESSAGING_QUEUES', + isPrivate: true, + }, + { + path: ROUTES.MESSAGING_QUEUES_DETAIL, + exact: true, + component: MQDetailPage, + key: 'MESSAGING_QUEUES_DETAIL', + isPrivate: true, + }, ]; export const SUPPORT_ROUTE: AppRoutes = { diff --git a/frontend/src/api/common/getQueryStats.ts b/frontend/src/api/common/getQueryStats.ts new file mode 100644 index 0000000000..c7e8bd2b4b --- /dev/null +++ b/frontend/src/api/common/getQueryStats.ts @@ -0,0 +1,62 @@ +import getLocalStorageApi from 'api/browser/localstorage/get'; +import { ENVIRONMENT } from 'constants/env'; +import { LOCALSTORAGE } from 'constants/localStorage'; +import { isEmpty } from 'lodash-es'; + +export interface WsDataEvent { + read_rows: number; + read_bytes: number; + elapsed_ms: number; +} +interface GetQueryStatsProps { + queryId: string; + setData: React.Dispatch>; +} + +function getURL(baseURL: string, queryId: string): URL | string { + if (baseURL && !isEmpty(baseURL)) { + return `${baseURL}/ws/query_progress?q=${queryId}`; + } + const url = new URL(`/ws/query_progress?q=${queryId}`, window.location.href); + + if (window.location.protocol === 'http:') { + url.protocol = 'ws'; + } else { + url.protocol = 'wss'; + } + + return url; +} + +export function getQueryStats(props: GetQueryStatsProps): void { + const { queryId, setData } = props; + + const token = getLocalStorageApi(LOCALSTORAGE.AUTH_TOKEN) || ''; + + // https://github.com/whatwg/websockets/issues/20 reason for not using the relative URLs + const url = getURL(ENVIRONMENT.wsURL, queryId); + + const socket = new WebSocket(url, token); + + socket.addEventListener('message', (event) => { + try { + const parsedData = JSON.parse(event?.data); + setData(parsedData); + } catch { + setData(event?.data); + } + }); + + socket.addEventListener('error', (event) => { + console.error(event); + }); + + socket.addEventListener('close', (event) => { + // 1000 is a normal closure status code + if (event.code !== 1000) { + console.error('WebSocket closed with error:', event); + } else { + console.error('WebSocket closed normally.'); + } + }); +} diff --git a/frontend/src/api/dashboard/variables/dashboardVariablesQuery.ts b/frontend/src/api/dashboard/variables/dashboardVariablesQuery.ts index 8605ce75f1..5e5c333520 100644 --- a/frontend/src/api/dashboard/variables/dashboardVariablesQuery.ts +++ b/frontend/src/api/dashboard/variables/dashboardVariablesQuery.ts @@ -1,6 +1,8 @@ import { ApiV2Instance as axios } from 'api'; import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; import { AxiosError } from 'axios'; +import getStartEndRangeTime from 'lib/getStartEndRangeTime'; +import store from 'store'; import { ErrorResponse, SuccessResponse } from 'types/api'; import { Props, @@ -11,7 +13,26 @@ const dashboardVariablesQuery = async ( props: Props, ): Promise | ErrorResponse> => { try { - const response = await axios.post(`/variables/query`, props); + const { globalTime } = store.getState(); + const { start, end } = getStartEndRangeTime({ + type: 'GLOBAL_TIME', + interval: globalTime.selectedTime, + }); + + const timeVariables: Record = { + start_timestamp_ms: parseInt(start, 10) * 1e3, + end_timestamp_ms: parseInt(end, 10) * 1e3, + start_timestamp_nano: parseInt(start, 10) * 1e9, + end_timestamp_nano: parseInt(end, 10) * 1e9, + start_timestamp: parseInt(start, 10), + end_timestamp: parseInt(end, 10), + }; + + const payload = { ...props }; + + payload.variables = { ...payload.variables, ...timeVariables }; + + const response = await axios.post(`/variables/query`, payload); return { statusCode: 200, diff --git a/frontend/src/api/metrics/getQueryRange.ts b/frontend/src/api/metrics/getQueryRange.ts index 40deb021bc..631372478d 100644 --- a/frontend/src/api/metrics/getQueryRange.ts +++ b/frontend/src/api/metrics/getQueryRange.ts @@ -12,10 +12,13 @@ export const getMetricsQueryRange = async ( props: QueryRangePayload, version: string, signal: AbortSignal, + headers?: Record, ): Promise | ErrorResponse> => { try { if (version && version === ENTITY_VERSION_V4) { - const response = await ApiV4Instance.post('/query_range', props, { signal }); + const response = await ApiV4Instance.post('/query_range', props, { + signal, + }); return { statusCode: 200, @@ -26,7 +29,10 @@ export const getMetricsQueryRange = async ( }; } - const response = await ApiV3Instance.post('/query_range', props, { signal }); + const response = await ApiV3Instance.post('/query_range', props, { + signal, + headers, + }); return { statusCode: 200, diff --git a/frontend/src/api/queryBuilder/getAttributeSuggestions.ts b/frontend/src/api/queryBuilder/getAttributeSuggestions.ts new file mode 100644 index 0000000000..45b380f9e8 --- /dev/null +++ b/frontend/src/api/queryBuilder/getAttributeSuggestions.ts @@ -0,0 +1,63 @@ +import { ApiV3Instance } from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError, AxiosResponse } from 'axios'; +import { baseAutoCompleteIdKeysOrder } from 'constants/queryBuilder'; +import { encode } from 'js-base64'; +import { createIdFromObjectFields } from 'lib/createIdFromObjectFields'; +import createQueryParams from 'lib/createQueryParams'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { + IGetAttributeSuggestionsPayload, + IGetAttributeSuggestionsSuccessResponse, +} from 'types/api/queryBuilder/getAttributeSuggestions'; +import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; + +export const getAttributeSuggestions = async ({ + searchText, + dataSource, + filters, +}: IGetAttributeSuggestionsPayload): Promise< + SuccessResponse | ErrorResponse +> => { + try { + let base64EncodedFiltersString; + try { + // the replace function is to remove the padding at the end of base64 encoded string which is auto added to make it a multiple of 4 + // why ? because the current working of qs doesn't work well with padding + base64EncodedFiltersString = encode(JSON.stringify(filters)).replace( + /=+$/, + '', + ); + } catch { + // default base64 encoded string for empty filters object + base64EncodedFiltersString = 'eyJpdGVtcyI6W10sIm9wIjoiQU5EIn0'; + } + const response: AxiosResponse<{ + data: IGetAttributeSuggestionsSuccessResponse; + }> = await ApiV3Instance.get( + `/filter_suggestions?${createQueryParams({ + searchText, + dataSource, + existingFilter: base64EncodedFiltersString, + })}`, + ); + + const payload: BaseAutocompleteData[] = + response.data.data.attributes?.map(({ id: _, ...item }) => ({ + ...item, + id: createIdFromObjectFields(item, baseAutoCompleteIdKeysOrder), + })) || []; + + return { + statusCode: 200, + error: null, + message: response.statusText, + payload: { + attributes: payload, + example_queries: response.data.data.example_queries, + }, + }; + } catch (e) { + return ErrorResponseHandler(e as AxiosError); + } +}; diff --git a/frontend/src/assets/CustomIcons/GroupByIcon.tsx b/frontend/src/assets/CustomIcons/GroupByIcon.tsx new file mode 100644 index 0000000000..4cfceef3c8 --- /dev/null +++ b/frontend/src/assets/CustomIcons/GroupByIcon.tsx @@ -0,0 +1,27 @@ +import { Color } from '@signozhq/design-tokens'; +import { useIsDarkMode } from 'hooks/useDarkMode'; + +function GroupByIcon(): JSX.Element { + const isDarkMode = useIsDarkMode(); + return ( + + + + + + + + + + + + ); +} + +export default GroupByIcon; diff --git a/frontend/src/components/ChatSupportGateway/ChatSupportGateway.tsx b/frontend/src/components/ChatSupportGateway/ChatSupportGateway.tsx index b361f725c3..67353f8ba2 100644 --- a/frontend/src/components/ChatSupportGateway/ChatSupportGateway.tsx +++ b/frontend/src/components/ChatSupportGateway/ChatSupportGateway.tsx @@ -7,6 +7,7 @@ import { useNotifications } from 'hooks/useNotifications'; import { CreditCard, X } from 'lucide-react'; import { useEffect, useState } from 'react'; import { useMutation } from 'react-query'; +import { useLocation } from 'react-router-dom'; import { ErrorResponse, SuccessResponse } from 'types/api'; import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout'; import { License } from 'types/api/licenses/def'; @@ -57,11 +58,11 @@ export default function ChatSupportGateway(): JSX.Element { onError: handleBillingOnError, }, ); - + const { pathname } = useLocation(); const handleAddCreditCard = (): void => { logEvent('Add Credit card modal: Clicked', { source: `intercom icon`, - page: '', + page: pathname, }); updateCreditCard({ @@ -79,7 +80,7 @@ export default function ChatSupportGateway(): JSX.Element { onClick={(): void => { logEvent('Disabled Chat Support: Clicked', { source: `intercom icon`, - page: '', + page: pathname, }); setIsAddCreditCardModalOpen(true); diff --git a/frontend/src/components/LaunchChatSupport/LaunchChatSupport.tsx b/frontend/src/components/LaunchChatSupport/LaunchChatSupport.tsx index c568a7e82b..eb0659cfb1 100644 --- a/frontend/src/components/LaunchChatSupport/LaunchChatSupport.tsx +++ b/frontend/src/components/LaunchChatSupport/LaunchChatSupport.tsx @@ -13,6 +13,7 @@ import { defaultTo } from 'lodash-es'; import { CreditCard, HelpCircle, X } from 'lucide-react'; import { useEffect, useState } from 'react'; import { useMutation } from 'react-query'; +import { useLocation } from 'react-router-dom'; import { ErrorResponse, SuccessResponse } from 'types/api'; import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout'; import { License } from 'types/api/licenses/def'; @@ -47,6 +48,7 @@ function LaunchChatSupport({ false, ); + const { pathname } = useLocation(); const isPremiumChatSupportEnabled = useFeatureFlags(FeatureKeys.PREMIUM_SUPPORT)?.active || false; @@ -65,6 +67,11 @@ function LaunchChatSupport({ const handleFacingIssuesClick = (): void => { if (showAddCreditCardModal) { + logEvent('Disabled Chat Support: Clicked', { + source: `facing issues button`, + page: pathname, + ...attributes, + }); setIsAddCreditCardModalOpen(true); } else { logEvent(eventName, attributes); @@ -105,7 +112,7 @@ function LaunchChatSupport({ const handleAddCreditCard = (): void => { logEvent('Add Credit card modal: Clicked', { source: `facing issues button`, - page: '', + page: pathname, ...attributes, }); diff --git a/frontend/src/components/LaunchChatSupport/util.ts b/frontend/src/components/LaunchChatSupport/util.ts index b99a31e970..8b610b11cc 100644 --- a/frontend/src/components/LaunchChatSupport/util.ts +++ b/frontend/src/components/LaunchChatSupport/util.ts @@ -41,6 +41,21 @@ I need help with managing alerts. Thanks`; +export const onboardingHelpMessage = ( + dataSourceName: string, + moduleId: string, +): string => `Hi Team, + +I am facing issues sending data to SigNoz. Here are my application details + +Data Source: ${dataSourceName} +Framework: +Environment: +Module: ${moduleId} + +Thanks +`; + export const alertHelpMessage = ( alertDef: AlertDef, ruleId: number, diff --git a/frontend/src/components/LogDetail/LogDetail.interfaces.ts b/frontend/src/components/LogDetail/LogDetail.interfaces.ts index 2a2dd56855..2c56d58fd1 100644 --- a/frontend/src/components/LogDetail/LogDetail.interfaces.ts +++ b/frontend/src/components/LogDetail/LogDetail.interfaces.ts @@ -3,12 +3,18 @@ import { AddToQueryHOCProps } from 'components/Logs/AddToQueryHOC'; import { ActionItemProps } from 'container/LogDetailedView/ActionItem'; import { IField } from 'types/api/logs/fields'; import { ILog } from 'types/api/logs/log'; +import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { VIEWS } from './constants'; export type LogDetailProps = { log: ILog | null; selectedTab: VIEWS; + onGroupByAttribute?: ( + fieldKey: string, + isJSON?: boolean, + dataType?: DataTypes, + ) => Promise; isListViewPanel?: boolean; listViewPanelSelectedFields?: IField[] | null; } & Pick & diff --git a/frontend/src/components/LogDetail/index.tsx b/frontend/src/components/LogDetail/index.tsx index 650d474736..b138718ed9 100644 --- a/frontend/src/components/LogDetail/index.tsx +++ b/frontend/src/components/LogDetail/index.tsx @@ -2,6 +2,7 @@ import './LogDetails.styles.scss'; import { Color, Spacing } from '@signozhq/design-tokens'; +import Convert from 'ansi-to-html'; import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd'; import { RadioChangeEvent } from 'antd/lib'; import cx from 'classnames'; @@ -10,8 +11,13 @@ import { LOCALSTORAGE } from 'constants/localStorage'; import ContextView from 'container/LogDetailedView/ContextView/ContextView'; import JSONView from 'container/LogDetailedView/JsonView'; import Overview from 'container/LogDetailedView/Overview'; -import { aggregateAttributesResourcesToString } from 'container/LogDetailedView/utils'; +import { + aggregateAttributesResourcesToString, + removeEscapeCharacters, + unescapeString, +} from 'container/LogDetailedView/utils'; import { useOptionsMenu } from 'container/OptionsMenu'; +import dompurify from 'dompurify'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useIsDarkMode } from 'hooks/useDarkMode'; import { useNotifications } from 'hooks/useNotifications'; @@ -28,15 +34,19 @@ import { useMemo, useState } from 'react'; import { useCopyToClipboard } from 'react-use'; import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData'; import { DataSource, StringOperators } from 'types/common/queryBuilder'; +import { FORBID_DOM_PURIFY_TAGS } from 'utils/app'; import { VIEW_TYPES, VIEWS } from './constants'; import { LogDetailProps } from './LogDetail.interfaces'; import QueryBuilderSearchWrapper from './QueryBuilderSearchWrapper'; +const convert = new Convert(); + function LogDetail({ log, onClose, onAddToQuery, + onGroupByAttribute, onClickActionItem, selectedTab, isListViewPanel = false, @@ -89,6 +99,17 @@ function LogDetail({ } }; + const htmlBody = useMemo( + () => ({ + __html: convert.toHtml( + dompurify.sanitize(unescapeString(log?.body || ''), { + FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS], + }), + ), + }), + [log?.body], + ); + const handleJSONCopy = (): void => { copyToClipboard(LogJsonData); notifications.success({ @@ -126,8 +147,8 @@ function LogDetail({ >
- - {log?.body} + +
 
@@ -209,6 +230,7 @@ function LogDetail({ logData={log} onAddToQuery={onAddToQuery} onClickActionItem={onClickActionItem} + onGroupByAttribute={onGroupByAttribute} isListViewPanel={isListViewPanel} selectedOptions={options} listViewPanelSelectedFields={listViewPanelSelectedFields} diff --git a/frontend/src/components/Logs/AddToQueryHOC.styles.scss b/frontend/src/components/Logs/AddToQueryHOC.styles.scss index 42baabd02a..ec790ceecc 100644 --- a/frontend/src/components/Logs/AddToQueryHOC.styles.scss +++ b/frontend/src/components/Logs/AddToQueryHOC.styles.scss @@ -1,3 +1,16 @@ .addToQueryContainer { cursor: pointer; + display: flex; + align-items: center; + &.small { + line-height: 16px; + } + + &.medium { + line-height: 20px; + } + + &.large { + line-height: 24px; + } } diff --git a/frontend/src/components/Logs/AddToQueryHOC.tsx b/frontend/src/components/Logs/AddToQueryHOC.tsx index 8391a23b81..df222b7552 100644 --- a/frontend/src/components/Logs/AddToQueryHOC.tsx +++ b/frontend/src/components/Logs/AddToQueryHOC.tsx @@ -1,13 +1,16 @@ import './AddToQueryHOC.styles.scss'; import { Popover } from 'antd'; +import cx from 'classnames'; import { OPERATORS } from 'constants/queryBuilder'; +import { FontSize } from 'container/OptionsMenu/types'; import { memo, MouseEvent, ReactNode, useMemo } from 'react'; function AddToQueryHOC({ fieldKey, fieldValue, onAddToQuery, + fontSize, children, }: AddToQueryHOCProps): JSX.Element { const handleQueryAdd = (event: MouseEvent): void => { @@ -21,7 +24,7 @@ function AddToQueryHOC({ return ( // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions -
+
{children} @@ -33,6 +36,7 @@ export interface AddToQueryHOCProps { fieldKey: string; fieldValue: string; onAddToQuery: (fieldKey: string, fieldValue: string, operator: string) => void; + fontSize: FontSize; children: ReactNode; } diff --git a/frontend/src/components/Logs/CopyClipboardHOC.tsx b/frontend/src/components/Logs/CopyClipboardHOC.tsx index a12208bf77..65cb6fc854 100644 --- a/frontend/src/components/Logs/CopyClipboardHOC.tsx +++ b/frontend/src/components/Logs/CopyClipboardHOC.tsx @@ -4,6 +4,7 @@ import { ReactNode, useCallback, useEffect } from 'react'; import { useCopyToClipboard } from 'react-use'; function CopyClipboardHOC({ + entityKey, textToCopy, children, }: CopyClipboardHOCProps): JSX.Element { @@ -11,11 +12,15 @@ function CopyClipboardHOC({ const { notifications } = useNotifications(); useEffect(() => { if (value.value) { + const key = entityKey || ''; + + const notificationMessage = `${key} copied to clipboard`; + notifications.success({ - message: 'Copied to clipboard', + message: notificationMessage, }); } - }, [value, notifications]); + }, [value, notifications, entityKey]); const onClick = useCallback((): void => { setCopy(textToCopy); @@ -34,6 +39,7 @@ function CopyClipboardHOC({ } interface CopyClipboardHOCProps { + entityKey: string | undefined; textToCopy: string; children: ReactNode; } diff --git a/frontend/src/components/Logs/ListLogView/ListLogView.styles.scss b/frontend/src/components/Logs/ListLogView/ListLogView.styles.scss index 3caf6a3282..21dcf171ce 100644 --- a/frontend/src/components/Logs/ListLogView/ListLogView.styles.scss +++ b/frontend/src/components/Logs/ListLogView/ListLogView.styles.scss @@ -6,6 +6,21 @@ font-weight: 400; line-height: 18px; /* 128.571% */ letter-spacing: -0.07px; + + &.small { + font-size: 11px; + line-height: 16px; + } + + &.medium { + font-size: 13px; + line-height: 20px; + } + + &.large { + font-size: 14px; + line-height: 24px; + } } .log-value { color: var(--text-vanilla-400, #c0c1c3); @@ -14,6 +29,21 @@ font-weight: 400; line-height: 18px; /* 128.571% */ letter-spacing: -0.07px; + + &.small { + font-size: 11px; + line-height: 16px; + } + + &.medium { + font-size: 13px; + line-height: 20px; + } + + &.large { + font-size: 14px; + line-height: 24px; + } } .log-line { display: flex; @@ -40,6 +70,20 @@ font-weight: 400; line-height: 18px; /* 128.571% */ letter-spacing: -0.07px; + &.small { + font-size: 11px; + line-height: 16px; + } + + &.medium { + font-size: 13px; + line-height: 20px; + } + + &.large { + font-size: 14px; + line-height: 24px; + } } .selected-log-value { @@ -52,12 +96,37 @@ line-height: 18px; letter-spacing: -0.07px; font-size: 14px; + &.small { + font-size: 11px; + line-height: 16px; + } + + &.medium { + font-size: 13px; + line-height: 20px; + } + + &.large { + font-size: 14px; + line-height: 24px; + } } .selected-log-kv { min-height: 24px; display: flex; align-items: center; + &.small { + min-height: 16px; + } + + &.medium { + min-height: 20px; + } + + &.large { + min-height: 24px; + } } } diff --git a/frontend/src/components/Logs/ListLogView/index.tsx b/frontend/src/components/Logs/ListLogView/index.tsx index fa8a2fb608..ed2627552d 100644 --- a/frontend/src/components/Logs/ListLogView/index.tsx +++ b/frontend/src/components/Logs/ListLogView/index.tsx @@ -3,8 +3,11 @@ import './ListLogView.styles.scss'; import { blue } from '@ant-design/colors'; import Convert from 'ansi-to-html'; import { Typography } from 'antd'; +import cx from 'classnames'; import LogDetail from 'components/LogDetail'; import { VIEW_TYPES } from 'components/LogDetail/constants'; +import { unescapeString } from 'container/LogDetailedView/utils'; +import { FontSize } from 'container/OptionsMenu/types'; import dayjs from 'dayjs'; import dompurify from 'dompurify'; import { useActiveLog } from 'hooks/logs/useActiveLog'; @@ -39,6 +42,7 @@ interface LogFieldProps { fieldKey: string; fieldValue: string; linesPerRow?: number; + fontSize: FontSize; } type LogSelectedFieldProps = Omit & @@ -48,11 +52,12 @@ function LogGeneralField({ fieldKey, fieldValue, linesPerRow = 1, + fontSize, }: LogFieldProps): JSX.Element { const html = useMemo( () => ({ __html: convert.toHtml( - dompurify.sanitize(fieldValue, { + dompurify.sanitize(unescapeString(fieldValue), { FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS], }), ), @@ -62,12 +67,12 @@ function LogGeneralField({ return ( - + {`${fieldKey} : `} 1 ? linesPerRow : undefined} /> @@ -78,6 +83,7 @@ function LogSelectedField({ fieldKey = '', fieldValue = '', onAddToQuery, + fontSize, }: LogSelectedFieldProps): JSX.Element { return (
@@ -85,16 +91,22 @@ function LogSelectedField({ fieldKey={fieldKey} fieldValue={fieldValue} onAddToQuery={onAddToQuery} + fontSize={fontSize} > - + {fieldKey} - - {': '} - {fieldValue || "''"} + + {': '} + + {fieldValue || "''"} +
); @@ -107,6 +119,7 @@ type ListLogViewProps = { onAddToQuery: AddToQueryHOCProps['onAddToQuery']; activeLog?: ILog | null; linesPerRow: number; + fontSize: FontSize; }; function ListLogView({ @@ -116,6 +129,7 @@ function ListLogView({ onAddToQuery, activeLog, linesPerRow, + fontSize, }: ListLogViewProps): JSX.Element { const flattenLogData = useMemo(() => FlatLogData(logData), [logData]); @@ -128,6 +142,7 @@ function ListLogView({ onAddToQuery: handleAddToQuery, onSetActiveLog: handleSetActiveContextLog, onClearActiveLog: handleClearActiveContextLog, + onGroupByAttribute, } = useActiveLog(); const isDarkMode = useIsDarkMode(); @@ -185,6 +200,7 @@ function ListLogView({ onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} onClick={handleDetailedView} + fontSize={fontSize} >
- + {flattenLogData.stream && ( - + )} - + {updatedSelecedFields.map((field) => isValidLogField(flattenLogData[field.name] as never) ? ( @@ -212,6 +238,7 @@ function ListLogView({ fieldKey={field.name} fieldValue={flattenLogData[field.name] as never} onAddToQuery={onAddToQuery} + fontSize={fontSize} /> ) : null, )} @@ -232,6 +259,7 @@ function ListLogView({ onAddToQuery={handleAddToQuery} selectedTab={VIEW_TYPES.CONTEXT} onClose={handlerClearActiveContextLog} + onGroupByAttribute={onGroupByAttribute} /> )} diff --git a/frontend/src/components/Logs/ListLogView/styles.ts b/frontend/src/components/Logs/ListLogView/styles.ts index 52cc2b20d4..d2a6342c77 100644 --- a/frontend/src/components/Logs/ListLogView/styles.ts +++ b/frontend/src/components/Logs/ListLogView/styles.ts @@ -1,21 +1,46 @@ +/* eslint-disable no-nested-ternary */ import { Color } from '@signozhq/design-tokens'; import { Card, Typography } from 'antd'; +import { FontSize } from 'container/OptionsMenu/types'; import styled from 'styled-components'; interface LogTextProps { linesPerRow?: number; } +interface LogContainerProps { + fontSize: FontSize; +} + export const Container = styled(Card)<{ $isActiveLog: boolean; $isDarkMode: boolean; + fontSize: FontSize; }>` width: 100% !important; margin-bottom: 0.3rem; + + ${({ fontSize }): string => + fontSize === FontSize.SMALL + ? `margin-bottom:0.1rem;` + : fontSize === FontSize.MEDIUM + ? `margin-bottom: 0.2rem;` + : fontSize === FontSize.LARGE + ? `margin-bottom:0.3rem;` + : ``} cursor: pointer; .ant-card-body { padding: 0.3rem 0.6rem; + ${({ fontSize }): string => + fontSize === FontSize.SMALL + ? `padding:0.1rem 0.6rem;` + : fontSize === FontSize.MEDIUM + ? `padding: 0.2rem 0.6rem;` + : fontSize === FontSize.LARGE + ? `padding:0.3rem 0.6rem;` + : ``} + ${({ $isActiveLog, $isDarkMode }): string => $isActiveLog ? `background-color: ${ @@ -38,11 +63,17 @@ export const TextContainer = styled.div` width: 100%; `; -export const LogContainer = styled.div` +export const LogContainer = styled.div` margin-left: 0.5rem; display: flex; flex-direction: column; gap: 6px; + ${({ fontSize }): string => + fontSize === FontSize.SMALL + ? `gap: 2px;` + : fontSize === FontSize.MEDIUM + ? ` gap:4px;` + : `gap:6px;`} `; export const LogText = styled.div` diff --git a/frontend/src/components/Logs/LogStateIndicator/LogStateIndicator.styles.scss b/frontend/src/components/Logs/LogStateIndicator/LogStateIndicator.styles.scss index a00c7f6761..61870abc71 100644 --- a/frontend/src/components/Logs/LogStateIndicator/LogStateIndicator.styles.scss +++ b/frontend/src/components/Logs/LogStateIndicator/LogStateIndicator.styles.scss @@ -9,11 +9,24 @@ border-radius: 50px; background-color: transparent; + &.small { + min-height: 16px; + } + + &.medium { + min-height: 20px; + } + + &.large { + min-height: 24px; + } + &.INFO { background-color: var(--bg-slate-400); } - &.WARNING, &.WARN { + &.WARNING, + &.WARN { background-color: var(--bg-amber-500); } diff --git a/frontend/src/components/Logs/LogStateIndicator/LogStateIndicator.test.tsx b/frontend/src/components/Logs/LogStateIndicator/LogStateIndicator.test.tsx index d924c27426..06cc9d3ec4 100644 --- a/frontend/src/components/Logs/LogStateIndicator/LogStateIndicator.test.tsx +++ b/frontend/src/components/Logs/LogStateIndicator/LogStateIndicator.test.tsx @@ -1,10 +1,13 @@ import { render } from '@testing-library/react'; +import { FontSize } from 'container/OptionsMenu/types'; import LogStateIndicator from './LogStateIndicator'; describe('LogStateIndicator', () => { it('renders correctly with default props', () => { - const { container } = render(); + const { container } = render( + , + ); const indicator = container.firstChild as HTMLElement; expect(indicator.classList.contains('log-state-indicator')).toBe(true); expect(indicator.classList.contains('isActive')).toBe(false); @@ -15,28 +18,30 @@ describe('LogStateIndicator', () => { }); it('renders correctly when isActive is true', () => { - const { container } = render(); + const { container } = render( + , + ); const indicator = container.firstChild as HTMLElement; expect(indicator.classList.contains('isActive')).toBe(true); }); it('renders correctly with different types', () => { const { container: containerInfo } = render( - , + , ); expect(containerInfo.querySelector('.line')?.classList.contains('INFO')).toBe( true, ); const { container: containerWarning } = render( - , + , ); expect( containerWarning.querySelector('.line')?.classList.contains('WARNING'), ).toBe(true); const { container: containerError } = render( - , + , ); expect( containerError.querySelector('.line')?.classList.contains('ERROR'), diff --git a/frontend/src/components/Logs/LogStateIndicator/LogStateIndicator.tsx b/frontend/src/components/Logs/LogStateIndicator/LogStateIndicator.tsx index ebad7bd116..b9afa5b7a2 100644 --- a/frontend/src/components/Logs/LogStateIndicator/LogStateIndicator.tsx +++ b/frontend/src/components/Logs/LogStateIndicator/LogStateIndicator.tsx @@ -1,6 +1,7 @@ import './LogStateIndicator.styles.scss'; import cx from 'classnames'; +import { FontSize } from 'container/OptionsMenu/types'; export const SEVERITY_TEXT_TYPE = { TRACE: 'TRACE', @@ -44,13 +45,15 @@ export const LogType = { function LogStateIndicator({ type, isActive, + fontSize, }: { type: string; + fontSize: FontSize; isActive?: boolean; }): JSX.Element { return (
-
+
); } diff --git a/frontend/src/components/Logs/RawLogView/index.tsx b/frontend/src/components/Logs/RawLogView/index.tsx index d1ae19fe99..2cda9c7247 100644 --- a/frontend/src/components/Logs/RawLogView/index.tsx +++ b/frontend/src/components/Logs/RawLogView/index.tsx @@ -4,6 +4,7 @@ import Convert from 'ansi-to-html'; import { DrawerProps } from 'antd'; import LogDetail from 'components/LogDetail'; import { VIEW_TYPES, VIEWS } from 'components/LogDetail/constants'; +import { unescapeString } from 'container/LogDetailedView/utils'; import LogsExplorerContext from 'container/LogsExplorerContext'; import dayjs from 'dayjs'; import dompurify from 'dompurify'; @@ -39,6 +40,7 @@ function RawLogView({ linesPerRow, isTextOverflowEllipsisDisabled, selectedFields = [], + fontSize, }: RawLogViewProps): JSX.Element { const { isHighlighted, isLogsExplorerPage, onLogCopy } = useCopyLogLink( data.id, @@ -54,6 +56,7 @@ function RawLogView({ onSetActiveLog, onClearActiveLog, onAddToQuery, + onGroupByAttribute, } = useActiveLog(); const [hasActionButtons, setHasActionButtons] = useState(false); @@ -143,7 +146,9 @@ function RawLogView({ const html = useMemo( () => ({ __html: convert.toHtml( - dompurify.sanitize(text, { FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS] }), + dompurify.sanitize(unescapeString(text), { + FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS], + }), ), }), [text], @@ -160,6 +165,7 @@ function RawLogView({ $isActiveLog={isActiveLog} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} + fontSize={fontSize} > @@ -199,6 +207,7 @@ function RawLogView({ onClose={handleCloseLogDetail} onAddToQuery={onAddToQuery} onClickActionItem={onAddToQuery} + onGroupByAttribute={onGroupByAttribute} /> )} diff --git a/frontend/src/components/Logs/RawLogView/styles.ts b/frontend/src/components/Logs/RawLogView/styles.ts index d86de435c2..d464f35910 100644 --- a/frontend/src/components/Logs/RawLogView/styles.ts +++ b/frontend/src/components/Logs/RawLogView/styles.ts @@ -1,6 +1,8 @@ +/* eslint-disable no-nested-ternary */ import { blue } from '@ant-design/colors'; import { Color } from '@signozhq/design-tokens'; import { Col, Row, Space } from 'antd'; +import { FontSize } from 'container/OptionsMenu/types'; import styled from 'styled-components'; import { getActiveLogBackground, getDefaultLogBackground } from 'utils/logs'; @@ -11,6 +13,7 @@ export const RawLogViewContainer = styled(Row)<{ $isReadOnly?: boolean; $isActiveLog?: boolean; $isHightlightedLog: boolean; + fontSize: FontSize; }>` position: relative; width: 100%; @@ -22,6 +25,13 @@ export const RawLogViewContainer = styled(Row)<{ .log-state-indicator { margin: 4px 0; + + ${({ fontSize }): string => + fontSize === FontSize.SMALL + ? `margin: 1px 0;` + : fontSize === FontSize.MEDIUM + ? `margin: 1px 0;` + : `margin: 2px 0;`} } ${({ $isActiveLog }): string => getActiveLogBackground($isActiveLog)} @@ -50,8 +60,8 @@ export const RawLogContent = styled.div` margin-bottom: 0; font-family: 'SF Mono', monospace; font-family: 'Geist Mono'; - font-size: 13px; - font-weight: 400; + letter-spacing: -0.07px; + padding: 4px; text-align: left; color: ${({ $isDarkMode }): string => $isDarkMode ? Color.BG_VANILLA_400 : Color.BG_INK_400}; @@ -66,9 +76,15 @@ export const RawLogContent = styled.div` line-clamp: ${linesPerRow}; -webkit-box-orient: vertical;`}; + font-size: 13px; + font-weight: 400; line-height: 24px; - letter-spacing: -0.07px; - padding: 4px; + ${({ fontSize }): string => + fontSize === FontSize.SMALL + ? `font-size:11px; line-height:16px; padding:1px;` + : fontSize === FontSize.MEDIUM + ? `font-size:13px; line-height:20px; padding:1px;` + : `font-size:14px; line-height:24px; padding:2px;`} cursor: ${({ $isActiveLog, $isReadOnly }): string => $isActiveLog || $isReadOnly ? 'initial' : 'pointer'}; diff --git a/frontend/src/components/Logs/RawLogView/types.ts b/frontend/src/components/Logs/RawLogView/types.ts index a9c85c2ad6..ed73725dcc 100644 --- a/frontend/src/components/Logs/RawLogView/types.ts +++ b/frontend/src/components/Logs/RawLogView/types.ts @@ -1,3 +1,4 @@ +import { FontSize } from 'container/OptionsMenu/types'; import { IField } from 'types/api/logs/fields'; import { ILog } from 'types/api/logs/log'; @@ -7,11 +8,13 @@ export interface RawLogViewProps { isTextOverflowEllipsisDisabled?: boolean; data: ILog; linesPerRow: number; + fontSize: FontSize; selectedFields?: IField[]; } export interface RawLogContentProps { linesPerRow: number; + fontSize: FontSize; $isReadOnly?: boolean; $isActiveLog?: boolean; $isDarkMode?: boolean; diff --git a/frontend/src/components/Logs/TableView/styles.ts b/frontend/src/components/Logs/TableView/styles.ts index 9213021971..a79db04a76 100644 --- a/frontend/src/components/Logs/TableView/styles.ts +++ b/frontend/src/components/Logs/TableView/styles.ts @@ -1,7 +1,10 @@ +/* eslint-disable no-nested-ternary */ +import { FontSize } from 'container/OptionsMenu/types'; import styled from 'styled-components'; interface TableBodyContentProps { linesPerRow: number; + fontSize: FontSize; isDarkMode?: boolean; } @@ -20,4 +23,10 @@ export const TableBodyContent = styled.div` -webkit-line-clamp: ${(props): number => props.linesPerRow}; line-clamp: ${(props): number => props.linesPerRow}; -webkit-box-orient: vertical; + ${({ fontSize }): string => + fontSize === FontSize.SMALL + ? `font-size:11px; line-height:16px;` + : fontSize === FontSize.MEDIUM + ? `font-size:13px; line-height:20px;` + : `font-size:14px; line-height:24px;`} `; diff --git a/frontend/src/components/Logs/TableView/types.ts b/frontend/src/components/Logs/TableView/types.ts index 36a796ac0f..b2d3670dd8 100644 --- a/frontend/src/components/Logs/TableView/types.ts +++ b/frontend/src/components/Logs/TableView/types.ts @@ -1,4 +1,5 @@ import { ColumnsType, ColumnType } from 'antd/es/table'; +import { FontSize } from 'container/OptionsMenu/types'; import { IField } from 'types/api/logs/fields'; import { ILog } from 'types/api/logs/log'; @@ -10,6 +11,7 @@ export type LogsTableViewProps = { logs: ILog[]; fields: IField[]; linesPerRow: number; + fontSize: FontSize; onClickExpand?: (log: ILog) => void; }; diff --git a/frontend/src/components/Logs/TableView/useTableView.styles.scss b/frontend/src/components/Logs/TableView/useTableView.styles.scss index 3723ecc705..9592d0ae12 100644 --- a/frontend/src/components/Logs/TableView/useTableView.styles.scss +++ b/frontend/src/components/Logs/TableView/useTableView.styles.scss @@ -5,6 +5,21 @@ font-weight: 400; line-height: 18px; /* 128.571% */ letter-spacing: -0.07px; + + &.small { + font-size: 11px; + line-height: 16px; + } + + &.medium { + font-size: 13px; + line-height: 20px; + } + + &.large { + font-size: 14px; + line-height: 24px; + } } .table-timestamp { @@ -25,3 +40,21 @@ color: var(--bg-slate-400); } } + +.paragraph { + padding: 0px !important; + &.small { + font-size: 11px !important; + line-height: 16px !important; + } + + &.medium { + font-size: 13px !important; + line-height: 20px !important; + } + + &.large { + font-size: 14px !important; + line-height: 24px !important; + } +} diff --git a/frontend/src/components/Logs/TableView/useTableView.tsx b/frontend/src/components/Logs/TableView/useTableView.tsx index fd37132110..43b4ba2628 100644 --- a/frontend/src/components/Logs/TableView/useTableView.tsx +++ b/frontend/src/components/Logs/TableView/useTableView.tsx @@ -3,6 +3,8 @@ import './useTableView.styles.scss'; import Convert from 'ansi-to-html'; import { Typography } from 'antd'; import { ColumnsType } from 'antd/es/table'; +import cx from 'classnames'; +import { unescapeString } from 'container/LogDetailedView/utils'; import dayjs from 'dayjs'; import dompurify from 'dompurify'; import { useIsDarkMode } from 'hooks/useDarkMode'; @@ -31,6 +33,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => { logs, fields, linesPerRow, + fontSize, appendTo = 'center', activeContextLog, activeLog, @@ -57,7 +60,10 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => { : getDefaultCellStyle(isDarkMode), }, children: ( - + {field} ), @@ -87,8 +93,9 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => { isActive={ activeLog?.id === item.id || activeContextLog?.id === item.id } + fontSize={fontSize} /> - + {date}
@@ -109,11 +116,12 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => { @@ -130,6 +138,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => { linesPerRow, activeLog?.id, activeContextLog?.id, + fontSize, ]); return { columns, dataSource: flattenLogData }; diff --git a/frontend/src/components/LogsFormatOptionsMenu/LogsFormatOptionsMenu.styles.scss b/frontend/src/components/LogsFormatOptionsMenu/LogsFormatOptionsMenu.styles.scss index af325a2d25..070d440781 100644 --- a/frontend/src/components/LogsFormatOptionsMenu/LogsFormatOptionsMenu.styles.scss +++ b/frontend/src/components/LogsFormatOptionsMenu/LogsFormatOptionsMenu.styles.scss @@ -17,17 +17,126 @@ box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2); backdrop-filter: blur(20px); + .font-size-dropdown { + display: flex; + flex-direction: column; + + .back-btn { + display: flex; + align-items: center; + gap: 6px; + padding: 12px; + border: none !important; + box-shadow: none !important; + + .icon { + flex-shrink: 0; + } + .text { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 13px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: 0.14px; + } + } + + .back-btn:hover { + background-color: unset !important; + } + + .content { + display: flex; + flex-direction: column; + .option-btn { + display: flex; + align-items: center; + padding: 12px; + border: none !important; + box-shadow: none !important; + justify-content: space-between; + + .icon { + flex-shrink: 0; + } + .text { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 13px; + font-style: normal; + font-weight: 400; + line-height: normal; /* 142.857% */ + letter-spacing: 0.14px; + text-transform: capitalize; + } + + .text:hover { + color: var(--bg-vanilla-300); + } + } + + .option-btn:hover { + background-color: unset !important; + } + } + } + + .font-size-container { + padding: 12px; + display: flex; + flex-direction: column; + gap: 12px; + + .title { + color: var(--bg-slate-50); + font-family: Inter; + font-size: 11px; + font-style: normal; + font-weight: 500; + line-height: 18px; /* 163.636% */ + letter-spacing: 0.88px; + text-transform: uppercase; + } + + .value { + display: flex; + height: 20px; + padding: 4px 0px; + justify-content: space-between; + align-items: center; + border: none !important; + .font-value { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 13px; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: 0.14px; + text-transform: capitalize; + } + .icon { + } + } + + .value:hover { + background-color: unset !important; + } + } + .menu-container { padding: 12px; .title { font-family: Inter; font-size: 11px; - font-weight: 600; + font-weight: 500; line-height: 18px; letter-spacing: 0.08em; text-align: left; - color: #52575c; + color: var(--bg-slate-50); } .menu-items { @@ -65,11 +174,11 @@ padding: 12px; .title { - color: #52575c; + color: var(--bg-slate-50); font-family: Inter; font-size: 11px; font-style: normal; - font-weight: 600; + font-weight: 500; line-height: 18px; /* 163.636% */ letter-spacing: 0.88px; text-transform: uppercase; @@ -149,11 +258,11 @@ } .title { - color: #52575c; + color: var(--bg-slate-50); font-family: Inter; font-size: 11px; font-style: normal; - font-weight: 600; + font-weight: 500; line-height: 18px; /* 163.636% */ letter-spacing: 0.88px; text-transform: uppercase; @@ -299,6 +408,38 @@ box-shadow: 4px 10px 16px 2px rgba(255, 255, 255, 0.2); + .font-size-dropdown { + .back-btn { + .text { + color: var(--bg-ink-400); + } + } + + .content { + .option-btn { + .text { + color: var(--bg-ink-400); + } + + .text:hover { + color: var(--bg-ink-300); + } + } + } + } + + .font-size-container { + .title { + color: var(--bg-ink-100); + } + + .value { + .font-value { + color: var(--bg-ink-400); + } + } + } + .horizontal-line { background: var(--bg-vanilla-300); } diff --git a/frontend/src/components/LogsFormatOptionsMenu/LogsFormatOptionsMenu.tsx b/frontend/src/components/LogsFormatOptionsMenu/LogsFormatOptionsMenu.tsx index 3a42e9a0b0..527c77c6af 100644 --- a/frontend/src/components/LogsFormatOptionsMenu/LogsFormatOptionsMenu.tsx +++ b/frontend/src/components/LogsFormatOptionsMenu/LogsFormatOptionsMenu.tsx @@ -3,12 +3,12 @@ /* eslint-disable jsx-a11y/click-events-have-key-events */ import './LogsFormatOptionsMenu.styles.scss'; -import { Divider, Input, InputNumber, Tooltip } from 'antd'; +import { Button, Divider, Input, InputNumber, Tooltip, Typography } from 'antd'; import cx from 'classnames'; import { LogViewMode } from 'container/LogsTable'; -import { OptionsMenuConfig } from 'container/OptionsMenu/types'; +import { FontSize, OptionsMenuConfig } from 'container/OptionsMenu/types'; import useDebouncedFn from 'hooks/useDebouncedFunction'; -import { Check, Minus, Plus, X } from 'lucide-react'; +import { Check, ChevronLeft, ChevronRight, Minus, Plus, X } from 'lucide-react'; import { useCallback, useEffect, useState } from 'react'; interface LogsFormatOptionsMenuProps { @@ -24,10 +24,16 @@ export default function LogsFormatOptionsMenu({ selectedOptionFormat, config, }: LogsFormatOptionsMenuProps): JSX.Element { - const { maxLines, format, addColumn } = config; + const { maxLines, format, addColumn, fontSize } = config; const [selectedItem, setSelectedItem] = useState(selectedOptionFormat); const maxLinesNumber = (maxLines?.value as number) || 1; const [maxLinesPerRow, setMaxLinesPerRow] = useState(maxLinesNumber); + const [fontSizeValue, setFontSizeValue] = useState( + fontSize?.value || FontSize.SMALL, + ); + const [isFontSizeOptionsOpen, setIsFontSizeOptionsOpen] = useState( + false, + ); const [addNewColumn, setAddNewColumn] = useState(false); @@ -88,6 +94,12 @@ export default function LogsFormatOptionsMenu({ } }, [maxLinesPerRow]); + useEffect(() => { + if (fontSizeValue && config && config.fontSize?.onChange) { + config.fontSize.onChange(fontSizeValue); + } + }, [fontSizeValue]); + return (
-
-
{title}
- -
- {items.map( - (item: any): JSX.Element => ( -
handleMenuItemClick(item.key)} - > -
- {item.label} - - {selectedItem === item.key && } -
-
- ), - )} + {isFontSizeOptionsOpen ? ( +
+ +
+
+ + + +
-
- - {selectedItem && ( + ) : ( <> - <> -
-
-
max lines per row
-
- - - -
-
- +
+
Font Size
+ +
+
+
+
{title}
-
- {!addNewColumn &&
} +
+ {items.map( + (item: any): JSX.Element => ( +
handleMenuItemClick(item.key)} + > +
+ {item.label} - {addNewColumn && ( -
-
- {' '} - columns - {' '} -
- - -
- )} - -
- {!addNewColumn && ( -
- columns - {' '} -
- )} - -
- {addColumn?.value?.map(({ key, id }) => ( -
-
- - {key} - + {selectedItem === item.key && }
- addColumn.onRemove(id as string)} - />
- ))} -
- - {addColumn?.isFetching && ( -
Loading ...
- )} - - {addNewColumn && - addColumn && - addColumn.value.length > 0 && - addColumn.options && - addColumn?.options?.length > 0 && ( - - )} - - {addNewColumn && ( -
- {addColumn?.options?.map(({ label, value }) => ( -
{ - eve.stopPropagation(); - - if (addColumn && addColumn?.onSelect) { - addColumn?.onSelect(value, { label, disabled: false }); - } - }} - > -
- - {label} - -
-
- ))} -
+ ), )}
+ + {selectedItem && ( + <> + <> +
+
+
max lines per row
+
+ + + +
+
+ + +
+ {!addNewColumn &&
} + + {addNewColumn && ( +
+
+ {' '} + columns + {' '} +
+ + +
+ )} + +
+ {!addNewColumn && ( +
+ columns + {' '} +
+ )} + +
+ {addColumn?.value?.map(({ key, id }) => ( +
+
+ + {key} + +
+ addColumn.onRemove(id as string)} + /> +
+ ))} +
+ + {addColumn?.isFetching && ( +
Loading ...
+ )} + + {addNewColumn && + addColumn && + addColumn.value.length > 0 && + addColumn.options && + addColumn?.options?.length > 0 && ( + + )} + + {addNewColumn && ( +
+ {addColumn?.options?.map(({ label, value }) => ( +
{ + eve.stopPropagation(); + + if (addColumn && addColumn?.onSelect) { + addColumn?.onSelect(value, { label, disabled: false }); + } + }} + > +
+ + {label} + +
+
+ ))} +
+ )} +
+
+ + )} )}
diff --git a/frontend/src/constants/env.ts b/frontend/src/constants/env.ts index 2c5230dfcc..cf75739eff 100644 --- a/frontend/src/constants/env.ts +++ b/frontend/src/constants/env.ts @@ -3,4 +3,5 @@ export const ENVIRONMENT = { process?.env?.FRONTEND_API_ENDPOINT || process?.env?.GITPOD_WORKSPACE_URL?.replace('://', '://8080-') || '', + wsURL: process?.env?.WEBSOCKET_API_ENDPOINT || '', }; diff --git a/frontend/src/constants/query.ts b/frontend/src/constants/query.ts index 9b731ca089..3ee0a39634 100644 --- a/frontend/src/constants/query.ts +++ b/frontend/src/constants/query.ts @@ -32,4 +32,8 @@ export enum QueryParams { relativeTime = 'relativeTime', alertType = 'alertType', ruleId = 'ruleId', + consumerGrp = 'consumerGrp', + topic = 'topic', + partition = 'partition', + selectedTimelineQuery = 'selectedTimelineQuery', } diff --git a/frontend/src/constants/queryBuilder.ts b/frontend/src/constants/queryBuilder.ts index 7b7b464b3e..5fe7112796 100644 --- a/frontend/src/constants/queryBuilder.ts +++ b/frontend/src/constants/queryBuilder.ts @@ -52,7 +52,7 @@ export const selectValueDivider = '__'; export const baseAutoCompleteIdKeysOrder: (keyof Omit< BaseAutocompleteData, - 'id' | 'isJSON' + 'id' | 'isJSON' | 'isIndexed' >)[] = ['key', 'dataType', 'type', 'isColumn']; export const autocompleteType: Record = { @@ -71,6 +71,7 @@ export const alphabet: string[] = alpha.map((str) => String.fromCharCode(str)); export enum QueryBuilderKeys { GET_AGGREGATE_ATTRIBUTE = 'GET_AGGREGATE_ATTRIBUTE', GET_AGGREGATE_KEYS = 'GET_AGGREGATE_KEYS', + GET_ATTRIBUTE_SUGGESTIONS = 'GET_ATTRIBUTE_SUGGESTIONS', } export const mapOfOperators = { diff --git a/frontend/src/constants/reactQueryKeys.ts b/frontend/src/constants/reactQueryKeys.ts index 63fc205d81..52ae235ef6 100644 --- a/frontend/src/constants/reactQueryKeys.ts +++ b/frontend/src/constants/reactQueryKeys.ts @@ -8,4 +8,5 @@ export const REACT_QUERY_KEY = { GET_FEATURES_FLAGS: 'GET_FEATURES_FLAGS', DELETE_DASHBOARD: 'DELETE_DASHBOARD', LOGS_PIPELINE_PREVIEW: 'LOGS_PIPELINE_PREVIEW', + GET_CONSUMER_LAG_DETAILS: 'GET_CONSUMER_LAG_DETAILS', }; diff --git a/frontend/src/constants/routes.ts b/frontend/src/constants/routes.ts index ef73184a86..8f76cd0386 100644 --- a/frontend/src/constants/routes.ts +++ b/frontend/src/constants/routes.ts @@ -54,6 +54,8 @@ const ROUTES = { WORKSPACE_LOCKED: '/workspace-locked', SHORTCUTS: '/shortcuts', INTEGRATIONS: '/integrations', + MESSAGING_QUEUES: '/messaging-queues', + MESSAGING_QUEUES_DETAIL: '/messaging-queues/detail', } as const; export default ROUTES; diff --git a/frontend/src/constants/shortcuts/globalShortcuts.ts b/frontend/src/constants/shortcuts/globalShortcuts.ts index 81420fc830..4ab7752fac 100644 --- a/frontend/src/constants/shortcuts/globalShortcuts.ts +++ b/frontend/src/constants/shortcuts/globalShortcuts.ts @@ -9,6 +9,7 @@ export const GlobalShortcuts = { NavigateToDashboards: 'd+shift', NavigateToAlerts: 'a+shift', NavigateToExceptions: 'e+shift', + NavigateToMessagingQueues: 'm+shift', }; export const GlobalShortcutsName = { @@ -19,6 +20,7 @@ export const GlobalShortcutsName = { NavigateToDashboards: 'shift+d', NavigateToAlerts: 'shift+a', NavigateToExceptions: 'shift+e', + NavigateToMessagingQueues: 'shift+m', }; export const GlobalShortcutsDescription = { @@ -29,4 +31,5 @@ export const GlobalShortcutsDescription = { NavigateToDashboards: 'Navigate to dashboards page', NavigateToAlerts: 'Navigate to alerts page', NavigateToExceptions: 'Navigate to Exceptions page', + NavigateToMessagingQueues: 'Navigate to Messaging Queues page', }; diff --git a/frontend/src/constants/shortcuts/logsExplorerShortcuts.ts b/frontend/src/constants/shortcuts/logsExplorerShortcuts.ts index 33c2b4061f..68331a4c2d 100644 --- a/frontend/src/constants/shortcuts/logsExplorerShortcuts.ts +++ b/frontend/src/constants/shortcuts/logsExplorerShortcuts.ts @@ -4,6 +4,7 @@ const userOS = getUserOperatingSystem(); export const LogsExplorerShortcuts = { StageAndRunQuery: 'enter+meta', FocusTheSearchBar: 's', + ShowAllFilters: '/+meta', }; export const LogsExplorerShortcutsName = { @@ -11,9 +12,11 @@ export const LogsExplorerShortcutsName = { userOS === UserOperatingSystem.MACOS ? 'cmd' : 'ctrl' }+enter`, FocusTheSearchBar: 's', + ShowAllFilters: `${userOS === UserOperatingSystem.MACOS ? 'cmd' : 'ctrl'}+/`, }; export const LogsExplorerShortcutsDescription = { StageAndRunQuery: 'Stage and Run the current query', FocusTheSearchBar: 'Shift the focus to the last query filter bar', + ShowAllFilters: 'Toggle all filters in the filters dropdown', }; diff --git a/frontend/src/container/AppLayout/index.tsx b/frontend/src/container/AppLayout/index.tsx index 0f267976d2..e821e67104 100644 --- a/frontend/src/container/AppLayout/index.tsx +++ b/frontend/src/container/AppLayout/index.tsx @@ -47,6 +47,7 @@ import { UPDATE_LATEST_VERSION_ERROR, } from 'types/actions/app'; import AppReducer from 'types/reducer/app'; +import { isCloudUser } from 'utils/app'; import { getFormattedDate, getRemainingDays } from 'utils/timeUtils'; import { ChildrenContainer, Layout, LayoutContent } from './styles'; @@ -71,7 +72,14 @@ function AppLayout(props: AppLayoutProps): JSX.Element { const isPremiumChatSupportEnabled = useFeatureFlags(FeatureKeys.PREMIUM_SUPPORT)?.active || false; + const isChatSupportEnabled = + useFeatureFlags(FeatureKeys.CHAT_SUPPORT)?.active || false; + + const isCloudUserVal = isCloudUser(); + const showAddCreditCardModal = + isChatSupportEnabled && + isCloudUserVal && !isPremiumChatSupportEnabled && !licenseData?.payload?.trialConvertedToSubscription; @@ -241,6 +249,9 @@ function AppLayout(props: AppLayoutProps): JSX.Element { const isTracesView = (): boolean => routeKey === 'TRACES_EXPLORER' || routeKey === 'TRACES_SAVE_VIEWS'; + const isMessagingQueues = (): boolean => + routeKey === 'MESSAGING_QUEUES' || routeKey === 'MESSAGING_QUEUES_DETAIL'; + const isDashboardListView = (): boolean => routeKey === 'ALL_DASHBOARD'; const isDashboardView = (): boolean => { /** @@ -329,7 +340,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element { isTracesView() || isDashboardView() || isDashboardWidgetView() || - isDashboardListView() + isDashboardListView() || + isMessagingQueues() ? 0 : '0 1rem', }} diff --git a/frontend/src/container/ExplorerOptions/ExplorerOptions.tsx b/frontend/src/container/ExplorerOptions/ExplorerOptions.tsx index 138694058e..44378e602b 100644 --- a/frontend/src/container/ExplorerOptions/ExplorerOptions.tsx +++ b/frontend/src/container/ExplorerOptions/ExplorerOptions.tsx @@ -33,6 +33,7 @@ import useErrorNotification from 'hooks/useErrorNotification'; import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange'; import { useNotifications } from 'hooks/useNotifications'; import { mapCompositeQueryFromQuery } from 'lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery'; +import { cloneDeep } from 'lodash-es'; import { Check, ConciergeBell, @@ -56,7 +57,7 @@ import { useHistory } from 'react-router-dom'; import { AppState } from 'store/reducers'; import { Dashboard } from 'types/api/dashboard/getAll'; import { Query } from 'types/api/queryBuilder/queryBuilderData'; -import { DataSource } from 'types/common/queryBuilder'; +import { DataSource, StringOperators } from 'types/common/queryBuilder'; import AppReducer from 'types/reducer/app'; import { USER_ROLES } from 'types/roles'; @@ -120,6 +121,21 @@ function ExplorerOptions({ const { role } = useSelector((state) => state.app); + const handleConditionalQueryModification = useCallback((): string => { + if ( + query?.builder?.queryData?.[0]?.aggregateOperator !== StringOperators.NOOP + ) { + return JSON.stringify(query); + } + + // Modify aggregateOperator to count, as noop is not supported in alerts + const modifiedQuery = cloneDeep(query); + + modifiedQuery.builder.queryData[0].aggregateOperator = StringOperators.COUNT; + + return JSON.stringify(modifiedQuery); + }, [query]); + const onCreateAlertsHandler = useCallback(() => { if (sourcepage === DataSource.TRACES) { logEvent('Traces Explorer: Create alert', { @@ -130,13 +146,16 @@ function ExplorerOptions({ panelType, }); } + + const stringifiedQuery = handleConditionalQueryModification(); + history.push( `${ROUTES.ALERTS_NEW}?${QueryParams.compositeQuery}=${encodeURIComponent( - JSON.stringify(query), + stringifiedQuery, )}`, ); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [history, query]); + }, [handleConditionalQueryModification, history]); const onCancel = (value: boolean) => (): void => { onModalToggle(value); @@ -482,6 +501,7 @@ function ExplorerOptions({ shape="circle" onClick={hideToolbar} icon={} + data-testid="hide-toolbar" />
@@ -511,6 +531,7 @@ function ExplorerOptions({ icon={} onClick={onSaveHandler} disabled={isSaveViewLoading} + data-testid="save-view-btn" > Save this view , diff --git a/frontend/src/container/ExplorerOptions/ExplorerOptionsHideArea.tsx b/frontend/src/container/ExplorerOptions/ExplorerOptionsHideArea.tsx index a420c25ecc..efdaef1cd1 100644 --- a/frontend/src/container/ExplorerOptions/ExplorerOptionsHideArea.tsx +++ b/frontend/src/container/ExplorerOptions/ExplorerOptionsHideArea.tsx @@ -65,6 +65,7 @@ function ExplorerOptionsHideArea({ // style={{ alignSelf: 'center', marginRight: 'calc(10% - 20px)' }} className="explorer-show-btn" onClick={handleShowExplorerOption} + data-testid="show-explorer-option" >
diff --git a/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx b/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx index d0e16857be..b76c7c9f73 100644 --- a/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx +++ b/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx @@ -47,6 +47,7 @@ function WidgetGraphComponent({ setRequestData, onClickHandler, onDragSelect, + customTooltipElement, }: WidgetGraphComponentProps): JSX.Element { const [deleteModal, setDeleteModal] = useState(false); const [hovered, setHovered] = useState(false); @@ -335,6 +336,7 @@ function WidgetGraphComponent({ onClickHandler={onClickHandler} onDragSelect={onDragSelect} tableProcessedDataRef={tableProcessedDataRef} + customTooltipElement={customTooltipElement} />
)} diff --git a/frontend/src/container/GridCardLayout/GridCard/index.tsx b/frontend/src/container/GridCardLayout/GridCard/index.tsx index 8e34e32879..d7d2e729cb 100644 --- a/frontend/src/container/GridCardLayout/GridCard/index.tsx +++ b/frontend/src/container/GridCardLayout/GridCard/index.tsx @@ -33,6 +33,7 @@ function GridCardGraph({ version, onClickHandler, onDragSelect, + customTooltipElement, }: GridCardGraphProps): JSX.Element { const dispatch = useDispatch(); const [errorMessage, setErrorMessage] = useState(); @@ -215,6 +216,7 @@ function GridCardGraph({ setRequestData={setRequestData} onClickHandler={onClickHandler} onDragSelect={onDragSelect} + customTooltipElement={customTooltipElement} /> )}
diff --git a/frontend/src/container/GridCardLayout/GridCard/types.ts b/frontend/src/container/GridCardLayout/GridCard/types.ts index 1235a26440..d0edede5a1 100644 --- a/frontend/src/container/GridCardLayout/GridCard/types.ts +++ b/frontend/src/container/GridCardLayout/GridCard/types.ts @@ -31,6 +31,7 @@ export interface WidgetGraphComponentProps { setRequestData?: Dispatch>; onClickHandler?: OnClickPluginOpts['onClick']; onDragSelect: (start: number, end: number) => void; + customTooltipElement?: HTMLDivElement; } export interface GridCardGraphProps { @@ -42,6 +43,7 @@ export interface GridCardGraphProps { variables?: Dashboard['data']['variables']; version?: string; onDragSelect: (start: number, end: number) => void; + customTooltipElement?: HTMLDivElement; } export interface GetGraphVisibilityStateOnLegendClickProps { diff --git a/frontend/src/container/GridCardLayout/GridCardLayout.tsx b/frontend/src/container/GridCardLayout/GridCardLayout.tsx index 75fde3ce7a..a96599b127 100644 --- a/frontend/src/container/GridCardLayout/GridCardLayout.tsx +++ b/frontend/src/container/GridCardLayout/GridCardLayout.tsx @@ -194,7 +194,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element { urlQuery.set(QueryParams.startTime, startTimestamp.toString()); urlQuery.set(QueryParams.endTime, endTimestamp.toString()); const generatedUrl = `${pathname}?${urlQuery.toString()}`; - history.replace(generatedUrl); + history.push(generatedUrl); if (startTimestamp !== endTimestamp) { dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp])); diff --git a/frontend/src/container/ListOfDashboard/DashboardList.styles.scss b/frontend/src/container/ListOfDashboard/DashboardList.styles.scss index 7fff109d2a..cf9ec283d2 100644 --- a/frontend/src/container/ListOfDashboard/DashboardList.styles.scss +++ b/frontend/src/container/ListOfDashboard/DashboardList.styles.scss @@ -590,6 +590,8 @@ } .new-dashboard-menu { + width: 200px; + .create-dashboard-menu-item { display: flex; align-items: center; @@ -1067,7 +1069,7 @@ color: var(--bg-ink-500); } .subtitle { - color: var(--bg-vanilla-400); + color: var(--bg-ink-300); } .ant-table-row { @@ -1087,6 +1089,10 @@ .dashboard-title { color: var(--bg-slate-300); + + .title { + color: var(--bg-ink-500); + } } .title-with-action { diff --git a/frontend/src/container/ListOfDashboard/DashboardsList.tsx b/frontend/src/container/ListOfDashboard/DashboardsList.tsx index 8de34fdaf1..421c7e31c4 100644 --- a/frontend/src/container/ListOfDashboard/DashboardsList.tsx +++ b/frontend/src/container/ListOfDashboard/DashboardsList.tsx @@ -45,6 +45,8 @@ import { Ellipsis, EllipsisVertical, Expand, + ExternalLink, + Github, HdmiPort, LayoutGrid, Link2, @@ -53,6 +55,8 @@ import { RotateCw, Search, } from 'lucide-react'; +// #TODO: lucide will be removing brand icons like Github in future, in that case we can use simple icons +// see more: https://github.com/lucide-icons/lucide/issues/94 import { handleContactSupport } from 'pages/Integrations/utils'; import { useDashboard } from 'providers/Dashboard/Dashboard'; import { @@ -600,6 +604,28 @@ function DashboardsList(): JSX.Element { ), key: '1', }, + { + label: ( + + +
+ View templates +
+ +
+
+ ), + key: '2', + }, ]; if (createNewDashboard) { diff --git a/frontend/src/container/ListOfDashboard/ImportJSON/index.tsx b/frontend/src/container/ListOfDashboard/ImportJSON/index.tsx index 9bf1db2051..62767e6799 100644 --- a/frontend/src/container/ListOfDashboard/ImportJSON/index.tsx +++ b/frontend/src/container/ListOfDashboard/ImportJSON/index.tsx @@ -4,7 +4,15 @@ import { red } from '@ant-design/colors'; import { ExclamationCircleTwoTone } from '@ant-design/icons'; import MEditor, { Monaco } from '@monaco-editor/react'; import { Color } from '@signozhq/design-tokens'; -import { Button, Modal, Space, Typography, Upload, UploadProps } from 'antd'; +import { + Button, + Flex, + Modal, + Space, + Typography, + Upload, + UploadProps, +} from 'antd'; import logEvent from 'api/common/logEvent'; import createDashboard from 'api/dashboard/create'; import ROUTES from 'constants/routes'; @@ -13,7 +21,9 @@ import { MESSAGE } from 'hooks/useFeatureFlag'; import { useNotifications } from 'hooks/useNotifications'; import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout'; import history from 'lib/history'; -import { MonitorDot, MoveRight, X } from 'lucide-react'; +import { ExternalLink, Github, MonitorDot, MoveRight, X } from 'lucide-react'; +// #TODO: Lucide will be removing brand icons like GitHub in the future. In that case, we can use Simple Icons. https://simpleicons.org/ +// See more: https://github.com/lucide-icons/lucide/issues/94 import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { generatePath } from 'react-router-dom'; @@ -174,27 +184,43 @@ function ImportJSON({ )}
- false} - action="none" - data={jsonData} - > - - + + + + + +
- ); - }, + render: (fieldData: Record, record): JSX.Element => ( + + ), }, ]; function sortPinnedAttributes( @@ -379,9 +321,10 @@ function TableView({ TableView.defaultProps = { isListViewPanel: false, listViewPanelSelectedFields: null, + onGroupByAttribute: undefined, }; -interface DataType { +export interface DataType { key: string; field: string; value: string; diff --git a/frontend/src/container/LogDetailedView/TableView/TableViewActions.styles.scss b/frontend/src/container/LogDetailedView/TableView/TableViewActions.styles.scss new file mode 100644 index 0000000000..f5a45ef416 --- /dev/null +++ b/frontend/src/container/LogDetailedView/TableView/TableViewActions.styles.scss @@ -0,0 +1,61 @@ +.open-popover { + &.value-field { + .action-btn { + display: flex !important; + position: absolute !important; + top: 50% !important; + right: 16px !important; + transform: translateY(-50%) !important; + gap: 4px !important; + } + } +} + +.table-view-actions-content { + .ant-popover-inner { + border-radius: 4px; + border: 1px solid var(--bg-slate-400); + background: linear-gradient( + 139deg, + rgba(18, 19, 23, 0.8) 0%, + rgba(18, 19, 23, 0.9) 98.68% + ); + box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2); + backdrop-filter: blur(20px); + padding: 0px; + .group-by-clause { + display: flex; + align-items: center; + gap: 4px; + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: 0.14px; + padding: 12px 18px 12px 14px; + + .ant-btn-icon { + margin-inline-end: 0px; + } + } + + .group-by-clause:hover { + background-color: unset !important; + } + } +} + +.lightMode { + .table-view-actions-content { + .ant-popover-inner { + border: 1px solid var(--bg-vanilla-400); + background: var(--bg-vanilla-100) !important; + + .group-by-clause { + color: var(--bg-ink-400); + } + } + } +} diff --git a/frontend/src/container/LogDetailedView/TableView/TableViewActions.tsx b/frontend/src/container/LogDetailedView/TableView/TableViewActions.tsx new file mode 100644 index 0000000000..63912ffa82 --- /dev/null +++ b/frontend/src/container/LogDetailedView/TableView/TableViewActions.tsx @@ -0,0 +1,185 @@ +import './TableViewActions.styles.scss'; + +import { Color } from '@signozhq/design-tokens'; +import Convert from 'ansi-to-html'; +import { Button, Popover, Spin, Tooltip, Tree } from 'antd'; +import GroupByIcon from 'assets/CustomIcons/GroupByIcon'; +import cx from 'classnames'; +import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC'; +import { OPERATORS } from 'constants/queryBuilder'; +import ROUTES from 'constants/routes'; +import dompurify from 'dompurify'; +import { isEmpty } from 'lodash-es'; +import { ArrowDownToDot, ArrowUpFromDot, Ellipsis } from 'lucide-react'; +import { useMemo, useState } from 'react'; +import { useLocation } from 'react-router-dom'; +import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { FORBID_DOM_PURIFY_TAGS } from 'utils/app'; + +import { DataType } from '../TableView'; +import { + filterKeyForField, + jsonToDataNodes, + recursiveParseJSON, + removeEscapeCharacters, + unescapeString, +} from '../utils'; + +interface ITableViewActionsProps { + fieldData: Record; + record: DataType; + isListViewPanel: boolean; + isfilterInLoading: boolean; + isfilterOutLoading: boolean; + onGroupByAttribute?: ( + fieldKey: string, + isJSON?: boolean, + dataType?: DataTypes, + ) => Promise; + onClickHandler: ( + operator: string, + fieldKey: string, + fieldValue: string, + ) => () => void; +} + +const convert = new Convert(); + +export function TableViewActions( + props: ITableViewActionsProps, +): React.ReactElement { + const { + fieldData, + record, + isListViewPanel, + isfilterInLoading, + isfilterOutLoading, + onClickHandler, + onGroupByAttribute, + } = props; + + const { pathname } = useLocation(); + + // there is no option for where clause in old logs explorer and live logs page + const isOldLogsExplorerOrLiveLogsPage = useMemo( + () => pathname === ROUTES.OLD_LOGS_EXPLORER || pathname === ROUTES.LIVE_LOGS, + [pathname], + ); + + const [isOpen, setIsOpen] = useState(false); + const textToCopy = fieldData.value; + + if (record.field === 'body') { + const parsedBody = recursiveParseJSON(fieldData.value); + if (!isEmpty(parsedBody)) { + return ( + + ); + } + } + const bodyHtml = + record.field === 'body' + ? { + __html: convert.toHtml( + dompurify.sanitize(unescapeString(record.value), { + FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS], + }), + ), + } + : { __html: '' }; + + const fieldFilterKey = filterKeyForField(fieldData.field); + + return ( +
+ {record.field === 'body' ? ( + + + + ) : ( + + + {removeEscapeCharacters(fieldData.value)} + + + )} + + {!isListViewPanel && ( + + + +
+ } + rootClassName="table-view-actions-content" + trigger="hover" + placement="bottomLeft" + > +
+ ); +} + +TableViewActions.defaultProps = { + onGroupByAttribute: undefined, +}; diff --git a/frontend/src/container/LogDetailedView/utils.tsx b/frontend/src/container/LogDetailedView/utils.tsx index 8418e13893..766bb8b5bf 100644 --- a/frontend/src/container/LogDetailedView/utils.tsx +++ b/frontend/src/container/LogDetailedView/utils.tsx @@ -250,19 +250,37 @@ export const getDataTypes = (value: unknown): DataTypes => { return determineType(value); }; +// now we do not want to render colors everywhere like in tooltip and monaco editor hence we remove such codes to make +// the log line readable export const removeEscapeCharacters = (str: string): string => - str.replace(/\\([ntfr'"\\])/g, (_: string, char: string) => { - const escapeMap: Record = { - n: '\n', - t: '\t', - f: '\f', - r: '\r', - "'": "'", - '"': '"', - '\\': '\\', - }; - return escapeMap[char as keyof typeof escapeMap]; - }); + str + .replace(/\\x1[bB][[0-9;]*m/g, '') + .replace(/\\u001[bB][[0-9;]*m/g, '') + .replace(/\\x[0-9A-Fa-f]{2}/g, '') + .replace(/\\u[0-9A-Fa-f]{4}/g, '') + .replace(/\\[btnfrv0'"\\]/g, ''); + +// we need to remove the escape from the escaped characters as some recievers like file log escape the unicode escape characters. +// example: Log [\u001B[32;1mThis is bright green\u001B[0m] is being sent as [\\u001B[32;1mThis is bright green\\u001B[0m] +// +// so we need to remove this escapes to render the color properly +export const unescapeString = (str: string): string => + str + .replace(/\\n/g, '\n') // Replaces escaped newlines + .replace(/\\r/g, '\r') // Replaces escaped carriage returns + .replace(/\\t/g, '\t') // Replaces escaped tabs + .replace(/\\b/g, '\b') // Replaces escaped backspaces + .replace(/\\f/g, '\f') // Replaces escaped form feeds + .replace(/\\v/g, '\v') // Replaces escaped vertical tabs + .replace(/\\'/g, "'") // Replaces escaped single quotes + .replace(/\\"/g, '"') // Replaces escaped double quotes + .replace(/\\\\/g, '\\') // Replaces escaped backslashes + .replace(/\\x([0-9A-Fa-f]{2})/g, (_, hex) => + String.fromCharCode(parseInt(hex, 16)), + ) // Replaces hexadecimal escape sequences + .replace(/\\u([0-9A-Fa-f]{4})/g, (_, hex) => + String.fromCharCode(parseInt(hex, 16)), + ); // Replaces Unicode escape sequences export function removeExtraSpaces(input: string): string { return input.replace(/\s+/g, ' ').trim(); diff --git a/frontend/src/container/LogsContextList/configs.ts b/frontend/src/container/LogsContextList/configs.ts index baa3b39420..9b70dfc5be 100644 --- a/frontend/src/container/LogsContextList/configs.ts +++ b/frontend/src/container/LogsContextList/configs.ts @@ -1,6 +1,7 @@ import { OrderByPayload } from 'types/api/queryBuilder/queryBuilderData'; export const INITIAL_PAGE_SIZE = 10; +export const INITIAL_PAGE_SIZE_SMALL_FONT = 12; export const LOGS_MORE_PAGE_SIZE = 10; export const getOrderByTimestamp = (order: string): OrderByPayload => ({ diff --git a/frontend/src/container/LogsContextList/index.tsx b/frontend/src/container/LogsContextList/index.tsx index 270291a33e..d215386c03 100644 --- a/frontend/src/container/LogsContextList/index.tsx +++ b/frontend/src/container/LogsContextList/index.tsx @@ -5,6 +5,7 @@ import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar'; import Spinner from 'components/Spinner'; import { DEFAULT_ENTITY_VERSION } from 'constants/app'; import { PANEL_TYPES } from 'constants/queryBuilder'; +import { FontSize } from 'container/OptionsMenu/types'; import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config'; import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange'; import { useIsDarkMode } from 'hooks/useDarkMode'; @@ -167,6 +168,7 @@ function LogsContextList({ key={log.id} data={log} linesPerRow={1} + fontSize={FontSize.SMALL} /> ), [], diff --git a/frontend/src/container/LogsExplorerContext/index.tsx b/frontend/src/container/LogsExplorerContext/index.tsx index d62cdb274b..32075097e5 100644 --- a/frontend/src/container/LogsExplorerContext/index.tsx +++ b/frontend/src/container/LogsExplorerContext/index.tsx @@ -2,6 +2,7 @@ import { EditFilled } from '@ant-design/icons'; import { Modal, Typography } from 'antd'; import RawLogView from 'components/Logs/RawLogView'; import LogsContextList from 'container/LogsContextList'; +import { FontSize } from 'container/OptionsMenu/types'; import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config'; import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch'; import { useIsDarkMode } from 'hooks/useDarkMode'; @@ -99,6 +100,7 @@ function LogsExplorerContext({ isTextOverflowEllipsisDisabled data={log} linesPerRow={1} + fontSize={FontSize.SMALL} /> void; logs: ILog[]; hasActions: boolean; + fontSize: FontSize; } export default function TableRow({ @@ -33,6 +35,7 @@ export default function TableRow({ handleSetActiveContextLog, logs, hasActions, + fontSize, }: TableRowProps): JSX.Element { const isDarkMode = useIsDarkMode(); @@ -78,6 +81,7 @@ export default function TableRow({ $isDragColumn={false} $isDarkMode={isDarkMode} key={column.key} + fontSize={fontSize} > {cloneElement(children, props)} diff --git a/frontend/src/container/LogsExplorerList/InfinityTableView/config.ts b/frontend/src/container/LogsExplorerList/InfinityTableView/config.ts index ec16ba1024..c235cbd5e7 100644 --- a/frontend/src/container/LogsExplorerList/InfinityTableView/config.ts +++ b/frontend/src/container/LogsExplorerList/InfinityTableView/config.ts @@ -1,3 +1,5 @@ +/* eslint-disable no-nested-ternary */ +import { FontSize } from 'container/OptionsMenu/types'; import { CSSProperties } from 'react'; export const infinityDefaultStyles: CSSProperties = { @@ -5,3 +7,16 @@ export const infinityDefaultStyles: CSSProperties = { overflowX: 'scroll', marginTop: '15px', }; + +export function getInfinityDefaultStyles(fontSize: FontSize): CSSProperties { + return { + width: '100%', + overflowX: 'scroll', + marginTop: + fontSize === FontSize.SMALL + ? '10px' + : fontSize === FontSize.MEDIUM + ? '12px' + : '15px', + }; +} diff --git a/frontend/src/container/LogsExplorerList/InfinityTableView/index.tsx b/frontend/src/container/LogsExplorerList/InfinityTableView/index.tsx index 2875575162..fe2d2ba1d4 100644 --- a/frontend/src/container/LogsExplorerList/InfinityTableView/index.tsx +++ b/frontend/src/container/LogsExplorerList/InfinityTableView/index.tsx @@ -15,7 +15,7 @@ import { } from 'react-virtuoso'; import { ILog } from 'types/api/logs/log'; -import { infinityDefaultStyles } from './config'; +import { getInfinityDefaultStyles } from './config'; import { LogsCustomTable } from './LogsCustomTable'; import { TableHeaderCellStyled, TableRowStyled } from './styles'; import TableRow from './TableRow'; @@ -59,6 +59,7 @@ const InfinityTable = forwardRef( onSetActiveLog, onClearActiveLog, onAddToQuery, + onGroupByAttribute, } = useActiveLog(); const { dataSource, columns } = useTableView({ @@ -95,9 +96,15 @@ const InfinityTable = forwardRef( handleSetActiveContextLog={handleSetActiveContextLog} logs={tableViewProps.logs} hasActions + fontSize={tableViewProps.fontSize} /> ), - [handleSetActiveContextLog, tableColumns, tableViewProps.logs], + [ + handleSetActiveContextLog, + tableColumns, + tableViewProps.fontSize, + tableViewProps.logs, + ], ); const tableHeader = useCallback( @@ -112,6 +119,7 @@ const InfinityTable = forwardRef( $isDarkMode={isDarkMode} $isDragColumn={isDragColumn} key={column.key} + fontSize={tableViewProps?.fontSize} // eslint-disable-next-line react/jsx-props-no-spreading {...(isDragColumn && { className: 'dragHandler' })} > @@ -121,7 +129,7 @@ const InfinityTable = forwardRef( })} ), - [tableColumns, isDarkMode], + [tableColumns, isDarkMode, tableViewProps?.fontSize], ); const handleClickExpand = (index: number): void => { @@ -137,7 +145,7 @@ const InfinityTable = forwardRef( initialTopMostItemIndex={ tableViewProps.activeLogIndex !== -1 ? tableViewProps.activeLogIndex : 0 } - style={infinityDefaultStyles} + style={getInfinityDefaultStyles(tableViewProps.fontSize)} data={dataSource} components={{ // eslint-disable-next-line react/jsx-props-no-spreading @@ -165,6 +173,7 @@ const InfinityTable = forwardRef( onClose={handleClearActiveContextLog} onAddToQuery={handleAddToQuery} selectedTab={VIEW_TYPES.CONTEXT} + onGroupByAttribute={onGroupByAttribute} /> )} ( onClose={onClearActiveLog} onAddToQuery={onAddToQuery} onClickActionItem={onAddToQuery} + onGroupByAttribute={onGroupByAttribute} /> ); diff --git a/frontend/src/container/LogsExplorerList/InfinityTableView/styles.ts b/frontend/src/container/LogsExplorerList/InfinityTableView/styles.ts index e2719dbc3f..89c9592dd4 100644 --- a/frontend/src/container/LogsExplorerList/InfinityTableView/styles.ts +++ b/frontend/src/container/LogsExplorerList/InfinityTableView/styles.ts @@ -1,5 +1,7 @@ +/* eslint-disable no-nested-ternary */ import { Color } from '@signozhq/design-tokens'; import { themeColors } from 'constants/theme'; +import { FontSize } from 'container/OptionsMenu/types'; import styled from 'styled-components'; import { getActiveLogBackground } from 'utils/logs'; @@ -7,6 +9,7 @@ interface TableHeaderCellStyledProps { $isDragColumn: boolean; $isDarkMode: boolean; $isTimestamp?: boolean; + fontSize?: FontSize; } export const TableStyled = styled.table` @@ -15,6 +18,14 @@ export const TableStyled = styled.table` export const TableCellStyled = styled.td` padding: 0.5rem; + ${({ fontSize }): string => + fontSize === FontSize.SMALL + ? `padding:0.3rem;` + : fontSize === FontSize.MEDIUM + ? `padding:0.4rem;` + : fontSize === FontSize.LARGE + ? `padding:0.5rem;` + : ``} background-color: ${(props): string => props.$isDarkMode ? 'inherit' : themeColors.whiteCream}; @@ -33,7 +44,7 @@ export const TableRowStyled = styled.tr<{ ? `background-color: ${ $isDarkMode ? Color.BG_SLATE_500 : Color.BG_VANILLA_300 } !important` - : ''} + : ''}; } cursor: pointer; @@ -66,9 +77,17 @@ export const TableHeaderCellStyled = styled.th` line-height: 18px; letter-spacing: -0.07px; background: ${(props): string => (props.$isDarkMode ? '#0b0c0d' : '#fdfdfd')}; - ${({ $isTimestamp }): string => ($isTimestamp ? 'padding-left: 24px;' : '')} ${({ $isDragColumn }): string => ($isDragColumn ? 'cursor: col-resize;' : '')} + ${({ fontSize }): string => + fontSize === FontSize.SMALL + ? `font-size:11px; line-height:16px; padding: 0.1rem;` + : fontSize === FontSize.MEDIUM + ? `font-size:13px; line-height:20px; padding:0.3rem;` + : fontSize === FontSize.LARGE + ? `font-size:14px; line-height:24px; padding: 0.5rem;` + : ``}; + ${({ $isTimestamp }): string => ($isTimestamp ? 'padding-left: 24px;' : '')} color: ${(props): string => props.$isDarkMode ? 'var(--bg-vanilla-100, #fff)' : themeColors.bckgGrey}; `; diff --git a/frontend/src/container/LogsExplorerList/index.tsx b/frontend/src/container/LogsExplorerList/index.tsx index 760f3fea30..dd97916ce6 100644 --- a/frontend/src/container/LogsExplorerList/index.tsx +++ b/frontend/src/container/LogsExplorerList/index.tsx @@ -14,6 +14,7 @@ import EmptyLogsSearch from 'container/EmptyLogsSearch/EmptyLogsSearch'; import LogsError from 'container/LogsError/LogsError'; import { LogsLoading } from 'container/LogsLoading/LogsLoading'; import { useOptionsMenu } from 'container/OptionsMenu'; +import { FontSize } from 'container/OptionsMenu/types'; import { useActiveLog } from 'hooks/logs/useActiveLog'; import { useCopyLogLink } from 'hooks/logs/useCopyLogLink'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; @@ -50,6 +51,7 @@ function LogsExplorerList({ activeLog, onClearActiveLog, onAddToQuery, + onGroupByAttribute, onSetActiveLog, } = useActiveLog(); @@ -79,6 +81,7 @@ function LogsExplorerList({ data={log} linesPerRow={options.maxLines} selectedFields={selectedFields} + fontSize={options.fontSize} /> ); } @@ -91,6 +94,7 @@ function LogsExplorerList({ onAddToQuery={onAddToQuery} onSetActiveLog={onSetActiveLog} activeLog={activeLog} + fontSize={options.fontSize} linesPerRow={options.maxLines} /> ); @@ -99,6 +103,7 @@ function LogsExplorerList({ activeLog, onAddToQuery, onSetActiveLog, + options.fontSize, options.format, options.maxLines, selectedFields, @@ -121,6 +126,7 @@ function LogsExplorerList({ logs, fields: selectedFields, linesPerRow: options.maxLines, + fontSize: options.fontSize, appendTo: 'end', activeLogIndex, }} @@ -129,13 +135,27 @@ function LogsExplorerList({ ); } + function getMarginTop(): string { + switch (options.fontSize) { + case FontSize.SMALL: + return '10px'; + case FontSize.MEDIUM: + return '12px'; + case FontSize.LARGE: + return '15px'; + default: + return '15px'; + } + } + return ( diff --git a/frontend/src/container/LogsExplorerViews/LogsExplorerViews.styles.scss b/frontend/src/container/LogsExplorerViews/LogsExplorerViews.styles.scss index a6142d195c..3821e7025c 100644 --- a/frontend/src/container/LogsExplorerViews/LogsExplorerViews.styles.scss +++ b/frontend/src/container/LogsExplorerViews/LogsExplorerViews.styles.scss @@ -80,6 +80,36 @@ position: relative; } } + .query-stats { + display: flex; + align-items: center; + gap: 12px; + .rows { + color: var(--bg-vanilla-400); + font-family: 'Geist Mono'; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 150% */ + letter-spacing: 0.36px; + } + + .divider { + width: 1px; + height: 14px; + background: #242834; + } + + .time { + color: var(--bg-vanilla-400); + font-family: 'Geist Mono'; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 150% */ + letter-spacing: 0.36px; + } + } } .logs-actions-container { @@ -149,6 +179,15 @@ background: var(--bg-robin-400); } } + .query-stats { + .rows { + color: var(--bg-ink-400); + } + + .time { + color: var(--bg-ink-400); + } + } } } } diff --git a/frontend/src/container/LogsExplorerViews/QueryStatus.styles.scss b/frontend/src/container/LogsExplorerViews/QueryStatus.styles.scss new file mode 100644 index 0000000000..d6f9579113 --- /dev/null +++ b/frontend/src/container/LogsExplorerViews/QueryStatus.styles.scss @@ -0,0 +1,4 @@ +.query-status { + display: flex; + align-items: center; +} diff --git a/frontend/src/container/LogsExplorerViews/QueryStatus.tsx b/frontend/src/container/LogsExplorerViews/QueryStatus.tsx new file mode 100644 index 0000000000..628b3d5fc1 --- /dev/null +++ b/frontend/src/container/LogsExplorerViews/QueryStatus.tsx @@ -0,0 +1,42 @@ +import './QueryStatus.styles.scss'; + +import { LoadingOutlined } from '@ant-design/icons'; +import { Color } from '@signozhq/design-tokens'; +import { Spin } from 'antd'; +import { CircleCheck } from 'lucide-react'; +import React, { useMemo } from 'react'; + +interface IQueryStatusProps { + loading: boolean; + error: boolean; + success: boolean; +} + +export default function QueryStatus( + props: IQueryStatusProps, +): React.ReactElement { + const { loading, error, success } = props; + + const content = useMemo((): React.ReactElement => { + if (loading) { + return } />; + } + if (error) { + return ( + header + ); + } + if (success) { + return ( + + ); + } + return
; + }, [error, loading, success]); + return
{content}
; +} diff --git a/frontend/src/container/LogsExplorerViews/index.tsx b/frontend/src/container/LogsExplorerViews/index.tsx index 6318e1da71..06a39a4da4 100644 --- a/frontend/src/container/LogsExplorerViews/index.tsx +++ b/frontend/src/container/LogsExplorerViews/index.tsx @@ -1,8 +1,10 @@ /* eslint-disable sonarjs/cognitive-complexity */ import './LogsExplorerViews.styles.scss'; -import { Button } from 'antd'; +import { Button, Typography } from 'antd'; +import { getQueryStats, WsDataEvent } from 'api/common/getQueryStats'; import logEvent from 'api/common/logEvent'; +import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig'; import LogsFormatOptionsMenu from 'components/LogsFormatOptionsMenu/LogsFormatOptionsMenu'; import { DEFAULT_ENTITY_VERSION } from 'constants/app'; import { LOCALSTORAGE } from 'constants/localStorage'; @@ -48,7 +50,15 @@ import { } from 'lodash-es'; import { Sliders } from 'lucide-react'; import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils'; -import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { + memo, + MutableRefObject, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; import { useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; import { AppState } from 'store/reducers'; @@ -69,12 +79,20 @@ import { GlobalReducer } from 'types/reducer/globalTime'; import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink'; import { v4 } from 'uuid'; +import QueryStatus from './QueryStatus'; + function LogsExplorerViews({ selectedView, showFrequencyChart, + setIsLoadingQueries, + listQueryKeyRef, + chartQueryKeyRef, }: { selectedView: SELECTED_VIEWS; showFrequencyChart: boolean; + setIsLoadingQueries: React.Dispatch>; + listQueryKeyRef: MutableRefObject; + chartQueryKeyRef: MutableRefObject; }): JSX.Element { const { notifications } = useNotifications(); const history = useHistory(); @@ -82,14 +100,14 @@ function LogsExplorerViews({ // this is to respect the panel type present in the URL rather than defaulting it to list always. const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST); - const { activeLogId, timeRange, onTimeRangeChange } = useCopyLogLink(); + const { activeLogId, onTimeRangeChange } = useCopyLogLink(); const { queryData: pageSize } = useUrlQueryData( QueryParams.pageSize, DEFAULT_PER_PAGE_VALUE, ); - const { minTime } = useSelector( + const { minTime, maxTime } = useSelector( (state) => state.globalTime, ); @@ -116,6 +134,8 @@ function LogsExplorerViews({ const [logs, setLogs] = useState([]); const [requestData, setRequestData] = useState(null); const [showFormatMenuItems, setShowFormatMenuItems] = useState(false); + const [queryId, setQueryId] = useState(v4()); + const [queryStats, setQueryStats] = useState(); const handleAxisError = useAxiosError(); @@ -214,9 +234,18 @@ function LogsExplorerViews({ { enabled: !!listChartQuery && panelType === PANEL_TYPES.LIST, }, + {}, + undefined, + chartQueryKeyRef, ); - const { data, isLoading, isFetching, isError } = useGetExplorerQueryRange( + const { + data, + isLoading, + isFetching, + isError, + isSuccess, + } = useGetExplorerQueryRange( requestData, panelType, DEFAULT_ENTITY_VERSION, @@ -225,13 +254,18 @@ function LogsExplorerViews({ enabled: !isLimit && !!requestData, }, { - ...(timeRange && - activeLogId && + ...(activeLogId && !logs.length && { - start: timeRange.start, - end: timeRange.end, + start: minTime, + end: maxTime, }), }, + undefined, + listQueryKeyRef, + { + ...(!isEmpty(queryId) && + selectedPanelType !== PANEL_TYPES.LIST && { 'X-SIGNOZ-QUERY-ID': queryId }), + }, ); const getRequestData = useCallback( @@ -318,6 +352,23 @@ function LogsExplorerViews({ ], ); + useEffect(() => { + setQueryId(v4()); + }, [data]); + + useEffect(() => { + if ( + !isEmpty(queryId) && + (isLoading || isFetching) && + selectedPanelType !== PANEL_TYPES.LIST + ) { + setQueryStats(undefined); + setTimeout(() => { + getQueryStats({ queryId, setData: setQueryStats }); + }, 500); + } + }, [queryId, isLoading, isFetching, selectedPanelType]); + const logEventCalledRef = useRef(false); useEffect(() => { if (!logEventCalledRef.current && !isUndefined(data?.payload)) { @@ -469,7 +520,7 @@ function LogsExplorerViews({ setLogs(newLogs); onTimeRangeChange({ start: currentParams?.start, - end: timeRange?.end || currentParams?.end, + end: currentParams?.end, pageSize: newLogs.length, }); } @@ -486,8 +537,7 @@ function LogsExplorerViews({ filters: listQuery?.filters || initialFilters, page: 1, log: null, - pageSize: - timeRange?.pageSize && activeLogId ? timeRange?.pageSize : pageSize, + pageSize, }); setLogs([]); @@ -502,7 +552,6 @@ function LogsExplorerViews({ listQuery, pageSize, minTime, - timeRange, activeLogId, onTimeRangeChange, panelType, @@ -569,6 +618,25 @@ function LogsExplorerViews({ }, }); + useEffect(() => { + if ( + isLoading || + isFetching || + isLoadingListChartData || + isFetchingListChartData + ) { + setIsLoadingQueries(true); + } else { + setIsLoadingQueries(false); + } + }, [ + isLoading, + isFetching, + isFetchingListChartData, + isLoadingListChartData, + setIsLoadingQueries, + ]); + const flattenLogData = useMemo( () => logs.map((log) => { @@ -665,6 +733,30 @@ function LogsExplorerViews({
)} + {(selectedPanelType === PANEL_TYPES.TIME_SERIES || + selectedPanelType === PANEL_TYPES.TABLE) && ( +
+ + {queryStats?.read_rows && ( + + {getYAxisFormattedValue(queryStats.read_rows?.toString(), 'short')}{' '} + rows + + )} + {queryStats?.elapsed_ms && ( + <> +
+ + {getYAxisFormattedValue(queryStats?.elapsed_ms?.toString(), 'ms')} + + + )} +
+ )}
diff --git a/frontend/src/container/LogsExplorerViews/tests/LogsExplorerViews.test.tsx b/frontend/src/container/LogsExplorerViews/tests/LogsExplorerViews.test.tsx index a1ae308efd..8262d6f9bc 100644 --- a/frontend/src/container/LogsExplorerViews/tests/LogsExplorerViews.test.tsx +++ b/frontend/src/container/LogsExplorerViews/tests/LogsExplorerViews.test.tsx @@ -46,6 +46,10 @@ jest.mock( }, ); +jest.mock('api/common/getQueryStats', () => ({ + getQueryStats: jest.fn(), +})); + jest.mock('constants/panelTypes', () => ({ AVAILABLE_EXPORT_PANEL_TYPES: ['graph', 'table'], })); @@ -79,6 +83,9 @@ const renderer = (): RenderResult => {}} + listQueryKeyRef={{ current: {} }} + chartQueryKeyRef={{ current: {} }} /> diff --git a/frontend/src/container/LogsPanelTable/LogsPanelComponent.tsx b/frontend/src/container/LogsPanelTable/LogsPanelComponent.tsx index 3835386cd3..a7598b5c76 100644 --- a/frontend/src/container/LogsPanelTable/LogsPanelComponent.tsx +++ b/frontend/src/container/LogsPanelTable/LogsPanelComponent.tsx @@ -108,6 +108,7 @@ function LogsPanelComponent({ onSetActiveLog, onClearActiveLog, onAddToQuery, + onGroupByAttribute, } = useActiveLog(); const handleRow = useCallback( @@ -244,6 +245,7 @@ function LogsPanelComponent({ onClose={onClearActiveLog} onAddToQuery={onAddToQuery} onClickActionItem={onAddToQuery} + onGroupByAttribute={onGroupByAttribute} isListViewPanel listViewPanelSelectedFields={widget?.selectedLogFields} /> diff --git a/frontend/src/container/LogsTable/index.tsx b/frontend/src/container/LogsTable/index.tsx index 6b20986d1f..577533adcf 100644 --- a/frontend/src/container/LogsTable/index.tsx +++ b/frontend/src/container/LogsTable/index.tsx @@ -10,11 +10,14 @@ import LogsTableView from 'components/Logs/TableView'; import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar'; import Spinner from 'components/Spinner'; import { CARD_BODY_STYLE } from 'constants/card'; +import { LOCALSTORAGE } from 'constants/localStorage'; +import { useOptionsMenu } from 'container/OptionsMenu'; import { useActiveLog } from 'hooks/logs/useActiveLog'; import { memo, useCallback, useMemo } from 'react'; import { useSelector } from 'react-redux'; import { Virtuoso } from 'react-virtuoso'; import { AppState } from 'store/reducers'; +import { DataSource, StringOperators } from 'types/common/queryBuilder'; // interfaces import { ILogsReducer } from 'types/reducer/logs'; @@ -35,6 +38,7 @@ function LogsTable(props: LogsTableProps): JSX.Element { activeLog, onClearActiveLog, onAddToQuery, + onGroupByAttribute, onSetActiveLog, } = useActiveLog(); @@ -55,6 +59,14 @@ function LogsTable(props: LogsTableProps): JSX.Element { liveTail, ]); + const { options } = useOptionsMenu({ + storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS, + // this component will alwyays be called on old logs explorer page itself! + dataSource: DataSource.LOGS, + // and we do not have table / timeseries aggregated views in the old logs explorer! + aggregateOperator: StringOperators.NOOP, + }); + const getItemContent = useCallback( (index: number): JSX.Element => { const log = logs[index]; @@ -66,6 +78,7 @@ function LogsTable(props: LogsTableProps): JSX.Element { data={log} linesPerRow={linesPerRow} selectedFields={selected} + fontSize={options.fontSize} /> ); } @@ -78,10 +91,19 @@ function LogsTable(props: LogsTableProps): JSX.Element { linesPerRow={linesPerRow} onAddToQuery={onAddToQuery} onSetActiveLog={onSetActiveLog} + fontSize={options.fontSize} /> ); }, - [logs, viewMode, selected, onAddToQuery, onSetActiveLog, linesPerRow], + [ + logs, + viewMode, + selected, + linesPerRow, + onAddToQuery, + onSetActiveLog, + options.fontSize, + ], ); const renderContent = useMemo(() => { @@ -92,6 +114,7 @@ function LogsTable(props: LogsTableProps): JSX.Element { logs={logs} fields={selected} linesPerRow={linesPerRow} + fontSize={options.fontSize} /> ); } @@ -103,7 +126,15 @@ function LogsTable(props: LogsTableProps): JSX.Element { ); - }, [getItemContent, linesPerRow, logs, onSetActiveLog, selected, viewMode]); + }, [ + getItemContent, + linesPerRow, + logs, + onSetActiveLog, + options.fontSize, + selected, + viewMode, + ]); if (isLoading) { return ; @@ -126,6 +157,7 @@ function LogsTable(props: LogsTableProps): JSX.Element { selectedTab={VIEW_TYPES.OVERVIEW} log={activeLog} onClose={onClearActiveLog} + onGroupByAttribute={onGroupByAttribute} onAddToQuery={onAddToQuery} onClickActionItem={onAddToQuery} /> diff --git a/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx b/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx index 3ee49ddc9a..da7bbbfc60 100644 --- a/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx @@ -1,6 +1,7 @@ import { Col } from 'antd'; import logEvent from 'api/common/logEvent'; import { ENTITY_VERSION_V4 } from 'constants/app'; +import { QueryParams } from 'constants/query'; import { PANEL_TYPES } from 'constants/queryBuilder'; import Graph from 'container/GridCardLayout/GridCard'; import { @@ -12,8 +13,12 @@ import { convertRawQueriesToTraceSelectedTags, resourceAttributesToTagFilterItems, } from 'hooks/useResourceAttribute/utils'; -import { useEffect, useMemo, useRef, useState } from 'react'; -import { useParams } from 'react-router-dom'; +import useUrlQuery from 'hooks/useUrlQuery'; +import history from 'lib/history'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useDispatch } from 'react-redux'; +import { useLocation, useParams } from 'react-router-dom'; +import { UpdateTimeInterval } from 'store/actions'; import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData'; import { EQueryType } from 'types/common/dashboard'; import { v4 as uuid } from 'uuid'; @@ -37,6 +42,26 @@ function DBCall(): JSX.Element { const servicename = decodeURIComponent(encodedServiceName); const [selectedTimeStamp, setSelectedTimeStamp] = useState(0); const { queries } = useResourceAttribute(); + const urlQuery = useUrlQuery(); + const { pathname } = useLocation(); + const dispatch = useDispatch(); + + const onDragSelect = useCallback( + (start: number, end: number) => { + const startTimestamp = Math.trunc(start); + const endTimestamp = Math.trunc(end); + + urlQuery.set(QueryParams.startTime, startTimestamp.toString()); + urlQuery.set(QueryParams.endTime, endTimestamp.toString()); + const generatedUrl = `${pathname}?${urlQuery.toString()}`; + history.push(generatedUrl); + + if (startTimestamp !== endTimestamp) { + dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp])); + } + }, + [dispatch, pathname, urlQuery], + ); const tagFilterItems: TagFilterItem[] = useMemo( () => @@ -150,6 +175,7 @@ function DBCall(): JSX.Element { 'database_call_rps', ); }} + onDragSelect={onDragSelect} version={ENTITY_VERSION_V4} /> @@ -185,6 +211,7 @@ function DBCall(): JSX.Element { 'database_call_avg_duration', ); }} + onDragSelect={onDragSelect} version={ENTITY_VERSION_V4} /> diff --git a/frontend/src/container/MetricsApplication/Tabs/External.tsx b/frontend/src/container/MetricsApplication/Tabs/External.tsx index 69abd6696d..5ba3d3df6c 100644 --- a/frontend/src/container/MetricsApplication/Tabs/External.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/External.tsx @@ -1,6 +1,7 @@ import { Col } from 'antd'; import logEvent from 'api/common/logEvent'; import { ENTITY_VERSION_V4 } from 'constants/app'; +import { QueryParams } from 'constants/query'; import { PANEL_TYPES } from 'constants/queryBuilder'; import Graph from 'container/GridCardLayout/GridCard'; import { @@ -14,8 +15,12 @@ import { convertRawQueriesToTraceSelectedTags, resourceAttributesToTagFilterItems, } from 'hooks/useResourceAttribute/utils'; -import { useEffect, useMemo, useRef, useState } from 'react'; -import { useParams } from 'react-router-dom'; +import useUrlQuery from 'hooks/useUrlQuery'; +import history from 'lib/history'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useDispatch } from 'react-redux'; +import { useLocation, useParams } from 'react-router-dom'; +import { UpdateTimeInterval } from 'store/actions'; import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { EQueryType } from 'types/common/dashboard'; import { v4 as uuid } from 'uuid'; @@ -40,6 +45,27 @@ function External(): JSX.Element { const servicename = decodeURIComponent(encodedServiceName); const { queries } = useResourceAttribute(); + const urlQuery = useUrlQuery(); + const { pathname } = useLocation(); + const dispatch = useDispatch(); + + const onDragSelect = useCallback( + (start: number, end: number) => { + const startTimestamp = Math.trunc(start); + const endTimestamp = Math.trunc(end); + + urlQuery.set(QueryParams.startTime, startTimestamp.toString()); + urlQuery.set(QueryParams.endTime, endTimestamp.toString()); + const generatedUrl = `${pathname}?${urlQuery.toString()}`; + history.push(generatedUrl); + + if (startTimestamp !== endTimestamp) { + dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp])); + } + }, + [dispatch, pathname, urlQuery], + ); + const tagFilterItems = useMemo( () => handleNonInQueryRange(resourceAttributesToTagFilterItems(queries)) || [], @@ -214,6 +240,7 @@ function External(): JSX.Element { 'external_call_error_percentage', ); }} + onDragSelect={onDragSelect} version={ENTITY_VERSION_V4} /> @@ -249,6 +276,7 @@ function External(): JSX.Element { 'external_call_duration', ); }} + onDragSelect={onDragSelect} version={ENTITY_VERSION_V4} /> @@ -285,6 +313,7 @@ function External(): JSX.Element { 'external_call_rps_by_address', ) } + onDragSelect={onDragSelect} version={ENTITY_VERSION_V4} /> @@ -320,6 +349,7 @@ function External(): JSX.Element { 'external_call_duration_by_address', ); }} + onDragSelect={onDragSelect} version={ENTITY_VERSION_V4} /> diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview.tsx b/frontend/src/container/MetricsApplication/Tabs/Overview.tsx index 916c3435b5..241395b23a 100644 --- a/frontend/src/container/MetricsApplication/Tabs/Overview.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/Overview.tsx @@ -185,7 +185,7 @@ function Application(): JSX.Element { urlQuery.set(QueryParams.startTime, startTimestamp.toString()); urlQuery.set(QueryParams.endTime, endTimestamp.toString()); const generatedUrl = `${pathname}?${urlQuery.toString()}`; - history.replace(generatedUrl); + history.push(generatedUrl); if (startTimestamp !== endTimestamp) { dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp])); diff --git a/frontend/src/container/MySettings/index.tsx b/frontend/src/container/MySettings/index.tsx index 234911796e..de6c12eb53 100644 --- a/frontend/src/container/MySettings/index.tsx +++ b/frontend/src/container/MySettings/index.tsx @@ -26,7 +26,9 @@ function MySettings(): JSX.Element { label: (
Light{' '} - Beta + + Beta +
), value: 'light', diff --git a/frontend/src/container/OnboardingContainer/OnboardingContainer.tsx b/frontend/src/container/OnboardingContainer/OnboardingContainer.tsx index 3129d76c8e..c1275ff115 100644 --- a/frontend/src/container/OnboardingContainer/OnboardingContainer.tsx +++ b/frontend/src/container/OnboardingContainer/OnboardingContainer.tsx @@ -247,8 +247,7 @@ export default function Onboarding(): JSX.Element { const handleNext = (): void => { if (activeStep <= 3) { - handleNextStep(); - history.replace(moduleRouteMap[selectedModule.id as ModulesMap]); + history.push(moduleRouteMap[selectedModule.id as ModulesMap]); } }; @@ -258,6 +257,13 @@ export default function Onboarding(): JSX.Element { updateSelectedDataSource(null); }; + const handleBackNavigation = (): void => { + setCurrent(0); + setActiveStep(1); + setSelectedModule(useCases.APM); + resetProgress(); + }; + useEffect(() => { const { pathname } = location; @@ -277,9 +283,11 @@ export default function Onboarding(): JSX.Element { } else if (pathname === ROUTES.GET_STARTED_AZURE_MONITORING) { handleModuleSelect(useCases.AzureMonitoring); handleNextStep(); + } else { + handleBackNavigation(); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [location.pathname]); const [form] = Form.useForm(); const [ diff --git a/frontend/src/container/OnboardingContainer/Steps/DataSource/DataSource.tsx b/frontend/src/container/OnboardingContainer/Steps/DataSource/DataSource.tsx index 138aac775c..f2f7028bdc 100644 --- a/frontend/src/container/OnboardingContainer/Steps/DataSource/DataSource.tsx +++ b/frontend/src/container/OnboardingContainer/Steps/DataSource/DataSource.tsx @@ -131,6 +131,11 @@ export default function DataSource(): JSX.Element { }; const goToIntegrationsPage = (): void => { + logEvent('Onboarding V2: Go to integrations', { + module: selectedModule?.id, + dataSource: selectedDataSource?.name, + framework: selectedFramework, + }); history.push(ROUTES.INTEGRATIONS); }; diff --git a/frontend/src/container/OnboardingContainer/common/ModuleStepsContainer/ModuleStepsContainer.tsx b/frontend/src/container/OnboardingContainer/common/ModuleStepsContainer/ModuleStepsContainer.tsx index ae74930d57..f9efa061f7 100644 --- a/frontend/src/container/OnboardingContainer/common/ModuleStepsContainer/ModuleStepsContainer.tsx +++ b/frontend/src/container/OnboardingContainer/common/ModuleStepsContainer/ModuleStepsContainer.tsx @@ -11,13 +11,15 @@ import { } from '@ant-design/icons'; import { Button, Space, Steps, Typography } from 'antd'; import logEvent from 'api/common/logEvent'; +import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport'; +import { onboardingHelpMessage } from 'components/LaunchChatSupport/util'; import ROUTES from 'constants/routes'; import { stepsMap } from 'container/OnboardingContainer/constants/stepsConfig'; import { DataSourceType } from 'container/OnboardingContainer/Steps/DataSource/DataSource'; import { hasFrameworks } from 'container/OnboardingContainer/utils/dataSourceUtils'; import history from 'lib/history'; import { isEmpty, isNull } from 'lodash-es'; -import { HelpCircle, UserPlus } from 'lucide-react'; +import { UserPlus } from 'lucide-react'; import { SetStateAction, useState } from 'react'; import { useOnboardingContext } from '../../context/OnboardingContext'; @@ -381,31 +383,6 @@ export default function ModuleStepsContainer({ history.push('/'); }; - const handleFacingIssuesClick = (): void => { - logEvent('Onboarding V2: Facing Issues Sending Data to SigNoz', { - dataSource: selectedDataSource?.id, - framework: selectedFramework, - environment: selectedEnvironment, - module: activeStep?.module?.id, - step: activeStep?.step?.id, - }); - - const message = `Hi Team, - -I am facing issues sending data to SigNoz. Here are my application details - -Data Source: ${selectedDataSource?.name} -Framework: -Environment: -Module: ${activeStep?.module?.id} - -Thanks -`; - if (window.Intercom) { - window.Intercom('showNewMessage', message); - } - }; - return (
@@ -493,19 +470,26 @@ Thanks > Back - - - +
diff --git a/frontend/src/container/OptionsMenu/constants.ts b/frontend/src/container/OptionsMenu/constants.ts index 2db02f85b8..7b591cd4c5 100644 --- a/frontend/src/container/OptionsMenu/constants.ts +++ b/frontend/src/container/OptionsMenu/constants.ts @@ -1,6 +1,6 @@ import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; -import { OptionsQuery } from './types'; +import { FontSize, OptionsQuery } from './types'; export const URL_OPTIONS = 'options'; @@ -8,6 +8,7 @@ export const defaultOptionsQuery: OptionsQuery = { selectColumns: [], maxLines: 2, format: 'list', + fontSize: FontSize.SMALL, }; export const defaultTraceSelectedColumns = [ @@ -18,6 +19,7 @@ export const defaultTraceSelectedColumns = [ isColumn: true, isJSON: false, id: 'serviceName--string--tag--true', + isIndexed: false, }, { key: 'name', @@ -26,6 +28,7 @@ export const defaultTraceSelectedColumns = [ isColumn: true, isJSON: false, id: 'name--string--tag--true', + isIndexed: false, }, { key: 'durationNano', @@ -34,6 +37,7 @@ export const defaultTraceSelectedColumns = [ isColumn: true, isJSON: false, id: 'durationNano--float64--tag--true', + isIndexed: false, }, { key: 'httpMethod', @@ -42,6 +46,7 @@ export const defaultTraceSelectedColumns = [ isColumn: true, isJSON: false, id: 'httpMethod--string--tag--true', + isIndexed: false, }, { key: 'responseStatusCode', @@ -50,5 +55,6 @@ export const defaultTraceSelectedColumns = [ isColumn: true, isJSON: false, id: 'responseStatusCode--string--tag--true', + isIndexed: false, }, ]; diff --git a/frontend/src/container/OptionsMenu/types.ts b/frontend/src/container/OptionsMenu/types.ts index 57b81364d6..2c57d66b28 100644 --- a/frontend/src/container/OptionsMenu/types.ts +++ b/frontend/src/container/OptionsMenu/types.ts @@ -2,10 +2,21 @@ import { InputNumberProps, RadioProps, SelectProps } from 'antd'; import { LogViewMode } from 'container/LogsTable'; import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; +export enum FontSize { + SMALL = 'small', + MEDIUM = 'medium', + LARGE = 'large', +} + +interface FontSizeProps { + value: FontSize; + onChange: (val: FontSize) => void; +} export interface OptionsQuery { selectColumns: BaseAutocompleteData[]; maxLines: number; format: LogViewMode; + fontSize: FontSize; } export interface InitialOptions @@ -18,6 +29,7 @@ export type OptionsMenuConfig = { onChange: (value: LogViewMode) => void; }; maxLines?: Pick; + fontSize?: FontSizeProps; addColumn?: Pick< SelectProps, 'options' | 'onSelect' | 'onFocus' | 'onSearch' | 'onBlur' diff --git a/frontend/src/container/OptionsMenu/useOptionsMenu.ts b/frontend/src/container/OptionsMenu/useOptionsMenu.ts index 97fbbbb006..7b3cfce035 100644 --- a/frontend/src/container/OptionsMenu/useOptionsMenu.ts +++ b/frontend/src/container/OptionsMenu/useOptionsMenu.ts @@ -7,6 +7,10 @@ import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys'; import useDebounce from 'hooks/useDebounce'; import { useNotifications } from 'hooks/useNotifications'; import useUrlQueryData from 'hooks/useUrlQueryData'; +import { + AllTraceFilterKeys, + AllTraceFilterKeyValue, +} from 'pages/TracesExplorer/Filter/filterUtils'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useQueries } from 'react-query'; import { ErrorResponse, SuccessResponse } from 'types/api'; @@ -21,7 +25,12 @@ import { defaultTraceSelectedColumns, URL_OPTIONS, } from './constants'; -import { InitialOptions, OptionsMenuConfig, OptionsQuery } from './types'; +import { + FontSize, + InitialOptions, + OptionsMenuConfig, + OptionsQuery, +} from './types'; import { getOptionsFromKeys } from './utils'; interface UseOptionsMenuProps { @@ -106,15 +115,40 @@ const useOptionsMenu = ({ [] as BaseAutocompleteData[], ); - return ( - (initialOptions.selectColumns - ?.map((column) => attributesData.find(({ key }) => key === column)) - .filter(Boolean) as BaseAutocompleteData[]) || [] - ); + let initialSelected = initialOptions.selectColumns + ?.map((column) => attributesData.find(({ key }) => key === column)) + .filter(Boolean) as BaseAutocompleteData[]; + + if (dataSource === DataSource.TRACES) { + initialSelected = initialSelected + ?.map((col) => { + if (col && Object.keys(AllTraceFilterKeyValue).includes(col?.key)) { + const metaData = defaultTraceSelectedColumns.find( + (coln) => coln.key === (col.key as AllTraceFilterKeys), + ); + + return { + ...metaData, + key: metaData?.key, + dataType: metaData?.dataType, + type: metaData?.type, + isColumn: metaData?.isColumn, + isJSON: metaData?.isJSON, + id: metaData?.id, + }; + } + return col; + }) + .filter(Boolean) as BaseAutocompleteData[]; + } + + return initialSelected || []; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [ isFetchedInitialAttributes, initialOptions?.selectColumns, initialAttributesResult, + dataSource, ]); const { @@ -248,6 +282,17 @@ const useOptionsMenu = ({ }, [handleRedirectWithOptionsData, optionsQueryData], ); + const handleFontSizeChange = useCallback( + (value: FontSize) => { + const optionsData: OptionsQuery = { + ...optionsQueryData, + fontSize: value, + }; + + handleRedirectWithOptionsData(optionsData); + }, + [handleRedirectWithOptionsData, optionsQueryData], + ); const handleSearchAttribute = useCallback((value: string) => { setSearchText(value); @@ -282,18 +327,24 @@ const useOptionsMenu = ({ value: optionsQueryData.maxLines || defaultOptionsQuery.maxLines, onChange: handleMaxLinesChange, }, + fontSize: { + value: optionsQueryData?.fontSize || defaultOptionsQuery.fontSize, + onChange: handleFontSizeChange, + }, }), [ - optionsFromAttributeKeys, - optionsQueryData?.maxLines, - optionsQueryData?.format, - optionsQueryData?.selectColumns, isSearchedAttributesFetching, - handleSearchAttribute, + optionsQueryData?.selectColumns, + optionsQueryData.format, + optionsQueryData.maxLines, + optionsQueryData?.fontSize, + optionsFromAttributeKeys, handleSelectColumns, handleRemoveSelectedColumn, + handleSearchAttribute, handleFormatChange, handleMaxLinesChange, + handleFontSizeChange, ], ); diff --git a/frontend/src/container/PanelWrapper/PanelWrapper.tsx b/frontend/src/container/PanelWrapper/PanelWrapper.tsx index fdc20f7d30..ed105b3948 100644 --- a/frontend/src/container/PanelWrapper/PanelWrapper.tsx +++ b/frontend/src/container/PanelWrapper/PanelWrapper.tsx @@ -15,6 +15,7 @@ function PanelWrapper({ onDragSelect, selectedGraph, tableProcessedDataRef, + customTooltipElement, }: PanelWrapperProps): JSX.Element { const Component = PanelTypeVsPanelWrapper[ selectedGraph || widget.panelTypes @@ -37,6 +38,7 @@ function PanelWrapper({ onDragSelect={onDragSelect} selectedGraph={selectedGraph} tableProcessedDataRef={tableProcessedDataRef} + customTooltipElement={customTooltipElement} /> ); } diff --git a/frontend/src/container/PanelWrapper/UplotPanelWrapper.tsx b/frontend/src/container/PanelWrapper/UplotPanelWrapper.tsx index dc404adabb..ff231b563a 100644 --- a/frontend/src/container/PanelWrapper/UplotPanelWrapper.tsx +++ b/frontend/src/container/PanelWrapper/UplotPanelWrapper.tsx @@ -30,6 +30,7 @@ function UplotPanelWrapper({ onClickHandler, onDragSelect, selectedGraph, + customTooltipElement, }: PanelWrapperProps): JSX.Element { const { toScrollWidgetId, setToScrollWidgetId } = useDashboard(); const isDarkMode = useIsDarkMode(); @@ -126,6 +127,7 @@ function UplotPanelWrapper({ stackBarChart: widget?.stackedBarChart, hiddenGraph, setHiddenGraph, + customTooltipElement, }), [ widget?.id, @@ -147,6 +149,7 @@ function UplotPanelWrapper({ selectedGraph, currentQuery, hiddenGraph, + customTooltipElement, ], ); diff --git a/frontend/src/container/PanelWrapper/panelWrapper.types.ts b/frontend/src/container/PanelWrapper/panelWrapper.types.ts index 6fd993f377..7d5e3122e8 100644 --- a/frontend/src/container/PanelWrapper/panelWrapper.types.ts +++ b/frontend/src/container/PanelWrapper/panelWrapper.types.ts @@ -23,6 +23,7 @@ export type PanelWrapperProps = { onDragSelect: (start: number, end: number) => void; selectedGraph?: PANEL_TYPES; tableProcessedDataRef?: React.MutableRefObject; + customTooltipElement?: HTMLDivElement; }; export type TooltipData = { diff --git a/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/LogsList/index.tsx b/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/LogsList/index.tsx index f7d3af3a88..5bbe6de737 100644 --- a/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/LogsList/index.tsx +++ b/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/LogsList/index.tsx @@ -13,6 +13,7 @@ function LogsList({ logs }: LogsListProps): JSX.Element { onSetActiveLog, onClearActiveLog, onAddToQuery, + onGroupByAttribute, } = useActiveLog(); const makeLogDetailsHandler = (log: ILog) => (): void => onSetActiveLog(log); @@ -42,6 +43,7 @@ function LogsList({ logs }: LogsListProps): JSX.Element { onClose={onClearActiveLog} onAddToQuery={onAddToQuery} onClickActionItem={onAddToQuery} + onGroupByAttribute={onGroupByAttribute} />
); diff --git a/frontend/src/container/QueryBuilder/components/ToolbarActions/RightToolbarActions.tsx b/frontend/src/container/QueryBuilder/components/ToolbarActions/RightToolbarActions.tsx index aea205d569..f5106dffbe 100644 --- a/frontend/src/container/QueryBuilder/components/ToolbarActions/RightToolbarActions.tsx +++ b/frontend/src/container/QueryBuilder/components/ToolbarActions/RightToolbarActions.tsx @@ -3,18 +3,27 @@ import './ToolbarActions.styles.scss'; import { Button } from 'antd'; import { LogsExplorerShortcuts } from 'constants/shortcuts/logsExplorerShortcuts'; import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys'; -import { Play } from 'lucide-react'; -import { useEffect } from 'react'; +import { Play, X } from 'lucide-react'; +import { MutableRefObject, useEffect } from 'react'; +import { useQueryClient } from 'react-query'; interface RightToolbarActionsProps { onStageRunQuery: () => void; + isLoadingQueries?: boolean; + listQueryKeyRef?: MutableRefObject; + chartQueryKeyRef?: MutableRefObject; } export default function RightToolbarActions({ onStageRunQuery, + isLoadingQueries, + listQueryKeyRef, + chartQueryKeyRef, }: RightToolbarActionsProps): JSX.Element { const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys(); + const queryClient = useQueryClient(); + useEffect(() => { registerShortcut(LogsExplorerShortcuts.StageAndRunQuery, onStageRunQuery); @@ -25,14 +34,41 @@ export default function RightToolbarActions({ }, [onStageRunQuery]); return (
- + {isLoadingQueries ? ( +
+ +
+ ) : ( + + )}
); } + +RightToolbarActions.defaultProps = { + isLoadingQueries: false, + listQueryKeyRef: null, + chartQueryKeyRef: null, +}; diff --git a/frontend/src/container/QueryBuilder/components/ToolbarActions/ToolbarActions.styles.scss b/frontend/src/container/QueryBuilder/components/ToolbarActions/ToolbarActions.styles.scss index a848cf8680..2eda1b7c86 100644 --- a/frontend/src/container/QueryBuilder/components/ToolbarActions/ToolbarActions.styles.scss +++ b/frontend/src/container/QueryBuilder/components/ToolbarActions/ToolbarActions.styles.scss @@ -5,8 +5,8 @@ .left-toolbar-query-actions { display: flex; border-radius: 2px; - border: 1px solid var(--bg-slate-400, #1d212d); - background: var(--bg-ink-300, #16181d); + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); flex-direction: row; .prom-ql-icon { @@ -24,7 +24,7 @@ border-radius: 0; &.active-tab { - background-color: #1d212d; + background-color: var(--bg-slate-400); } &:disabled { @@ -33,7 +33,7 @@ } } .action-btn + .action-btn { - border-left: 1px solid var(--bg-slate-400, #1d212d); + border-left: 1px solid var(--bg-slate-400); } } @@ -51,6 +51,50 @@ background-color: var(--bg-robin-600); } +.right-actions { + display: flex; + align-items: center; +} + +.loading-container { + display: flex; + gap: 8px; + align-items: center; + + .loading-btn { + display: flex; + width: 32px; + height: 33px; + padding: 4px 10px; + justify-content: center; + align-items: center; + gap: 6px; + flex-shrink: 0; + border-radius: 2px; + background: var(--bg-slate-300); + box-shadow: none; + border: none; + } + + .cancel-run { + display: flex; + height: 33px; + padding: 4px 10px; + justify-content: center; + align-items: center; + gap: 6px; + flex: 1 0 0; + border-radius: 2px; + background: var(--bg-cherry-500); + border-color: none; + } + .cancel-run:hover { + background-color: #ff7875 !important; + color: var(--bg-vanilla-100) !important; + border: none; + } +} + .lightMode { .left-toolbar { .left-toolbar-query-actions { @@ -68,4 +112,17 @@ } } } + .loading-container { + .loading-btn { + background: var(--bg-vanilla-300); + } + + .cancel-run { + color: var(--bg-vanilla-100); + } + + .cancel-run:hover { + background-color: #ff7875; + } + } } diff --git a/frontend/src/container/QueryBuilder/components/ToolbarActions/tests/ToolbarActions.test.tsx b/frontend/src/container/QueryBuilder/components/ToolbarActions/tests/ToolbarActions.test.tsx index 751bdbf99d..79ee020802 100644 --- a/frontend/src/container/QueryBuilder/components/ToolbarActions/tests/ToolbarActions.test.tsx +++ b/frontend/src/container/QueryBuilder/components/ToolbarActions/tests/ToolbarActions.test.tsx @@ -1,6 +1,7 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils'; +import MockQueryClientProvider from 'providers/test/MockQueryClientProvider'; import LeftToolbarActions from '../LeftToolbarActions'; import RightToolbarActions from '../RightToolbarActions'; @@ -94,7 +95,9 @@ describe('ToolbarActions', () => { it('RightToolbarActions - render correctly with props', async () => { const onStageRunQuery = jest.fn(); const { queryByText } = render( - , + + , + , ); const stageNRunBtn = queryByText('Stage & Run Query'); diff --git a/frontend/src/container/QueryBuilder/filters/AggregatorFilter/AggregatorFilter.tsx b/frontend/src/container/QueryBuilder/filters/AggregatorFilter/AggregatorFilter.tsx index 492329f69b..36c54aa9a1 100644 --- a/frontend/src/container/QueryBuilder/filters/AggregatorFilter/AggregatorFilter.tsx +++ b/frontend/src/container/QueryBuilder/filters/AggregatorFilter/AggregatorFilter.tsx @@ -62,7 +62,9 @@ export const AggregatorFilter = memo(function AggregatorFilter({ dataSource: query.dataSource, }), { - enabled: !!query.aggregateOperator && !!query.dataSource, + enabled: + query.dataSource === DataSource.METRICS || + (!!query.aggregateOperator && !!query.dataSource), onSuccess: (data) => { const options: ExtendedSelectOption[] = data?.payload?.attributeKeys?.map(({ id: _, ...item }) => ({ diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/ExampleQueriesRendererForLogs.tsx b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/ExampleQueriesRendererForLogs.tsx new file mode 100644 index 0000000000..df8921b4d1 --- /dev/null +++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/ExampleQueriesRendererForLogs.tsx @@ -0,0 +1,30 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ +import './QueryBuilderSearch.styles.scss'; + +import { TagFilter } from 'types/api/queryBuilder/queryBuilderData'; + +function ExampleQueriesRendererForLogs({ + label, + value, + handleAddTag, +}: ExampleQueriesRendererForLogsProps): JSX.Element { + return ( +
{ + handleAddTag(value); + }} + > + {label} +
+ ); +} + +interface ExampleQueriesRendererForLogsProps { + label: string; + value: TagFilter; + handleAddTag: (value: TagFilter) => void; +} + +export default ExampleQueriesRendererForLogs; diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/OptionRendererForLogs.tsx b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/OptionRendererForLogs.tsx new file mode 100644 index 0000000000..a1b5149a05 --- /dev/null +++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/OptionRendererForLogs.tsx @@ -0,0 +1,77 @@ +import './QueryBuilderSearch.styles.scss'; + +import { Color } from '@signozhq/design-tokens'; +import { Tooltip, Typography } from 'antd'; +import cx from 'classnames'; +import { Zap } from 'lucide-react'; +import { useState } from 'react'; + +import { getOptionType } from './utils'; + +function OptionRendererForLogs({ + label, + value, + dataType, + isIndexed, + setDynamicPlaceholder, +}: OptionRendererProps): JSX.Element { + const [truncated, setTruncated] = useState(false); + const optionType = getOptionType(label); + + return ( + setDynamicPlaceholder(value)} + onFocus={(): void => setDynamicPlaceholder(value)} + > + {optionType ? ( + +
+
+ {isIndexed ? ( + + ) : ( +
+ )} + setTruncated(ellipsis) }} + > + {value} + +
+
+
{dataType}
+
+
+ {optionType} +
+
+
+
+ ) : ( + +
+
+ setTruncated(ellipsis) }} + > + {label} + +
+ + )} + + ); +} + +interface OptionRendererProps { + label: string; + value: string; + dataType: string; + isIndexed: boolean; + setDynamicPlaceholder: React.Dispatch>; +} + +export default OptionRendererForLogs; diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/QueryBuilderSearch.styles.scss b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/QueryBuilderSearch.styles.scss index a6f5fcaf37..db03f5a862 100644 --- a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/QueryBuilderSearch.styles.scss +++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/QueryBuilderSearch.styles.scss @@ -11,6 +11,290 @@ } } +.logs-popup { + &.hide-scroll { + .rc-virtual-list-holder { + height: 100px; + } + } +} + +.logs-explorer-popup { + padding: 0px; + .ant-select-item-group { + padding: 12px 14px 8px 14px; + color: var(--bg-slate-50); + font-family: Inter; + font-size: 11px; + font-style: normal; + font-weight: 500; + line-height: 18px; /* 163.636% */ + letter-spacing: 0.88px; + text-transform: uppercase; + } + + .show-all-filter-props { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 13px; + width: 100%; + cursor: pointer; + + .content { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + + .left-section { + display: flex; + align-items: center; + gap: 4px; + + .text { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + } + + .text:hover { + color: var(--bg-vanilla-100); + } + } + .right-section { + display: flex; + align-items: center; + gap: 4px; + .keyboard-shortcut-slash { + width: 16px; + height: 16px; + flex-shrink: 0; + border-radius: 2.286px; + border-top: 1.143px solid var(--bg-ink-200); + border-right: 1.143px solid var(--bg-ink-200); + border-bottom: 2.286px solid var(--bg-ink-200); + border-left: 1.143px solid var(--bg-ink-200); + background: var(--bg-ink-400); + } + } + } + } + + .show-all-filter-props:hover { + background: rgba(255, 255, 255, 0.04) !important; + } + + .example-queries { + cursor: default; + .heading { + padding: 12px 14px 8px 14px; + color: var(--bg-slate-50); + font-family: Inter; + font-size: 11px; + font-style: normal; + font-weight: 500; + line-height: 18px; /* 163.636% */ + letter-spacing: 0.88px; + text-transform: uppercase; + } + + .query-container { + display: flex; + flex-direction: column; + gap: 12px; + padding: 0px 12px 12px 12px; + cursor: pointer; + + .example-query { + display: flex; + padding: 4px 8px; + justify-content: center; + align-items: center; + gap: 10px; + border-radius: 2px; + background: var(--bg-ink-200); + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: -0.07px; + width: fit-content; + } + + .example-query:hover { + color: var(--bg-vanilla-100); + } + } + } + + .ant-select-item-option-grouped { + padding-inline-start: 0px; + padding: 7px 13px; + } + + .keyboard-shortcuts { + display: flex; + align-items: center; + border-radius: 0px 0px 4px 4px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); + padding: 11px 16px; + cursor: default; + + .icons { + width: 16px; + height: 16px; + flex-shrink: 0; + border-radius: 2.286px; + border-top: 1.143px solid var(--Ink-200, #23262e); + border-right: 1.143px solid var(--Ink-200, #23262e); + border-bottom: 2.286px solid var(--Ink-200, #23262e); + border-left: 1.143px solid var(--Ink-200, #23262e); + background: var(--Ink-400, #121317); + } + + .keyboard-text { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 142.857% */ + letter-spacing: -0.07px; + } + + .navigate { + display: flex; + align-items: center; + padding-right: 12px; + gap: 4px; + border-right: 1px solid #1d212d; + } + + .update-query { + display: flex; + align-items: center; + margin-left: 12px; + gap: 4px; + } + } + + .without-option-type { + display: flex; + gap: 8px; + align-items: center; + .dot { + height: 5px; + width: 5px; + border-radius: 50%; + background-color: var(--bg-slate-300); + } + } + + .logs-options-select { + display: flex; + align-items: center; + justify-content: space-between; + + .text { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + } + + .tags { + display: flex; + height: 20px; + padding: 4px 8px; + justify-content: center; + align-items: center; + gap: 4px; + border-radius: 20px; + } + + .dot { + height: 5px; + width: 5px; + border-radius: 50%; + flex-shrink: 0; + } + + .left-section { + display: flex; + align-items: center; + gap: 8px; + width: 90%; + + .dot { + background-color: var(--bg-slate-300); + } + + .value { + width: 100%; + } + } + + .right-section { + display: flex; + align-items: center; + gap: 4px; + + .data-type-tag { + background: rgba(255, 255, 255, 0.08); + } + + .option-type-tag { + display: flex; + gap: 4px; + align-items: center; + padding: 0px 6px; + text-transform: capitalize; + } + + .tag { + border-radius: 50px; + background: rgba(189, 153, 121, 0.1); + color: var(--bg-sienna-400); + + .dot { + background-color: var(--bg-sienna-400); + } + } + + .resource { + border-radius: 50px; + background: rgba(245, 108, 135, 0.1); + color: var(--bg-sakura-400); + + .dot { + background-color: var(--bg-sakura-400); + } + } + } + } + + .ant-select-item-option-active { + .logs-options-select { + .left-section { + .value { + color: var(--bg-vanilla-100); + } + } + } + } +} + .lightMode { .query-builder-search { .ant-select-dropdown { @@ -21,4 +305,108 @@ background-color: var(--bg-vanilla-200) !important; } } + .logs-explorer-popup { + .ant-select-item-group { + color: var(--bg-slate-50); + } + + .show-all-filter-props { + .content { + .left-section { + .text { + color: var(--bg-ink-400); + } + + .text:hover { + color: var(--bg-slate-100); + } + } + .right-section { + .keyboard-shortcut-slash { + border-top: 1.143px solid var(--bg-ink-200); + border-right: 1.143px solid var(--bg-ink-200); + border-bottom: 2.286px solid var(--bg-ink-200); + border-left: 1.143px solid var(--bg-ink-200); + background: var(--bg-vanilla-200); + } + } + } + } + + .show-all-filter-props:hover { + background: var(--bg-vanilla-200) !important; + } + + .example-queries { + .heading { + color: var(--bg-slate-50); + } + + .query-container { + .example-query-container { + .example-query { + background: var(--bg-vanilla-200); + color: var(--bg-ink-400); + } + + .example-query:hover { + color: var(--bg-ink-400); + } + } + } + } + + .keyboard-shortcuts { + border: 1px solid var(--bg-vanilla-400); + background: var(--bg-vanilla-200); + + .icons { + border-top: 1.143px solid var(--Ink-200, #23262e); + border-right: 1.143px solid var(--Ink-200, #23262e); + border-bottom: 2.286px solid var(--Ink-200, #23262e); + border-left: 1.143px solid var(--Ink-200, #23262e); + background: var(--bg-vanilla-200); + } + + .keyboard-text { + color: var(--bg-ink-400); + } + + .navigate { + border-right: 1px solid #1d212d; + } + } + + .logs-options-select { + .text { + color: var(--bg-ink-400); + } + + .right-section { + .data-type-tag { + background: var(--bg-vanilla-200); + } + + .tag { + background: rgba(189, 153, 121, 0.1); + color: var(--bg-sienna-400); + } + + .resource { + background: rgba(245, 108, 135, 0.1); + color: var(--bg-sakura-400); + } + } + } + + .ant-select-item-option-active { + .logs-options-select { + .left-section { + .value { + color: var(--bg-ink-100); + } + } + } + } + } } diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/index.tsx b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/index.tsx index eaaccb607d..c1f4b85a11 100644 --- a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/index.tsx +++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/index.tsx @@ -1,7 +1,10 @@ +/* eslint-disable react/no-unstable-nested-components */ import './QueryBuilderSearch.styles.scss'; -import { Select, Spin, Tag, Tooltip } from 'antd'; +import { Button, Select, Spin, Tag, Tooltip, Typography } from 'antd'; +import cx from 'classnames'; import { OPERATORS } from 'constants/queryBuilder'; +import ROUTES from 'constants/routes'; import { LogsExplorerShortcuts } from 'constants/shortcuts/logsExplorerShortcuts'; import { getDataTypes } from 'container/LogDetailedView/utils'; import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys'; @@ -11,7 +14,17 @@ import { } from 'hooks/queryBuilder/useAutoComplete'; import { useFetchKeysAndValues } from 'hooks/queryBuilder/useFetchKeysAndValues'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; -import { isEqual } from 'lodash-es'; +import { isEqual, isUndefined } from 'lodash-es'; +import { + ArrowDown, + ArrowUp, + ChevronDown, + ChevronUp, + Command, + CornerDownLeft, + Filter, + Slash, +} from 'lucide-react'; import type { BaseSelectRef } from 'rc-select'; import { KeyboardEvent, @@ -23,6 +36,7 @@ import { useRef, useState, } from 'react'; +import { useLocation } from 'react-router-dom'; import { BaseAutocompleteData, DataTypes, @@ -32,14 +46,18 @@ import { TagFilter, } from 'types/api/queryBuilder/queryBuilderData'; import { DataSource } from 'types/common/queryBuilder'; +import { getUserOperatingSystem, UserOperatingSystem } from 'utils/getUserOS'; import { popupContainer } from 'utils/selectPopupContainer'; import { v4 as uuid } from 'uuid'; import { selectStyle } from './config'; import { PLACEHOLDER } from './constant'; +import ExampleQueriesRendererForLogs from './ExampleQueriesRendererForLogs'; import OptionRenderer from './OptionRenderer'; +import OptionRendererForLogs from './OptionRendererForLogs'; import { StyledCheckOutlined, TypographyText } from './style'; import { + convertExampleQueriesToOptions, getOperatorValue, getRemovePrefixFromKey, getTagToken, @@ -55,6 +73,10 @@ function QueryBuilderSearch({ placeholder, suffixIcon, }: QueryBuilderSearchProps): JSX.Element { + const { pathname } = useLocation(); + const isLogsExplorerPage = useMemo(() => pathname === ROUTES.LOGS_EXPLORER, [ + pathname, + ]); const { updateTag, handleClearTag, @@ -69,14 +91,20 @@ function QueryBuilderSearch({ isFetching, setSearchKey, searchKey, - } = useAutoComplete(query, whereClauseConfig); - + key, + exampleQueries, + } = useAutoComplete(query, whereClauseConfig, isLogsExplorerPage); const [isOpen, setIsOpen] = useState(false); + const [showAllFilters, setShowAllFilters] = useState(false); + const [dynamicPlacholder, setDynamicPlaceholder] = useState( + placeholder || '', + ); const selectRef = useRef(null); const { sourceKeys, handleRemoveSourceKey } = useFetchKeysAndValues( searchValue, query, searchKey, + isLogsExplorerPage, ); const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys(); @@ -140,6 +168,12 @@ function QueryBuilderSearch({ handleRunQuery(); setIsOpen(false); } + + if ((event.ctrlKey || event.metaKey) && event.key === '/') { + event.preventDefault(); + event.stopPropagation(); + setShowAllFilters((prev) => !prev); + } }; const handleDeselect = useCallback( @@ -229,6 +263,28 @@ function QueryBuilderSearch({ deregisterShortcut(LogsExplorerShortcuts.FocusTheSearchBar); }, [deregisterShortcut, isLastQuery, registerShortcut]); + useEffect(() => { + if (!isOpen) { + setDynamicPlaceholder(placeholder || ''); + } + }, [isOpen, placeholder]); + + const userOs = getUserOperatingSystem(); + + // conditional changes here to use a seperate component to render the example queries based on the option group label + const customRendererForLogsExplorer = options.map((option) => ( + + + {option.selected && } + + )); + return (
3 && !key ? 'hide-scroll' : '', + )} rootClassName="query-builder-search" disabled={isMetricsDataSource && !query.aggregateAttribute.key} style={selectStyle} @@ -259,20 +321,99 @@ function QueryBuilderSearch({ onDeselect={handleDeselect} onInputKeyDown={onInputKeyDownHandler} notFoundContent={isFetching ? : null} - suffixIcon={suffixIcon} + suffixIcon={ + // eslint-disable-next-line no-nested-ternary + !isUndefined(suffixIcon) ? ( + suffixIcon + ) : isOpen ? ( + + ) : ( + + ) + } showAction={['focus']} onBlur={handleOnBlur} + popupClassName={isLogsExplorerPage ? 'logs-explorer-popup' : ''} + dropdownRender={(menu): ReactElement => ( +
+ {!searchKey && isLogsExplorerPage && ( +
Suggested Filters
+ )} + {menu} + {isLogsExplorerPage && ( +
+ {!searchKey && tags.length === 0 && ( +
+
Example Queries
+
+ {convertExampleQueriesToOptions(exampleQueries).map((query) => ( + + ))} +
+
+ )} + {!key && !isFetching && !showAllFilters && options.length > 3 && ( + + )} +
+
+ + + to navigate +
+
+ + to update query +
+
+
+ )} +
+ )} > - {options.map((option) => ( - - - {option.selected && } - - ))} + {isLogsExplorerPage + ? customRendererForLogsExplorer + : options.map((option) => ( + + + {option.selected && } + + ))}
); diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/utils.ts b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/utils.ts index ec7eba3973..04d8a0f77d 100644 --- a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/utils.ts +++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/utils.ts @@ -1,6 +1,8 @@ import { OPERATORS } from 'constants/queryBuilder'; import { MetricsType } from 'container/MetricsApplication/constant'; +import { queryFilterTags } from 'hooks/queryBuilder/useTag'; import { parse } from 'papaparse'; +import { TagFilter } from 'types/api/queryBuilder/queryBuilderData'; import { orderByValueDelimiter } from '../OrderByFilter/utils'; @@ -162,3 +164,17 @@ export function getOptionType(label: string): MetricsType | undefined { return optionType; } + +/** + * + * @param exampleQueries the example queries based on recommendation engine + * @returns the data formatted to the Option[] + */ +export function convertExampleQueriesToOptions( + exampleQueries: TagFilter[], +): { label: string; value: TagFilter }[] { + return exampleQueries.map((query) => ({ + value: query, + label: queryFilterTags(query).join(' , '), + })); +} diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchDropdown.tsx b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchDropdown.tsx new file mode 100644 index 0000000000..c910ca1be4 --- /dev/null +++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchDropdown.tsx @@ -0,0 +1,112 @@ +/* eslint-disable no-nested-ternary */ +import { Typography } from 'antd'; +import { + ArrowDown, + ArrowUp, + ChevronUp, + Command, + CornerDownLeft, + Slash, +} from 'lucide-react'; +import { TagFilter } from 'types/api/queryBuilder/queryBuilderData'; +import { getUserOperatingSystem, UserOperatingSystem } from 'utils/getUserOS'; + +import ExampleQueriesRendererForLogs from '../QueryBuilderSearch/ExampleQueriesRendererForLogs'; +import { convertExampleQueriesToOptions } from '../QueryBuilderSearch/utils'; +import { ITag, Option } from './QueryBuilderSearchV2'; + +interface ICustomDropdownProps { + menu: React.ReactElement; + searchValue: string; + tags: ITag[]; + options: Option[]; + exampleQueries: TagFilter[]; + onChange: (value: TagFilter) => void; + currentFilterItem?: ITag; +} + +export default function QueryBuilderSearchDropdown( + props: ICustomDropdownProps, +): React.ReactElement { + const { + menu, + currentFilterItem, + searchValue, + tags, + exampleQueries, + options, + onChange, + } = props; + const userOs = getUserOperatingSystem(); + return ( + <> +
+ {!currentFilterItem?.key ? ( +
Suggested Filters
+ ) : !currentFilterItem?.op ? ( +
+ + Operator for{' '} + + + {currentFilterItem?.key?.key} + +
+ ) : ( +
+ + Value(s) for{' '} + + + {currentFilterItem?.key?.key} {currentFilterItem?.op} + +
+ )} + {menu} + {!searchValue && tags.length === 0 && ( +
+
Example Queries
+
+ {convertExampleQueriesToOptions(exampleQueries).map((query) => ( + + ))} +
+
+ )} +
+ +
+
+ + + to navigate +
+
+ + to update query +
+ {!currentFilterItem?.key && options.length > 3 && ( +
+ {userOs === UserOperatingSystem.MACOS ? ( + + ) : ( + + )} + + + + Show all filter items +
+ )} +
+ + ); +} + +QueryBuilderSearchDropdown.defaultProps = { + currentFilterItem: undefined, +}; diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.styles.scss b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.styles.scss new file mode 100644 index 0000000000..1ca8bd7529 --- /dev/null +++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.styles.scss @@ -0,0 +1,261 @@ +.query-builder-search-v2 { + display: flex; + gap: 4px; + + .show-all-filters { + .content { + .rc-virtual-list-holder { + height: 100px; + } + } + } + + .content { + .suggested-filters { + color: var(--bg-slate-50); + font-family: Inter; + font-size: 11px; + font-style: normal; + font-weight: 600; + line-height: 18px; + letter-spacing: 0.88px; + text-transform: uppercase; + padding: 12px 0px 8px 14px; + } + + .operator-for { + display: flex; + align-items: center; + gap: 6px; + padding: 12px 0px 8px 14px; + + .operator-for-text { + color: var(--bg-slate-200); + font-family: Inter; + font-size: 11px; + font-style: normal; + font-weight: 600; + line-height: 18px; /* 163.636% */ + letter-spacing: 0.88px; + text-transform: uppercase; + } + + .operator-for-value { + display: flex; + align-items: center; + height: 20px; + padding: 0px 8px; + justify-content: center; + gap: 4px; + border-radius: 50px; + background: rgba(255, 255, 255, 0.1); + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 150% */ + letter-spacing: -0.06px; + } + } + + .value-for { + display: flex; + align-items: center; + gap: 6px; + padding: 12px 0px 8px 14px; + .value-for-text { + color: var(--bg-slate-200); + font-family: Inter; + font-size: 11px; + font-style: normal; + font-weight: 600; + line-height: 18px; /* 163.636% */ + letter-spacing: 0.88px; + text-transform: uppercase; + } + + .value-for-value { + display: flex; + align-items: center; + height: 20px; + padding: 0px 8px; + justify-content: center; + gap: 4px; + border-radius: 50px; + background: rgba(255, 255, 255, 0.1); + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 150% */ + letter-spacing: -0.06px; + } + } + .example-queries { + cursor: default; + .heading { + padding: 12px 14px 8px 14px; + color: var(--bg-slate-50); + font-family: Inter; + font-size: 11px; + font-style: normal; + font-weight: 500; + line-height: 18px; /* 163.636% */ + letter-spacing: 0.88px; + text-transform: uppercase; + } + + .query-container { + display: flex; + flex-direction: column; + gap: 12px; + padding: 0px 12px 12px 12px; + cursor: pointer; + + .example-query { + display: flex; + padding: 4px 8px; + justify-content: center; + align-items: center; + gap: 10px; + border-radius: 2px; + background: var(--bg-ink-200); + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: -0.07px; + width: fit-content; + } + + .example-query:hover { + color: var(--bg-vanilla-100); + } + } + } + } + + .keyboard-shortcuts { + display: flex; + align-items: center; + border-radius: 0px 0px 4px 4px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); + padding: 11px 16px; + cursor: default; + + .icons { + width: 16px; + height: 16px; + flex-shrink: 0; + border-radius: 2.286px; + border-top: 1.143px solid var(--bg-ink-200); + border-right: 1.143px solid var(--bg-ink-200); + border-bottom: 2.286px solid var(--bg-ink-200); + border-left: 1.143px solid var(--bg-ink-200); + background: var(--Ink-400, #121317); + } + + .keyboard-text { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 142.857% */ + letter-spacing: -0.07px; + } + + .navigate { + display: flex; + align-items: center; + padding-right: 12px; + gap: 4px; + border-right: 1px solid #1d212d; + } + + .update-query { + display: flex; + align-items: center; + margin-left: 12px; + gap: 4px; + } + .show-all-filter-items { + padding-left: 12px; + border-left: 1px solid #1d212d; + display: flex; + align-items: center; + margin-left: 12px; + gap: 4px; + } + } + + .search-bar { + width: 100%; + } + + .qb-search-bar-tokenised-tags { + .ant-tag { + display: flex; + align-items: center; + border-radius: 2px 0px 0px 2px; + border: 1px solid var(--bg-slate-300); + background: var(--bg-slate-300); + box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1); + padding: 0px; + + .ant-typography { + color: var(--bg-vanilla-100); + font-family: Inter; + font-size: 14px !important; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + padding: 2px 6px; + } + + .ant-tag-close-icon { + display: flex; + align-items: center; + justify-content: center; + border-radius: 0px 2px 2px 0px; + width: 20px; + height: 24px; + flex-shrink: 0; + margin-inline-start: 0px !important; + margin-inline-end: 0px !important; + } + + &.resource { + border: 1px solid rgba(242, 71, 105, 0.2); + + .ant-typography { + color: var(--bg-sakura-400); + background: rgba(245, 108, 135, 0.1); + font-size: 14px; + } + + .ant-tag-close-icon { + background: rgba(245, 108, 135, 0.1); + } + } + &.tag { + border: 1px solid rgba(189, 153, 121, 0.2); + + .ant-typography { + color: var(--bg-sienna-400); + background: rgba(189, 153, 121, 0.1); + font-size: 14px; + } + + .ant-tag-close-icon { + background: rgba(189, 153, 121, 0.1); + } + } + } + } +} diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.tsx b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.tsx new file mode 100644 index 0000000000..d53a517c48 --- /dev/null +++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.tsx @@ -0,0 +1,862 @@ +/* eslint-disable sonarjs/cognitive-complexity */ +import './QueryBuilderSearchV2.styles.scss'; + +import { Select, Spin, Tag, Tooltip } from 'antd'; +import cx from 'classnames'; +import { + OPERATORS, + QUERY_BUILDER_OPERATORS_BY_TYPES, + QUERY_BUILDER_SEARCH_VALUES, +} from 'constants/queryBuilder'; +import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig'; +import ROUTES from 'constants/routes'; +import { LogsExplorerShortcuts } from 'constants/shortcuts/logsExplorerShortcuts'; +import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys'; +import { WhereClauseConfig } from 'hooks/queryBuilder/useAutoComplete'; +import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys'; +import { useGetAggregateValues } from 'hooks/queryBuilder/useGetAggregateValues'; +import { useGetAttributeSuggestions } from 'hooks/queryBuilder/useGetAttributeSuggestions'; +import { validationMapper } from 'hooks/queryBuilder/useIsValidTag'; +import { operatorTypeMapper } from 'hooks/queryBuilder/useOperatorType'; +import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; +import useDebounceValue from 'hooks/useDebounce'; +import { + cloneDeep, + isArray, + isEmpty, + isEqual, + isObject, + isUndefined, + unset, +} from 'lodash-es'; +import { ChevronDown, ChevronUp } from 'lucide-react'; +import type { BaseSelectRef } from 'rc-select'; +import { + KeyboardEvent, + ReactElement, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import { useLocation } from 'react-router-dom'; +import { + BaseAutocompleteData, + DataTypes, +} from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { + IBuilderQuery, + TagFilter, +} from 'types/api/queryBuilder/queryBuilderData'; +import { DataSource } from 'types/common/queryBuilder'; +import { popupContainer } from 'utils/selectPopupContainer'; +import { v4 as uuid } from 'uuid'; + +import { selectStyle } from '../QueryBuilderSearch/config'; +import { PLACEHOLDER } from '../QueryBuilderSearch/constant'; +import { TypographyText } from '../QueryBuilderSearch/style'; +import { getTagToken, isInNInOperator } from '../QueryBuilderSearch/utils'; +import QueryBuilderSearchDropdown from './QueryBuilderSearchDropdown'; +import Suggestions from './Suggestions'; + +export interface ITag { + id?: string; + key: BaseAutocompleteData; + op: string; + value: string[] | string | number | boolean; +} + +interface CustomTagProps { + label: React.ReactNode; + value: string; + disabled: boolean; + onClose: () => void; + closable: boolean; +} + +interface QueryBuilderSearchV2Props { + query: IBuilderQuery; + onChange: (value: TagFilter) => void; + whereClauseConfig?: WhereClauseConfig; + placeholder?: string; + className?: string; + suffixIcon?: React.ReactNode; +} + +export interface Option { + label: string; + value: BaseAutocompleteData | string; +} + +export enum DropdownState { + ATTRIBUTE_KEY = 'ATTRIBUTE_KEY', + OPERATOR = 'OPERATOR', + ATTRIBUTE_VALUE = 'ATTRIBUTE_VALUE', +} + +function getInitTags(query: IBuilderQuery): ITag[] { + return query.filters.items.map((item) => ({ + id: item.id, + key: item.key as BaseAutocompleteData, + op: item.op, + value: `${item.value}`, + })); +} + +function QueryBuilderSearchV2( + props: QueryBuilderSearchV2Props, +): React.ReactElement { + const { + query, + onChange, + placeholder, + className, + suffixIcon, + whereClauseConfig, + } = props; + + const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys(); + + const { handleRunQuery, currentQuery } = useQueryBuilder(); + + const selectRef = useRef(null); + + const [isOpen, setIsOpen] = useState(false); + + // create the tags from the initial query here, this should only be computed on the first load as post that tags and query will be always in sync. + const [tags, setTags] = useState(() => getInitTags(query)); + + // this will maintain the current state of in process filter item + const [currentFilterItem, setCurrentFilterItem] = useState(); + + const [currentState, setCurrentState] = useState( + DropdownState.ATTRIBUTE_KEY, + ); + + // to maintain the current running state until the tokenization happens for the tag + const [searchValue, setSearchValue] = useState(''); + + const [dropdownOptions, setDropdownOptions] = useState([]); + + const [showAllFilters, setShowAllFilters] = useState(false); + + const { pathname } = useLocation(); + const isLogsExplorerPage = useMemo(() => pathname === ROUTES.LOGS_EXPLORER, [ + pathname, + ]); + + const memoizedSearchParams = useMemo( + () => [ + searchValue, + query.dataSource, + query.aggregateOperator, + query.aggregateAttribute.key, + ], + [ + searchValue, + query.dataSource, + query.aggregateOperator, + query.aggregateAttribute.key, + ], + ); + + const queryFiltersWithoutId = useMemo( + () => ({ + ...query.filters, + items: query.filters.items.map((item) => { + const filterWithoutId = cloneDeep(item); + unset(filterWithoutId, 'id'); + return filterWithoutId; + }), + }), + [query.filters], + ); + + const memoizedSuggestionsParams = useMemo( + () => [searchValue, query.dataSource, queryFiltersWithoutId], + [query.dataSource, queryFiltersWithoutId, searchValue], + ); + + const memoizedValueParams = useMemo( + () => [ + query.aggregateOperator, + query.dataSource, + query.aggregateAttribute.key, + currentFilterItem?.key?.key || '', + currentFilterItem?.key?.dataType, + currentFilterItem?.key?.type ?? '', + isArray(currentFilterItem?.value) + ? currentFilterItem?.value?.[currentFilterItem.value.length - 1] + : currentFilterItem?.value, + ], + [ + query.aggregateOperator, + query.dataSource, + query.aggregateAttribute.key, + currentFilterItem?.key?.key, + currentFilterItem?.key?.dataType, + currentFilterItem?.key?.type, + currentFilterItem?.value, + ], + ); + + const searchParams = useDebounceValue(memoizedSearchParams, DEBOUNCE_DELAY); + + const valueParams = useDebounceValue(memoizedValueParams, DEBOUNCE_DELAY); + + const suggestionsParams = useDebounceValue( + memoizedSuggestionsParams, + DEBOUNCE_DELAY, + ); + + const isQueryEnabled = useMemo(() => { + if (currentState === DropdownState.ATTRIBUTE_KEY) { + return query.dataSource === DataSource.METRICS + ? !!query.aggregateOperator && + !!query.dataSource && + !!query.aggregateAttribute.dataType + : true; + } + return false; + }, [ + currentState, + query.aggregateAttribute.dataType, + query.aggregateOperator, + query.dataSource, + ]); + + const { data, isFetching } = useGetAggregateKeys( + { + searchText: searchValue, + dataSource: query.dataSource, + aggregateOperator: query.aggregateOperator, + aggregateAttribute: query.aggregateAttribute.key, + tagType: query.aggregateAttribute.type ?? null, + }, + { + queryKey: [searchParams], + enabled: isQueryEnabled && !isLogsExplorerPage, + }, + ); + + const { + data: suggestionsData, + isFetching: isFetchingSuggestions, + } = useGetAttributeSuggestions( + { + searchText: searchValue.split(' ')[0], + dataSource: query.dataSource, + filters: query.filters, + }, + { + queryKey: [suggestionsParams], + enabled: isQueryEnabled && isLogsExplorerPage, + }, + ); + + const { + data: attributeValues, + isFetching: isFetchingAttributeValues, + } = useGetAggregateValues( + { + aggregateOperator: query.aggregateOperator, + dataSource: query.dataSource, + aggregateAttribute: query.aggregateAttribute.key, + attributeKey: currentFilterItem?.key?.key || '', + filterAttributeKeyDataType: + currentFilterItem?.key?.dataType ?? DataTypes.EMPTY, + tagType: currentFilterItem?.key?.type ?? '', + searchText: isArray(currentFilterItem?.value) + ? currentFilterItem?.value?.[currentFilterItem.value.length - 1] || '' + : currentFilterItem?.value?.toString() || '', + }, + { + enabled: currentState === DropdownState.ATTRIBUTE_VALUE, + queryKey: [valueParams], + }, + ); + + const handleDropdownSelect = useCallback( + (value: string) => { + let parsedValue: BaseAutocompleteData | string; + + try { + parsedValue = JSON.parse(value); + } catch { + parsedValue = value; + } + if (currentState === DropdownState.ATTRIBUTE_KEY) { + setCurrentFilterItem((prev) => ({ + ...prev, + key: parsedValue as BaseAutocompleteData, + op: '', + value: '', + })); + setCurrentState(DropdownState.OPERATOR); + setSearchValue((parsedValue as BaseAutocompleteData)?.key); + } else if (currentState === DropdownState.OPERATOR) { + if (value === OPERATORS.EXISTS || value === OPERATORS.NOT_EXISTS) { + setTags((prev) => [ + ...prev, + { + key: currentFilterItem?.key, + op: value, + value: '', + } as ITag, + ]); + setCurrentFilterItem(undefined); + setSearchValue(''); + setCurrentState(DropdownState.ATTRIBUTE_KEY); + } else { + setCurrentFilterItem((prev) => ({ + key: prev?.key as BaseAutocompleteData, + op: value as string, + value: '', + })); + setCurrentState(DropdownState.ATTRIBUTE_VALUE); + setSearchValue(`${currentFilterItem?.key?.key} ${value}`); + } + } else if (currentState === DropdownState.ATTRIBUTE_VALUE) { + const operatorType = + operatorTypeMapper[currentFilterItem?.op || ''] || 'NOT_VALID'; + const isMulti = operatorType === QUERY_BUILDER_SEARCH_VALUES.MULTIPLY; + + if (isMulti) { + const { tagKey, tagOperator, tagValue } = getTagToken(searchValue); + const newSearch = [...tagValue]; + newSearch[newSearch.length === 0 ? 0 : newSearch.length - 1] = value; + const newSearchValue = newSearch.join(','); + setSearchValue(`${tagKey} ${tagOperator} ${newSearchValue},`); + } else { + setSearchValue(''); + setCurrentState(DropdownState.ATTRIBUTE_KEY); + setCurrentFilterItem(undefined); + setTags((prev) => [ + ...prev, + { + key: currentFilterItem?.key, + op: currentFilterItem?.op, + value, + } as ITag, + ]); + } + } + }, + [currentFilterItem?.key, currentFilterItem?.op, currentState, searchValue], + ); + + const handleSearch = useCallback((value: string) => { + setSearchValue(value); + }, []); + + const onInputKeyDownHandler = useCallback( + (event: KeyboardEvent): void => { + if (event.key === 'Backspace' && !searchValue) { + event.stopPropagation(); + setTags((prev) => prev.slice(0, -1)); + } + if ((event.ctrlKey || event.metaKey) && event.key === '/') { + event.preventDefault(); + event.stopPropagation(); + setShowAllFilters((prev) => !prev); + } + if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') { + event.preventDefault(); + event.stopPropagation(); + handleRunQuery(); + setIsOpen(false); + } + }, + [handleRunQuery, searchValue], + ); + + const handleOnBlur = useCallback((): void => { + if (searchValue) { + const operatorType = + operatorTypeMapper[currentFilterItem?.op || ''] || 'NOT_VALID'; + if ( + currentFilterItem?.key && + isEmpty(currentFilterItem?.op) && + whereClauseConfig?.customKey === 'body' && + whereClauseConfig?.customOp === OPERATORS.CONTAINS + ) { + setTags((prev) => [ + ...prev, + { + key: { + key: 'body', + dataType: DataTypes.String, + type: '', + isColumn: true, + isJSON: false, + id: 'body--string----true', + }, + op: OPERATORS.CONTAINS, + value: currentFilterItem?.key?.key, + }, + ]); + setCurrentFilterItem(undefined); + setSearchValue(''); + setCurrentState(DropdownState.ATTRIBUTE_KEY); + } else if ( + currentFilterItem?.op === OPERATORS.EXISTS || + currentFilterItem?.op === OPERATORS.NOT_EXISTS + ) { + setTags((prev) => [ + ...prev, + { + key: currentFilterItem?.key, + op: currentFilterItem?.op, + value: '', + }, + ]); + setCurrentFilterItem(undefined); + setSearchValue(''); + setCurrentState(DropdownState.ATTRIBUTE_KEY); + } else if ( + validationMapper[operatorType]?.( + isArray(currentFilterItem?.value) + ? currentFilterItem?.value.length || 0 + : 1, + ) + ) { + setTags((prev) => [ + ...prev, + { + key: currentFilterItem?.key as BaseAutocompleteData, + op: currentFilterItem?.op as string, + value: currentFilterItem?.value || '', + }, + ]); + setCurrentFilterItem(undefined); + setSearchValue(''); + setCurrentState(DropdownState.ATTRIBUTE_KEY); + } + } + }, [ + currentFilterItem?.key, + currentFilterItem?.op, + currentFilterItem?.value, + searchValue, + whereClauseConfig?.customKey, + whereClauseConfig?.customOp, + ]); + + // this useEffect takes care of tokenisation based on the search state + useEffect(() => { + if (isFetchingSuggestions) { + return; + } + if (!searchValue) { + setCurrentFilterItem(undefined); + setCurrentState(DropdownState.ATTRIBUTE_KEY); + } + const { tagKey, tagOperator, tagValue } = getTagToken(searchValue); + + if (tagKey && isUndefined(currentFilterItem?.key)) { + let currentRunningAttributeKey; + const isSuggestedKeyInAutocomplete = suggestionsData?.payload?.attributes?.some( + (value) => value.key === tagKey.split(' ')[0], + ); + + if (isSuggestedKeyInAutocomplete) { + const allAttributesMatchingTheKey = + suggestionsData?.payload?.attributes?.filter( + (value) => value.key === tagKey.split(' ')[0], + ) || []; + + if (allAttributesMatchingTheKey?.length === 1) { + [currentRunningAttributeKey] = allAttributesMatchingTheKey; + } + if (allAttributesMatchingTheKey?.length > 1) { + // the priority logic goes here + [currentRunningAttributeKey] = allAttributesMatchingTheKey; + } + + if (currentRunningAttributeKey) { + setCurrentFilterItem({ + key: currentRunningAttributeKey, + op: '', + value: '', + }); + + setCurrentState(DropdownState.OPERATOR); + } + } + if (suggestionsData?.payload?.attributes?.length === 0) { + setCurrentFilterItem({ + key: { + key: tagKey.split(' ')[0], + // update this for has and nhas operator , check the useEffect of source keys in older component for details + dataType: DataTypes.EMPTY, + type: '', + isColumn: false, + isJSON: false, + }, + op: '', + value: '', + }); + setCurrentState(DropdownState.OPERATOR); + } + } else if ( + currentFilterItem?.key && + currentFilterItem?.key?.key !== tagKey.split(' ')[0] + ) { + setCurrentFilterItem(undefined); + setCurrentState(DropdownState.ATTRIBUTE_KEY); + } else if (tagOperator && isEmpty(currentFilterItem?.op)) { + if ( + tagOperator === OPERATORS.EXISTS || + tagOperator === OPERATORS.NOT_EXISTS + ) { + setTags((prev) => [ + ...prev, + { + key: currentFilterItem?.key, + op: tagOperator, + value: '', + } as ITag, + ]); + setCurrentFilterItem(undefined); + setSearchValue(''); + setCurrentState(DropdownState.ATTRIBUTE_KEY); + } else { + setCurrentFilterItem((prev) => ({ + key: prev?.key as BaseAutocompleteData, + op: tagOperator, + value: '', + })); + + setCurrentState(DropdownState.ATTRIBUTE_VALUE); + } + } else if ( + !isEmpty(currentFilterItem?.op) && + tagOperator !== currentFilterItem?.op + ) { + setCurrentFilterItem((prev) => ({ + key: prev?.key as BaseAutocompleteData, + op: '', + value: '', + })); + setCurrentState(DropdownState.OPERATOR); + } else if (!isEmpty(tagValue)) { + const currentValue = { + key: currentFilterItem?.key as BaseAutocompleteData, + operator: currentFilterItem?.op as string, + value: tagValue, + }; + if (!isEqual(currentValue, currentFilterItem)) { + setCurrentFilterItem((prev) => ({ + key: prev?.key as BaseAutocompleteData, + op: prev?.op as string, + value: tagValue, + })); + } + } + }, [ + currentFilterItem, + currentFilterItem?.key, + currentFilterItem?.op, + suggestionsData?.payload?.attributes, + searchValue, + isFetchingSuggestions, + ]); + + // the useEffect takes care of setting the dropdown values correctly on change of the current state + useEffect(() => { + if (currentState === DropdownState.ATTRIBUTE_KEY) { + if (isLogsExplorerPage) { + setDropdownOptions( + suggestionsData?.payload?.attributes?.map((key) => ({ + label: key.key, + value: key, + })) || [], + ); + } else { + setDropdownOptions( + data?.payload?.attributeKeys?.map((key) => ({ + label: key.key, + value: key, + })) || [], + ); + } + } + if (currentState === DropdownState.OPERATOR) { + const keyOperator = searchValue.split(' '); + const partialOperator = keyOperator?.[1]; + const strippedKey = keyOperator?.[0]; + + let operatorOptions; + if (currentFilterItem?.key?.dataType) { + operatorOptions = QUERY_BUILDER_OPERATORS_BY_TYPES[ + currentFilterItem.key + .dataType as keyof typeof QUERY_BUILDER_OPERATORS_BY_TYPES + ].map((operator) => ({ + label: operator, + value: operator, + })); + + if (partialOperator) { + operatorOptions = operatorOptions.filter((op) => + op.label.startsWith(partialOperator.toLocaleUpperCase()), + ); + } + setDropdownOptions(operatorOptions); + } else if (strippedKey.endsWith('[*]') && strippedKey.startsWith('body.')) { + operatorOptions = [OPERATORS.HAS, OPERATORS.NHAS].map((operator) => ({ + label: operator, + value: operator, + })); + setDropdownOptions(operatorOptions); + } else { + operatorOptions = QUERY_BUILDER_OPERATORS_BY_TYPES.universal.map( + (operator) => ({ + label: operator, + value: operator, + }), + ); + + if (partialOperator) { + operatorOptions = operatorOptions.filter((op) => + op.label.startsWith(partialOperator.toLocaleUpperCase()), + ); + } + setDropdownOptions(operatorOptions); + } + } + + if (currentState === DropdownState.ATTRIBUTE_VALUE) { + const values: string[] = + Object.values(attributeValues?.payload || {}).find((el) => !!el) || []; + + const { tagValue } = getTagToken(searchValue); + + if (values.length === 0) { + if (isArray(tagValue)) { + if (!isEmpty(tagValue[tagValue.length - 1])) + values.push(tagValue[tagValue.length - 1]); + } else if (!isEmpty(tagValue)) values.push(tagValue); + } + + setDropdownOptions( + values.map((val) => ({ + label: val, + value: val, + })), + ); + } + }, [ + attributeValues?.payload, + currentFilterItem?.key.dataType, + currentState, + data?.payload?.attributeKeys, + isLogsExplorerPage, + searchValue, + suggestionsData?.payload?.attributes, + ]); + + useEffect(() => { + const filterTags: IBuilderQuery['filters'] = { + op: 'AND', + items: [], + }; + tags.forEach((tag) => { + filterTags.items.push({ + id: tag.id || uuid().slice(0, 8), + key: tag.key, + op: tag.op, + value: tag.value, + }); + }); + + if (!isEqual(query.filters, filterTags)) { + onChange(filterTags); + setTags(filterTags.items as ITag[]); + } + }, [onChange, query.filters, tags]); + + const isLastQuery = useMemo( + () => + isEqual( + currentQuery.builder.queryData[currentQuery.builder.queryData.length - 1], + query, + ), + [currentQuery, query], + ); + + useEffect(() => { + if (isLastQuery) { + registerShortcut(LogsExplorerShortcuts.FocusTheSearchBar, () => { + // set timeout is needed here else the select treats the hotkey as input value + setTimeout(() => { + selectRef.current?.focus(); + }, 0); + }); + } + + return (): void => + deregisterShortcut(LogsExplorerShortcuts.FocusTheSearchBar); + }, [deregisterShortcut, isLastQuery, registerShortcut]); + + const loading = useMemo( + () => isFetching || isFetchingAttributeValues || isFetchingSuggestions, + [isFetching, isFetchingAttributeValues, isFetchingSuggestions], + ); + + const isMetricsDataSource = useMemo( + () => query.dataSource === DataSource.METRICS, + [query.dataSource], + ); + + const queryTags = useMemo( + () => tags.map((tag) => `${tag.key.key} ${tag.op} ${tag.value}`), + [tags], + ); + + const onTagRender = ({ + value, + closable, + onClose, + }: CustomTagProps): React.ReactElement => { + const { tagOperator } = getTagToken(value); + const isInNin = isInNInOperator(tagOperator); + const chipValue = isInNin + ? value?.trim()?.replace(/,\s*$/, '') + : value?.trim(); + + const indexInQueryTags = queryTags.findIndex((qTag) => isEqual(qTag, value)); + const tagDetails = tags[indexInQueryTags]; + + const onCloseHandler = (): void => { + onClose(); + setSearchValue(''); + setTags((prev) => prev.filter((t) => !isEqual(t, tagDetails))); + }; + + const tagEditHandler = (value: string): void => { + setCurrentFilterItem(tagDetails); + setSearchValue(value); + setCurrentState(DropdownState.ATTRIBUTE_VALUE); + setTags((prev) => prev.filter((t) => !isEqual(t, tagDetails))); + }; + + const isDisabled = !!searchValue; + + return ( + + + + { + if (!isDisabled) tagEditHandler(value); + }} + > + {chipValue} + + + + + ); + }; + + return ( +
+ +
+ ); +} + +QueryBuilderSearchV2.defaultProps = { + placeholder: PLACEHOLDER, + className: '', + suffixIcon: null, + whereClauseConfig: {}, +}; + +export default QueryBuilderSearchV2; diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/Suggestions.styles.scss b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/Suggestions.styles.scss new file mode 100644 index 0000000000..362d6e4c6a --- /dev/null +++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/Suggestions.styles.scss @@ -0,0 +1,147 @@ +.text { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; + letter-spacing: -0.07px; +} + +.dot { + height: 5px; + width: 5px; + border-radius: 50%; + background-color: var(--bg-slate-300); +} + +.option { + .container { + display: flex; + align-items: center; + justify-content: space-between; + + .left-section { + display: flex; + align-items: center; + width: 90%; + gap: 8px; + + .value { + } + } + .right-section { + display: flex; + align-items: center; + gap: 4px; + + .data-type { + display: flex; + height: 20px; + padding: 4px 8px; + justify-content: center; + align-items: center; + gap: 4px; + border-radius: 20px; + background: rgba(255, 255, 255, 0.08); + } + + .type-tag { + display: flex; + align-items: center; + height: 20px; + padding: 0px 6px; + justify-content: center; + gap: 4px; + border-radius: 50px; + text-transform: capitalize; + + &.tag { + border-radius: 50px; + background: rgba(189, 153, 121, 0.1) !important; + color: var(--bg-sienna-400) !important; + + .dot { + background-color: var(--bg-sienna-400); + } + .text { + color: var(--bg-sienna-400); + font-family: Inter; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 150% */ + letter-spacing: -0.06px; + } + } + + &.resource { + border-radius: 50px; + background: rgba(245, 108, 135, 0.1) !important; + color: var(--bg-sakura-400) !important; + + .dot { + background-color: var(--bg-sakura-400); + } + .text { + color: var(--bg-sakura-400); + font-family: Inter; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 150% */ + letter-spacing: -0.06px; + } + } + } + } + .option-meta-data-container { + display: flex; + gap: 8px; + } + } + + .container-without-tag { + display: flex; + align-items: center; + gap: 8px; + + .OPERATOR { + color: var(--bg-vanilla-400); + font-family: 'Space Mono'; + font-size: 14px; + font-style: normal; + font-weight: 500; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + text-transform: uppercase; + width: 100%; + } + + .VALUE { + color: var(--bg-vanilla-400); + font-family: 'Space Mono'; + font-size: 14px; + font-style: normal; + font-weight: 500; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + text-transform: uppercase; + width: 100%; + } + } +} +.option:hover { + .container { + .left-section { + .value { + color: var(--bg-vanilla-100); + } + } + } + .container-without-tag { + .value { + color: var(--bg-vanilla-100); + } + } +} diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/Suggestions.tsx b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/Suggestions.tsx new file mode 100644 index 0000000000..49d6040b7b --- /dev/null +++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/Suggestions.tsx @@ -0,0 +1,75 @@ +import './Suggestions.styles.scss'; + +import { Color } from '@signozhq/design-tokens'; +import { Tooltip, Typography } from 'antd'; +import cx from 'classnames'; +import { isEmpty, isObject } from 'lodash-es'; +import { Zap } from 'lucide-react'; +import { useMemo, useState } from 'react'; +import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; + +import { DropdownState } from './QueryBuilderSearchV2'; + +interface ISuggestionsProps { + label: string; + value: BaseAutocompleteData | string; + option: DropdownState; +} + +function Suggestions(props: ISuggestionsProps): React.ReactElement { + const { label, value, option } = props; + + const optionType = useMemo(() => { + if (isObject(value)) { + return value.type; + } + return ''; + }, [value]); + + const [truncated, setTruncated] = useState(false); + + return ( +
+ {!isEmpty(optionType) && isObject(value) ? ( + +
+
+ {value.isIndexed ? ( + + ) : ( +
+ )} + setTruncated(ellipsis) }} + > + {label} + +
+
+ {value.dataType} +
+
+ {value.type} +
+
+
+
+ ) : ( + +
+
+ setTruncated(ellipsis) }} + > + {`${label}`} + +
+ + )} +
+ ); +} + +export default Suggestions; diff --git a/frontend/src/container/QueryBuilder/type.ts b/frontend/src/container/QueryBuilder/type.ts index 892330ebdd..183dd157f8 100644 --- a/frontend/src/container/QueryBuilder/type.ts +++ b/frontend/src/container/QueryBuilder/type.ts @@ -15,4 +15,5 @@ export type Option = { label: string; selected?: boolean; dataType?: string; + isIndexed?: boolean; }; diff --git a/frontend/src/container/SideNav/NavItem/NavItem.styles.scss b/frontend/src/container/SideNav/NavItem/NavItem.styles.scss index aea7b3a0ee..ede4376033 100644 --- a/frontend/src/container/SideNav/NavItem/NavItem.styles.scss +++ b/frontend/src/container/SideNav/NavItem/NavItem.styles.scss @@ -75,6 +75,10 @@ text-overflow: ellipsis; } } + + .beta-tag { + padding-right: 0; + } } .lightMode { diff --git a/frontend/src/container/SideNav/NavItem/NavItem.tsx b/frontend/src/container/SideNav/NavItem/NavItem.tsx index aa9b57707e..97a8cdb0ed 100644 --- a/frontend/src/container/SideNav/NavItem/NavItem.tsx +++ b/frontend/src/container/SideNav/NavItem/NavItem.tsx @@ -24,14 +24,16 @@ export default function NavItem({ onClick={(event): void => onClick(event)} >
-
+
{icon}
{label}
{isBeta && (
- Beta + + Beta +
)}
diff --git a/frontend/src/container/SideNav/SideNav.tsx b/frontend/src/container/SideNav/SideNav.tsx index d4ad27908c..1ba863d8ec 100644 --- a/frontend/src/container/SideNav/SideNav.tsx +++ b/frontend/src/container/SideNav/SideNav.tsx @@ -347,6 +347,10 @@ function SideNav({ onClickHandler(ROUTES.ALL_DASHBOARD, null), ); + registerShortcut(GlobalShortcuts.NavigateToMessagingQueues, () => + onClickHandler(ROUTES.MESSAGING_QUEUES, null), + ); + registerShortcut(GlobalShortcuts.NavigateToAlerts, () => onClickHandler(ROUTES.LIST_ALL_ALERT, null), ); @@ -362,6 +366,7 @@ function SideNav({ deregisterShortcut(GlobalShortcuts.NavigateToDashboards); deregisterShortcut(GlobalShortcuts.NavigateToAlerts); deregisterShortcut(GlobalShortcuts.NavigateToExceptions); + deregisterShortcut(GlobalShortcuts.NavigateToMessagingQueues); }; }, [deregisterShortcut, onClickHandler, onCollapse, registerShortcut]); diff --git a/frontend/src/container/SideNav/config.ts b/frontend/src/container/SideNav/config.ts index 95028b0a05..37e9db4d9f 100644 --- a/frontend/src/container/SideNav/config.ts +++ b/frontend/src/container/SideNav/config.ts @@ -48,4 +48,6 @@ export const routeConfig: Record = { [ROUTES.TRACE_EXPLORER]: [QueryParams.resourceAttributes], [ROUTES.LOGS_PIPELINES]: [QueryParams.resourceAttributes], [ROUTES.WORKSPACE_LOCKED]: [QueryParams.resourceAttributes], + [ROUTES.MESSAGING_QUEUES]: [QueryParams.resourceAttributes], + [ROUTES.MESSAGING_QUEUES_DETAIL]: [QueryParams.resourceAttributes], }; diff --git a/frontend/src/container/SideNav/menuItems.tsx b/frontend/src/container/SideNav/menuItems.tsx index 2039e4df8e..be694227a1 100644 --- a/frontend/src/container/SideNav/menuItems.tsx +++ b/frontend/src/container/SideNav/menuItems.tsx @@ -10,6 +10,7 @@ import { FileKey2, Layers2, LayoutGrid, + ListMinus, MessageSquare, Receipt, Route, @@ -86,6 +87,12 @@ const menuItems: SidebarItem[] = [ label: 'Dashboards', icon: , }, + { + key: ROUTES.MESSAGING_QUEUES, + label: 'Messaging Queues', + icon: , + isBeta: true, + }, { key: ROUTES.LIST_ALL_ALERT, label: 'Alerts', diff --git a/frontend/src/container/TopNav/Breadcrumbs/index.tsx b/frontend/src/container/TopNav/Breadcrumbs/index.tsx index 4ab1e945d4..9efd50d2c3 100644 --- a/frontend/src/container/TopNav/Breadcrumbs/index.tsx +++ b/frontend/src/container/TopNav/Breadcrumbs/index.tsx @@ -27,6 +27,7 @@ const breadcrumbNameMap: Record = { [ROUTES.BILLING]: 'Billing', [ROUTES.SUPPORT]: 'Support', [ROUTES.WORKSPACE_LOCKED]: 'Workspace Locked', + [ROUTES.MESSAGING_QUEUES]: 'Messaging Queues', }; function ShowBreadcrumbs(props: RouteComponentProps): JSX.Element { diff --git a/frontend/src/container/TopNav/DateTimeSelectionV2/config.ts b/frontend/src/container/TopNav/DateTimeSelectionV2/config.ts index 473107265e..b652a68202 100644 --- a/frontend/src/container/TopNav/DateTimeSelectionV2/config.ts +++ b/frontend/src/container/TopNav/DateTimeSelectionV2/config.ts @@ -208,6 +208,8 @@ export const routesToSkip = [ ROUTES.DASHBOARD, ROUTES.DASHBOARD_WIDGET, ROUTES.SERVICE_TOP_LEVEL_OPERATIONS, + ROUTES.MESSAGING_QUEUES, + ROUTES.MESSAGING_QUEUES_DETAIL, ]; export const routesToDisable = [ROUTES.LOGS_EXPLORER, ROUTES.LIVE_LOGS]; diff --git a/frontend/src/container/TopNav/DateTimeSelectionV2/index.tsx b/frontend/src/container/TopNav/DateTimeSelectionV2/index.tsx index 2b553f8017..3895d8b38c 100644 --- a/frontend/src/container/TopNav/DateTimeSelectionV2/index.tsx +++ b/frontend/src/container/TopNav/DateTimeSelectionV2/index.tsx @@ -468,7 +468,6 @@ function DateTimeSelection({ if (updatedTime !== 'custom') { urlQuery.delete('startTime'); urlQuery.delete('endTime'); - urlQuery.set(QueryParams.relativeTime, updatedTime); } else { const startTime = preStartTime.toString(); @@ -476,6 +475,7 @@ function DateTimeSelection({ urlQuery.set(QueryParams.startTime, startTime); urlQuery.set(QueryParams.endTime, endTime); + urlQuery.delete(QueryParams.relativeTime); } const generatedUrl = `${location.pathname}?${urlQuery.toString()}`; diff --git a/frontend/src/container/TraceDetail/index.tsx b/frontend/src/container/TraceDetail/index.tsx index 38f6db7c08..096b648acf 100644 --- a/frontend/src/container/TraceDetail/index.tsx +++ b/frontend/src/container/TraceDetail/index.tsx @@ -219,10 +219,18 @@ function TraceDetail({ response }: TraceDetailProps): JSX.Element { - - @@ -262,6 +270,7 @@ function TraceDetail({ response }: TraceDetailProps): JSX.Element { collapsedWidth={40} defaultCollapsed onCollapse={(value): void => setCollapsed(value)} + data-testid="span-details-sider" > {!collapsed && ( diff --git a/frontend/src/hooks/logs/types.ts b/frontend/src/hooks/logs/types.ts index 3dbcbb61f1..e76e2de794 100644 --- a/frontend/src/hooks/logs/types.ts +++ b/frontend/src/hooks/logs/types.ts @@ -12,7 +12,6 @@ export type UseCopyLogLink = { isHighlighted: boolean; isLogsExplorerPage: boolean; activeLogId: string | null; - timeRange: LogTimeRange | null; onLogCopy: MouseEventHandler; onTimeRangeChange: (newTimeRange: LogTimeRange | null) => void; }; @@ -28,4 +27,9 @@ export type UseActiveLog = { isJSON?: boolean, dataType?: DataTypes, ) => void; + onGroupByAttribute: ( + fieldKey: string, + isJSON?: boolean, + dataType?: DataTypes, + ) => Promise; }; diff --git a/frontend/src/hooks/logs/useActiveLog.ts b/frontend/src/hooks/logs/useActiveLog.ts index a56c13c72e..0a968c4650 100644 --- a/frontend/src/hooks/logs/useActiveLog.ts +++ b/frontend/src/hooks/logs/useActiveLog.ts @@ -128,6 +128,54 @@ export const useActiveLog = (): UseActiveLog => { [currentQuery, notifications, queryClient, redirectWithQueryBuilderData], ); + const onGroupByAttribute = useCallback( + async ( + fieldKey: string, + isJSON?: boolean, + dataType?: DataTypes, + ): Promise => { + try { + const keysAutocompleteResponse = await queryClient.fetchQuery( + [QueryBuilderKeys.GET_AGGREGATE_KEYS, fieldKey], + // eslint-disable-next-line sonarjs/no-identical-functions + async () => + getAggregateKeys({ + searchText: fieldKey, + aggregateOperator: currentQuery.builder.queryData[0].aggregateOperator, + dataSource: currentQuery.builder.queryData[0].dataSource, + aggregateAttribute: + currentQuery.builder.queryData[0].aggregateAttribute.key, + }), + ); + + const keysAutocomplete: BaseAutocompleteData[] = + keysAutocompleteResponse.payload?.attributeKeys || []; + + const existAutocompleteKey = chooseAutocompleteFromCustomValue( + keysAutocomplete, + fieldKey, + isJSON, + dataType, + ); + + const nextQuery: Query = { + ...currentQuery, + builder: { + ...currentQuery.builder, + queryData: currentQuery.builder.queryData.map((item) => ({ + ...item, + groupBy: [...item.groupBy, existAutocompleteKey], + })), + }, + }; + + redirectWithQueryBuilderData(nextQuery); + } catch { + notifications.error({ message: SOMETHING_WENT_WRONG }); + } + }, + [currentQuery, notifications, queryClient, redirectWithQueryBuilderData], + ); const onAddToQueryLogs = useCallback( (fieldKey: string, fieldValue: string, operator: string) => { const updatedQueryString = getGeneratedFilterQueryString( @@ -147,5 +195,6 @@ export const useActiveLog = (): UseActiveLog => { onSetActiveLog, onClearActiveLog, onAddToQuery: isLogsPage ? onAddToQueryLogs : onAddToQueryExplorer, + onGroupByAttribute, }; }; diff --git a/frontend/src/hooks/logs/useCopyLogLink.ts b/frontend/src/hooks/logs/useCopyLogLink.ts index 8dee58c710..b3b512f380 100644 --- a/frontend/src/hooks/logs/useCopyLogLink.ts +++ b/frontend/src/hooks/logs/useCopyLogLink.ts @@ -26,33 +26,25 @@ export const useCopyLogLink = (logId?: string): UseCopyLogLink => { const [, setCopy] = useCopyToClipboard(); const { notifications } = useNotifications(); - const { queryData: timeRange } = useUrlQueryData( - QueryParams.timeRange, - null, - ); - const { queryData: activeLogId } = useUrlQueryData( QueryParams.activeLogId, null, ); - const { selectedTime } = useSelector( - (state) => state.globalTime, - ); + const { selectedTime, minTime, maxTime } = useSelector< + AppState, + GlobalReducer + >((state) => state.globalTime); const onTimeRangeChange = useCallback( (newTimeRange: LogTimeRange | null): void => { - urlQuery.set(QueryParams.timeRange, JSON.stringify(newTimeRange)); - if (selectedTime !== 'custom') { urlQuery.delete(QueryParams.startTime); urlQuery.delete(QueryParams.endTime); - urlQuery.set(QueryParams.relativeTime, selectedTime); } else { urlQuery.set(QueryParams.startTime, newTimeRange?.start.toString() || ''); urlQuery.set(QueryParams.endTime, newTimeRange?.end.toString() || ''); - urlQuery.delete(QueryParams.relativeTime); } @@ -76,14 +68,12 @@ export const useCopyLogLink = (logId?: string): UseCopyLogLink => { event.preventDefault(); event.stopPropagation(); - const range = JSON.stringify(timeRange); - urlQuery.delete(QueryParams.activeLogId); - urlQuery.delete(QueryParams.timeRange); + urlQuery.delete(QueryParams.relativeTime); + urlQuery.set(QueryParams.activeLogId, `"${logId}"`); - urlQuery.set(QueryParams.timeRange, range); - urlQuery.set(QueryParams.startTime, timeRange?.start.toString() || ''); - urlQuery.set(QueryParams.endTime, timeRange?.end.toString() || ''); + urlQuery.set(QueryParams.startTime, minTime?.toString() || ''); + urlQuery.set(QueryParams.endTime, maxTime?.toString() || ''); const link = `${window.location.origin}${pathname}?${urlQuery.toString()}`; @@ -92,7 +82,7 @@ export const useCopyLogLink = (logId?: string): UseCopyLogLink => { message: 'Copied to clipboard', }); }, - [logId, timeRange, urlQuery, pathname, setCopy, notifications], + [logId, urlQuery, minTime, maxTime, pathname, setCopy, notifications], ); useEffect(() => { @@ -110,7 +100,6 @@ export const useCopyLogLink = (logId?: string): UseCopyLogLink => { isHighlighted, isLogsExplorerPage, activeLogId, - timeRange, onLogCopy, onTimeRangeChange, }; diff --git a/frontend/src/hooks/queryBuilder/useAutoComplete.ts b/frontend/src/hooks/queryBuilder/useAutoComplete.ts index 8872b2dc02..ed1836a0fa 100644 --- a/frontend/src/hooks/queryBuilder/useAutoComplete.ts +++ b/frontend/src/hooks/queryBuilder/useAutoComplete.ts @@ -8,7 +8,10 @@ import { import { Option } from 'container/QueryBuilder/type'; import { parse } from 'papaparse'; import { KeyboardEvent, useCallback, useState } from 'react'; -import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; +import { + IBuilderQuery, + TagFilter, +} from 'types/api/queryBuilder/queryBuilderData'; import { useFetchKeysAndValues } from './useFetchKeysAndValues'; import { useOptions, WHERE_CLAUSE_CUSTOM_SUFFIX } from './useOptions'; @@ -24,14 +27,16 @@ export type WhereClauseConfig = { export const useAutoComplete = ( query: IBuilderQuery, whereClauseConfig?: WhereClauseConfig, + shouldUseSuggestions?: boolean, ): IAutoComplete => { const [searchValue, setSearchValue] = useState(''); const [searchKey, setSearchKey] = useState(''); - const { keys, results, isFetching } = useFetchKeysAndValues( + const { keys, results, isFetching, exampleQueries } = useFetchKeysAndValues( searchValue, query, searchKey, + shouldUseSuggestions, ); const [key, operator, result] = useSetCurrentKeyAndOperator(searchValue, keys); @@ -144,6 +149,8 @@ export const useAutoComplete = ( isFetching, setSearchKey, searchKey, + key, + exampleQueries, }; }; @@ -161,4 +168,6 @@ interface IAutoComplete { isFetching: boolean; setSearchKey: (value: string) => void; searchKey: string; + key: string; + exampleQueries: TagFilter[]; } diff --git a/frontend/src/hooks/queryBuilder/useFetchKeysAndValues.ts b/frontend/src/hooks/queryBuilder/useFetchKeysAndValues.ts index db54685b8e..6fd42175ad 100644 --- a/frontend/src/hooks/queryBuilder/useFetchKeysAndValues.ts +++ b/frontend/src/hooks/queryBuilder/useFetchKeysAndValues.ts @@ -6,17 +6,21 @@ import { isInNInOperator, } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils'; import useDebounceValue from 'hooks/useDebounce'; -import { isEqual, uniqWith } from 'lodash-es'; +import { cloneDeep, isEqual, uniqWith, unset } from 'lodash-es'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useDebounce } from 'react-use'; import { BaseAutocompleteData, DataTypes, } from 'types/api/queryBuilder/queryAutocompleteResponse'; -import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; +import { + IBuilderQuery, + TagFilter, +} from 'types/api/queryBuilder/queryBuilderData'; import { DataSource } from 'types/common/queryBuilder'; import { useGetAggregateKeys } from './useGetAggregateKeys'; +import { useGetAttributeSuggestions } from './useGetAttributeSuggestions'; type IuseFetchKeysAndValues = { keys: BaseAutocompleteData[]; @@ -24,6 +28,7 @@ type IuseFetchKeysAndValues = { isFetching: boolean; sourceKeys: BaseAutocompleteData[]; handleRemoveSourceKey: (newSourceKey: string) => void; + exampleQueries: TagFilter[]; }; /** @@ -37,8 +42,10 @@ export const useFetchKeysAndValues = ( searchValue: string, query: IBuilderQuery, searchKey: string, + shouldUseSuggestions?: boolean, ): IuseFetchKeysAndValues => { const [keys, setKeys] = useState([]); + const [exampleQueries, setExampleQueries] = useState([]); const [sourceKeys, setSourceKeys] = useState([]); const [results, setResults] = useState([]); const [isAggregateFetching, setAggregateFetching] = useState(false); @@ -60,18 +67,34 @@ export const useFetchKeysAndValues = ( const searchParams = useDebounceValue(memoizedSearchParams, DEBOUNCE_DELAY); + const queryFiltersWithoutId = useMemo( + () => ({ + ...query.filters, + items: query.filters.items.map((item) => { + const filterWithoutId = cloneDeep(item); + unset(filterWithoutId, 'id'); + return filterWithoutId; + }), + }), + [query.filters], + ); + + const memoizedSuggestionsParams = useMemo( + () => [searchKey, query.dataSource, queryFiltersWithoutId], + [query.dataSource, queryFiltersWithoutId, searchKey], + ); + + const suggestionsParams = useDebounceValue( + memoizedSuggestionsParams, + DEBOUNCE_DELAY, + ); + const isQueryEnabled = useMemo( () => query.dataSource === DataSource.METRICS - ? !!query.aggregateOperator && - !!query.dataSource && - !!query.aggregateAttribute.dataType + ? !!query.dataSource && !!query.aggregateAttribute.dataType : true, - [ - query.aggregateAttribute.dataType, - query.aggregateOperator, - query.dataSource, - ], + [query.aggregateAttribute.dataType, query.dataSource], ); const { data, isFetching, status } = useGetAggregateKeys( @@ -82,7 +105,26 @@ export const useFetchKeysAndValues = ( aggregateAttribute: query.aggregateAttribute.key, tagType: query.aggregateAttribute.type ?? null, }, - { queryKey: [searchParams], enabled: isQueryEnabled }, + { + queryKey: [searchParams], + enabled: isQueryEnabled && !shouldUseSuggestions, + }, + ); + + const { + data: suggestionsData, + isFetching: isFetchingSuggestions, + status: fetchingSuggestionsStatus, + } = useGetAttributeSuggestions( + { + searchText: searchKey, + dataSource: query.dataSource, + filters: query.filters, + }, + { + queryKey: [suggestionsParams], + enabled: isQueryEnabled && shouldUseSuggestions, + }, ); /** @@ -162,11 +204,41 @@ export const useFetchKeysAndValues = ( } }, [data?.payload?.attributeKeys, status]); + useEffect(() => { + if ( + fetchingSuggestionsStatus === 'success' && + suggestionsData?.payload?.attributes + ) { + setKeys(suggestionsData.payload.attributes); + setSourceKeys((prevState) => + uniqWith( + [...(suggestionsData.payload.attributes ?? []), ...prevState], + isEqual, + ), + ); + } else { + setKeys([]); + } + if ( + fetchingSuggestionsStatus === 'success' && + suggestionsData?.payload?.example_queries + ) { + setExampleQueries(suggestionsData.payload.example_queries); + } else { + setExampleQueries([]); + } + }, [ + suggestionsData?.payload?.attributes, + fetchingSuggestionsStatus, + suggestionsData?.payload?.example_queries, + ]); + return { keys, results, - isFetching: isFetching || isAggregateFetching, + isFetching: isFetching || isAggregateFetching || isFetchingSuggestions, sourceKeys, handleRemoveSourceKey, + exampleQueries, }; }; diff --git a/frontend/src/hooks/queryBuilder/useGetAggregateValues.ts b/frontend/src/hooks/queryBuilder/useGetAggregateValues.ts new file mode 100644 index 0000000000..d749e5ec9a --- /dev/null +++ b/frontend/src/hooks/queryBuilder/useGetAggregateValues.ts @@ -0,0 +1,33 @@ +import { getAttributesValues } from 'api/queryBuilder/getAttributesValues'; +import { useMemo } from 'react'; +import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { + IAttributeValuesResponse, + IGetAttributeValuesPayload, +} from 'types/api/queryBuilder/getAttributesValues'; + +type UseGetAttributeValues = ( + requestData: IGetAttributeValuesPayload, + options?: UseQueryOptions< + SuccessResponse | ErrorResponse + >, +) => UseQueryResult | ErrorResponse>; + +export const useGetAggregateValues: UseGetAttributeValues = ( + requestData, + options, +) => { + const queryKey = useMemo(() => { + if (options?.queryKey && Array.isArray(options.queryKey)) { + return [...options.queryKey]; + } + return [requestData]; + }, [options?.queryKey, requestData]); + + return useQuery | ErrorResponse>({ + queryKey, + queryFn: () => getAttributesValues(requestData), + ...options, + }); +}; diff --git a/frontend/src/hooks/queryBuilder/useGetAttributeSuggestions.ts b/frontend/src/hooks/queryBuilder/useGetAttributeSuggestions.ts new file mode 100644 index 0000000000..d2ee7042cd --- /dev/null +++ b/frontend/src/hooks/queryBuilder/useGetAttributeSuggestions.ts @@ -0,0 +1,38 @@ +import { getAttributeSuggestions } from 'api/queryBuilder/getAttributeSuggestions'; +import { QueryBuilderKeys } from 'constants/queryBuilder'; +import { useMemo } from 'react'; +import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { + IGetAttributeSuggestionsPayload, + IGetAttributeSuggestionsSuccessResponse, +} from 'types/api/queryBuilder/getAttributeSuggestions'; + +type UseGetAttributeSuggestions = ( + requestData: IGetAttributeSuggestionsPayload, + options?: UseQueryOptions< + SuccessResponse | ErrorResponse + >, +) => UseQueryResult< + SuccessResponse | ErrorResponse +>; + +export const useGetAttributeSuggestions: UseGetAttributeSuggestions = ( + requestData, + options, +) => { + const queryKey = useMemo(() => { + if (options?.queryKey && Array.isArray(options.queryKey)) { + return [QueryBuilderKeys.GET_ATTRIBUTE_SUGGESTIONS, ...options.queryKey]; + } + return [QueryBuilderKeys.GET_ATTRIBUTE_SUGGESTIONS, requestData]; + }, [options?.queryKey, requestData]); + + return useQuery< + SuccessResponse | ErrorResponse + >({ + queryKey, + queryFn: () => getAttributeSuggestions(requestData), + ...options, + }); +}; diff --git a/frontend/src/hooks/queryBuilder/useGetExplorerQueryRange.ts b/frontend/src/hooks/queryBuilder/useGetExplorerQueryRange.ts index cdcfb3e0c7..13093ab254 100644 --- a/frontend/src/hooks/queryBuilder/useGetExplorerQueryRange.ts +++ b/frontend/src/hooks/queryBuilder/useGetExplorerQueryRange.ts @@ -1,6 +1,6 @@ import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; -import { useMemo } from 'react'; +import { MutableRefObject, useMemo } from 'react'; import { UseQueryOptions, UseQueryResult } from 'react-query'; import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; @@ -19,6 +19,8 @@ export const useGetExplorerQueryRange = ( options?: UseQueryOptions, Error>, params?: Record, isDependentOnQB = true, + keyRef?: MutableRefObject, + headers?: Record, ): UseQueryResult, Error> => { const { isEnabledQuery } = useQueryBuilder(); const { selectedTime: globalSelectedInterval, minTime, maxTime } = useSelector< @@ -40,6 +42,11 @@ export const useGetExplorerQueryRange = ( return isEnabledQuery; }, [options, isEnabledQuery, isDependentOnQB]); + if (keyRef) { + // eslint-disable-next-line no-param-reassign + keyRef.current = [key, globalSelectedInterval, requestData, minTime, maxTime]; + } + return useGetQueryRange( { graphType: panelType || PANEL_TYPES.LIST, @@ -55,5 +62,6 @@ export const useGetExplorerQueryRange = ( queryKey: [key, globalSelectedInterval, requestData, minTime, maxTime], enabled: isEnabled, }, + headers, ); }; diff --git a/frontend/src/hooks/queryBuilder/useGetQueryRange.ts b/frontend/src/hooks/queryBuilder/useGetQueryRange.ts index 334ee7f628..e0c6ea3172 100644 --- a/frontend/src/hooks/queryBuilder/useGetQueryRange.ts +++ b/frontend/src/hooks/queryBuilder/useGetQueryRange.ts @@ -13,12 +13,14 @@ type UseGetQueryRange = ( requestData: GetQueryResultsProps, version: string, options?: UseQueryOptions, Error>, + headers?: Record, ) => UseQueryResult, Error>; export const useGetQueryRange: UseGetQueryRange = ( requestData, version, options, + headers, ) => { const newRequestData: GetQueryResultsProps = useMemo( () => ({ @@ -45,7 +47,7 @@ export const useGetQueryRange: UseGetQueryRange = ( return useQuery, Error>({ queryFn: async ({ signal }) => - GetMetricQueryRange(requestData, version, signal), + GetMetricQueryRange(requestData, version, signal, headers), ...options, queryKey, }); diff --git a/frontend/src/hooks/queryBuilder/useIsValidTag.ts b/frontend/src/hooks/queryBuilder/useIsValidTag.ts index 216971b0ac..d4e2b58080 100644 --- a/frontend/src/hooks/queryBuilder/useIsValidTag.ts +++ b/frontend/src/hooks/queryBuilder/useIsValidTag.ts @@ -2,7 +2,7 @@ import { useMemo } from 'react'; import { OperatorType } from './useOperatorType'; -const validationMapper: Record< +export const validationMapper: Record< OperatorType, (resultLength: number) => boolean > = { diff --git a/frontend/src/hooks/queryBuilder/useOperatorType.ts b/frontend/src/hooks/queryBuilder/useOperatorType.ts index 94de55df92..1ff0619506 100644 --- a/frontend/src/hooks/queryBuilder/useOperatorType.ts +++ b/frontend/src/hooks/queryBuilder/useOperatorType.ts @@ -6,7 +6,7 @@ export type OperatorType = | 'NON_VALUE' | 'NOT_VALID'; -const operatorTypeMapper: Record = { +export const operatorTypeMapper: Record = { [OPERATORS.IN]: 'MULTIPLY_VALUE', [OPERATORS.NIN]: 'MULTIPLY_VALUE', [OPERATORS.EXISTS]: 'NON_VALUE', diff --git a/frontend/src/hooks/queryBuilder/useOptions.ts b/frontend/src/hooks/queryBuilder/useOptions.ts index f050d19f82..2f24dd0d21 100644 --- a/frontend/src/hooks/queryBuilder/useOptions.ts +++ b/frontend/src/hooks/queryBuilder/useOptions.ts @@ -45,6 +45,7 @@ export const useOptions = ( label: `${getLabel(item)}`, value: item.key, dataType: item.dataType, + isIndexed: item?.isIndexed, })), [getLabel], ); diff --git a/frontend/src/hooks/useLogsData.ts b/frontend/src/hooks/useLogsData.ts index 16e550ba56..a26e57abfa 100644 --- a/frontend/src/hooks/useLogsData.ts +++ b/frontend/src/hooks/useLogsData.ts @@ -7,6 +7,8 @@ import { import { DEFAULT_PER_PAGE_VALUE } from 'container/Controls/config'; import { getPaginationQueryData } from 'lib/newQueryBuilder/getPaginationQueryData'; import { useEffect, useMemo, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; import { ILog } from 'types/api/logs/log'; import { IBuilderQuery, @@ -15,6 +17,7 @@ import { TagFilter, } from 'types/api/queryBuilder/queryBuilderData'; import { QueryDataV3 } from 'types/api/widgets/getQuery'; +import { GlobalReducer } from 'types/reducer/globalTime'; import { LogTimeRange } from './logs/types'; import { useCopyLogLink } from './logs/useCopyLogLink'; @@ -39,6 +42,10 @@ export const useLogsData = ({ const [requestData, setRequestData] = useState(null); const [shouldLoadMoreLogs, setShouldLoadMoreLogs] = useState(false); + const { minTime, maxTime } = useSelector( + (state) => state.globalTime, + ); + const { queryData: pageSize } = useUrlQueryData( QueryParams.pageSize, DEFAULT_PER_PAGE_VALUE, @@ -122,7 +129,7 @@ export const useLogsData = ({ return data; }; - const { activeLogId, timeRange, onTimeRangeChange } = useCopyLogLink(); + const { activeLogId, onTimeRangeChange } = useCopyLogLink(); const { data, isFetching } = useGetExplorerQueryRange( requestData, @@ -133,11 +140,10 @@ export const useLogsData = ({ enabled: !isLimit && !!requestData, }, { - ...(timeRange && - activeLogId && + ...(activeLogId && !logs.length && { - start: timeRange.start, - end: timeRange.end, + start: minTime, + end: maxTime, }), }, shouldLoadMoreLogs, @@ -156,7 +162,7 @@ export const useLogsData = ({ setLogs(newLogs); onTimeRangeChange({ start: currentParams?.start, - end: timeRange?.end || currentParams?.end, + end: currentParams?.end, pageSize: newLogs.length, }); } diff --git a/frontend/src/lib/dashboard/getQueryResults.ts b/frontend/src/lib/dashboard/getQueryResults.ts index 50e6648924..03ad5f8caa 100644 --- a/frontend/src/lib/dashboard/getQueryResults.ts +++ b/frontend/src/lib/dashboard/getQueryResults.ts @@ -23,6 +23,7 @@ export async function GetMetricQueryRange( props: GetQueryResultsProps, version: string, signal?: AbortSignal, + headers?: Record, ): Promise> { const { legendMap, queryPayload } = prepareQueryRangePayload(props); @@ -30,6 +31,7 @@ export async function GetMetricQueryRange( queryPayload, version || 'v3', signal, + headers, ); if (response.statusCode >= 400) { diff --git a/frontend/src/lib/uPlotLib/getUplotChartOptions.ts b/frontend/src/lib/uPlotLib/getUplotChartOptions.ts index 29904f0d6a..5000b0f0d8 100644 --- a/frontend/src/lib/uPlotLib/getUplotChartOptions.ts +++ b/frontend/src/lib/uPlotLib/getUplotChartOptions.ts @@ -53,6 +53,7 @@ export interface GetUPlotChartOptions { [key: string]: boolean; }> >; + customTooltipElement?: HTMLDivElement; } /** the function converts series A , series B , series C to @@ -154,6 +155,7 @@ export const getUPlotChartOptions = ({ stackBarChart: stackChart, hiddenGraph, setHiddenGraph, + customTooltipElement, }: GetUPlotChartOptions): uPlot.Options => { const timeScaleProps = getXAxisScale(minTimeScale, maxTimeScale); @@ -209,9 +211,16 @@ export const getUPlotChartOptions = ({ }, }, plugins: [ - tooltipPlugin({ apiResponse, yAxisUnit, stackBarChart, isDarkMode }), + tooltipPlugin({ + apiResponse, + yAxisUnit, + stackBarChart, + isDarkMode, + customTooltipElement, + }), onClickPlugin({ onClick: onClickHandler, + apiResponse, }), ], hooks: { diff --git a/frontend/src/lib/uPlotLib/plugins/onClickPlugin.ts b/frontend/src/lib/uPlotLib/plugins/onClickPlugin.ts index 7dfbbe9b47..fafc4afe64 100644 --- a/frontend/src/lib/uPlotLib/plugins/onClickPlugin.ts +++ b/frontend/src/lib/uPlotLib/plugins/onClickPlugin.ts @@ -1,10 +1,16 @@ +import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; + export interface OnClickPluginOpts { onClick: ( xValue: number, yValue: number, mouseX: number, mouseY: number, + data?: { + [key: string]: string; + }, ) => void; + apiResponse?: MetricRangePayloadProps; } function onClickPlugin(opts: OnClickPluginOpts): uPlot.Plugin { @@ -22,9 +28,24 @@ function onClickPlugin(opts: OnClickPluginOpts): uPlot.Plugin { const xValue = u.posToVal(event.offsetX, 'x'); const yValue = u.posToVal(event.offsetY, 'y'); - opts.onClick(xValue, yValue, mouseX, mouseY); - }; + let metric = {}; + const { series } = u; + const apiResult = opts.apiResponse?.data?.result || []; + // this is to get the metric value of the focused series + if (Array.isArray(series) && series.length > 0) { + series.forEach((item, index) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + if (item?.show && item?._focus) { + const { metric: focusedMetric } = apiResult[index - 1] || []; + metric = focusedMetric; + } + }); + } + + opts.onClick(xValue, yValue, mouseX, mouseY, metric); + }; u.over.addEventListener('click', handleClick); }, destroy: (u: uPlot) => { diff --git a/frontend/src/lib/uPlotLib/plugins/tooltipPlugin.ts b/frontend/src/lib/uPlotLib/plugins/tooltipPlugin.ts index 16178fe5f0..882bf73db0 100644 --- a/frontend/src/lib/uPlotLib/plugins/tooltipPlugin.ts +++ b/frontend/src/lib/uPlotLib/plugins/tooltipPlugin.ts @@ -222,6 +222,7 @@ type ToolTipPluginProps = { isMergedSeries?: boolean; stackBarChart?: boolean; isDarkMode: boolean; + customTooltipElement?: HTMLDivElement; }; const tooltipPlugin = ({ @@ -232,7 +233,9 @@ const tooltipPlugin = ({ isMergedSeries, stackBarChart, isDarkMode, -}: ToolTipPluginProps): any => { + customTooltipElement, +}: // eslint-disable-next-line sonarjs/cognitive-complexity +ToolTipPluginProps): any => { let over: HTMLElement; let bound: HTMLElement; let bLeft: any; @@ -298,6 +301,9 @@ const tooltipPlugin = ({ isMergedSeries, stackBarChart, ); + if (customTooltipElement) { + content.appendChild(customTooltipElement); + } overlay.appendChild(content); placement(overlay, anchor, 'right', 'start', { bound }); } diff --git a/frontend/src/mocks-server/__mockdata__/explorer_views.ts b/frontend/src/mocks-server/__mockdata__/explorer_views.ts index ae88071e55..4719e77697 100644 --- a/frontend/src/mocks-server/__mockdata__/explorer_views.ts +++ b/frontend/src/mocks-server/__mockdata__/explorer_views.ts @@ -77,5 +77,67 @@ export const explorerView = { }, extraData: '{"color":"#00ffd0"}', }, + { + uuid: '8c4bf492-d54d-4ab2-a8d6-9c1563f46e1f', + name: 'R-test panel', + category: '', + createdAt: '2024-07-01T13:45:57.924686766Z', + createdBy: 'test-user-test', + updatedAt: '2024-07-01T13:48:31.032106578Z', + updatedBy: 'test-user-test', + sourcePage: 'traces', + tags: [''], + compositeQuery: { + builderQueries: { + A: { + queryName: 'A', + stepInterval: 60, + dataSource: 'traces', + aggregateOperator: 'noop', + aggregateAttribute: { + key: '', + dataType: '', + type: '', + isColumn: false, + isJSON: false, + }, + filters: { + op: 'AND', + items: [ + { + key: { + key: 'httpMethod', + dataType: 'string', + type: 'tag', + isColumn: true, + isJSON: false, + }, + value: 'GET', + op: '=', + }, + ], + }, + expression: 'A', + disabled: false, + limit: 0, + offset: 0, + pageSize: 0, + orderBy: [ + { + columnName: 'timestamp', + order: 'desc', + }, + ], + reduceTo: 'avg', + timeAggregation: 'rate', + spaceAggregation: 'sum', + ShiftBy: 0, + }, + }, + panelType: 'list', + queryType: 'builder', + }, + extraData: '{"color":"#AD7F58"}', + }, ], }; diff --git a/frontend/src/mocks-server/__mockdata__/query_range.ts b/frontend/src/mocks-server/__mockdata__/query_range.ts index 69ff7bfb66..ef55283a96 100644 --- a/frontend/src/mocks-server/__mockdata__/query_range.ts +++ b/frontend/src/mocks-server/__mockdata__/query_range.ts @@ -1,3 +1,4 @@ +/* eslint-disable sonarjs/no-duplicate-string */ import { PANEL_TYPES } from 'constants/queryBuilder'; import { QueryRangePayload } from 'types/api/metrics/getQueryRange'; import { EQueryType } from 'types/common/dashboard'; @@ -77,3 +78,245 @@ export const queryRangeSuccessResponse: QueryRangePayload = { start: 0, step: 0, }; + +export const queryRangeForTimeSeries = { + status: 'success', + data: { + resultType: '', + result: [ + { + queryName: 'A', + series: [ + { + labels: {}, + labelsArray: null, + values: [ + { + timestamp: 1721378340000, + value: '3074', + }, + { + timestamp: 1721378100000, + value: '2983', + }, + { + timestamp: 1721378040000, + value: '2978', + }, + { + timestamp: 1721378160000, + value: '2940', + }, + { + timestamp: 1721377980000, + value: '2904', + }, + { + timestamp: 1721378280000, + value: '2874', + }, + { + timestamp: 1721378220000, + value: '2667', + }, + ], + }, + ], + }, + ], + }, +}; + +export const queryRangeForListView = { + status: 'success', + data: { + resultType: '', + result: [ + { + queryName: 'A', + list: [ + { + timestamp: '2024-07-19T08:39:59.949129915Z', + data: { + dbName: '', + durationNano: 790949390, + httpMethod: '', + name: 'authenticate_check_db', + responseStatusCode: '', + serviceName: 'demo-app', + spanID: '5704353737b6778e', + statusCode: 0, + traceID: 'a364a8e15af3e9a8c866e0528db8b637', + }, + }, + { + timestamp: '2024-07-19T08:39:59.506524482Z', + data: { + dbName: '', + durationNano: 1375203118, + httpMethod: '', + name: 'check cart in cache', + responseStatusCode: '', + serviceName: 'demo-app', + spanID: '2134bb1165c928aa', + statusCode: 0, + traceID: '7b565bc351bac2a12c004d92d3a809b1', + }, + }, + { + timestamp: '2024-07-19T08:39:58.735245Z', + data: { + dbName: '', + durationNano: 55306000, + httpMethod: 'GET', + name: 'HTTP GET', + responseStatusCode: '200', + serviceName: 'frontend', + spanID: '772c4d29dd9076ac', + statusCode: 0, + traceID: '0000000000000000344ded1387b08a7e', + }, + }, + ], + }, + ], + }, +}; + +export const queryRangeForTableView = { + status: 'success', + data: { + resultType: '', + result: [ + { + queryName: 'A', + series: [ + { + labels: {}, + labelsArray: null, + values: [ + { + timestamp: 1721583834000, + value: '87798', + }, + ], + }, + ], + }, + ], + }, +}; + +export const queryRangeForTraceView = { + status: 'success', + data: { + resultType: '', + result: [ + { + queryName: 'A', + list: [ + { + timestamp: '0001-01-01T00:00:00Z', + data: { + span_count: 8, + 'subQuery.durationNano': 7245231266, + 'subQuery.name': 'home', + 'subQuery.serviceName': 'demo-app', + traceID: '5765b60ba7cc4ddafe8bdaa9c1b4b246', + }, + }, + { + timestamp: '0001-01-01T00:00:00Z', + data: { + span_count: 8, + 'subQuery.durationNano': 7218609120, + 'subQuery.name': 'home', + 'subQuery.serviceName': 'demo-app', + traceID: '1593c896d96cc6b2478bb95dcc01e3f5', + }, + }, + { + timestamp: '0001-01-01T00:00:00Z', + data: { + span_count: 8, + 'subQuery.durationNano': 7217156051, + 'subQuery.name': 'home', + 'subQuery.serviceName': 'demo-app', + traceID: 'dcd145ed13937795c5e2ee8618ec7e32', + }, + }, + { + timestamp: '0001-01-01T00:00:00Z', + data: { + span_count: 8, + 'subQuery.durationNano': 7054152134, + 'subQuery.name': 'home', + 'subQuery.serviceName': 'demo-app', + traceID: 'd9ceed0a6b23ed4b3bff664e2b303382', + }, + }, + { + timestamp: '0001-01-01T00:00:00Z', + data: { + span_count: 8, + 'subQuery.durationNano': 7052324178, + 'subQuery.name': 'home', + 'subQuery.serviceName': 'demo-app', + traceID: 'f76f1acc10a9149121c2bf715d1f92c5', + }, + }, + { + timestamp: '0001-01-01T00:00:00Z', + data: { + span_count: 8, + 'subQuery.durationNano': 6998186102, + 'subQuery.name': 'home', + 'subQuery.serviceName': 'demo-app', + traceID: '1e3acf6649147117836cfdde66e2bde5', + }, + }, + { + timestamp: '0001-01-01T00:00:00Z', + data: { + span_count: 8, + 'subQuery.durationNano': 6898849195, + 'subQuery.name': 'home', + 'subQuery.serviceName': 'demo-app', + traceID: '035b210595493adcef4c7f297a427bb0', + }, + }, + { + timestamp: '0001-01-01T00:00:00Z', + data: { + span_count: 8, + 'subQuery.durationNano': 6829435795, + 'subQuery.name': 'home', + 'subQuery.serviceName': 'demo-app', + traceID: '4ae4d4d082fc6d7a20d90ae0b1d0fff1', + }, + }, + { + timestamp: '0001-01-01T00:00:00Z', + data: { + span_count: 8, + 'subQuery.durationNano': 6790765891, + 'subQuery.name': 'home', + 'subQuery.serviceName': 'demo-app', + traceID: '7975c032b430ac63479e5d578c1f0edd', + }, + }, + { + timestamp: '0001-01-01T00:00:00Z', + data: { + span_count: 8, + 'subQuery.durationNano': 6786616927, + 'subQuery.name': 'home', + 'subQuery.serviceName': 'demo-app', + traceID: 'ce9d3e5d66dbdd41d46d519b615cce52', + }, + }, + ], + }, + ], + }, +}; diff --git a/frontend/src/mocks-server/__mockdata__/tracedetail.ts b/frontend/src/mocks-server/__mockdata__/tracedetail.ts new file mode 100644 index 0000000000..c0e439f65f --- /dev/null +++ b/frontend/src/mocks-server/__mockdata__/tracedetail.ts @@ -0,0 +1,2089 @@ +/* eslint-disable sonarjs/no-duplicate-string */ +export const traceDetailResponse = [ + { + startTimestampMillis: 1721304358677, + endTimestampMillis: 1721304360928, + columns: [ + '__time', + 'SpanId', + 'TraceId', + 'ServiceName', + 'Name', + 'Kind', + 'DurationNano', + 'TagsKeys', + 'TagsValues', + 'References', + 'Events', + 'HasError', + 'StatusMessage', + 'StatusCodeString', + 'SpanKind', + ], + events: [ + [ + 1721304359454, + '74fff7b42cbc923b', + '000000000000000071dc9b0a338729b4', + 'customer', + 'HTTP GET /customer', + '2', + '348950000', + [ + 'signoz.collector.id', + 'client-uuid', + 'component', + 'host.name', + 'http.method', + 'http.url', + 'ip', + 'http.status_code', + 'opencensus.exporterversion', + 'service.name', + ], + [ + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '7cd5f22c2034bc4e', + 'net/http', + '4f6ec470feea', + 'GET', + '/customer?customer=731', + '172.25.0.2', + '200', + 'Jaeger-Go-2.30.0', + 'customer', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=1518282c4475a6d7, RefType=CHILD_OF}', + ], + [ + '{"name":"HTTP request received","timeUnixNano":1721304359454282000,"attributeMap":{"level":"info","method":"GET","url":"/customer?customer=731"}}', + '{"name":"Loading customer","timeUnixNano":1721304359454308000,"attributeMap":{"customer_id":"731","level":"info"}}', + ], + false, + '', + 'Unset', + 'Server', + ], + [ + 1721304359454, + '1518282c4475a6d7', + '000000000000000071dc9b0a338729b4', + 'frontend', + 'HTTP GET', + '3', + '349380000', + [ + 'net/http.was_idle', + 'client-uuid', + 'host.name', + 'http.status_code', + 'net/http.reused', + 'opencensus.exporterversion', + 'service.name', + 'signoz.collector.id', + 'component', + 'http.method', + 'http.url', + 'ip', + ], + [ + 'true', + '6fb81b8ca91b2b4d', + '4f6ec470feea', + '200', + 'true', + 'Jaeger-Go-2.30.0', + 'frontend', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + 'net/http', + 'GET', + '0.0.0.0:8081', + '172.25.0.2', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=51a3062c3456cd26, RefType=CHILD_OF}', + ], + [ + '{"name":"GetConn","timeUnixNano":1721304359454120000}', + '{"name":"GotConn","timeUnixNano":1721304359454129000}', + '{"name":"WroteHeaders","timeUnixNano":1721304359454156000}', + '{"name":"WroteRequest","timeUnixNano":1721304359454158000}', + '{"name":"GotFirstResponseByte","timeUnixNano":1721304359803413000}', + '{"name":"PutIdleConn","timeUnixNano":1721304359803452000}', + '{"name":"ClosedBody","timeUnixNano":1721304359803472000}', + ], + false, + '', + 'Unset', + 'Client', + ], + [ + 1721304359454, + '51a3062c3456cd26', + '000000000000000071dc9b0a338729b4', + 'frontend', + 'HTTP GET: /customer', + '0', + '349435000', + [ + 'opencensus.exporterversion', + 'service.name', + 'signoz.collector.id', + 'client-uuid', + 'host.name', + 'ip', + ], + [ + 'Jaeger-Go-2.30.0', + 'frontend', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '6fb81b8ca91b2b4d', + '4f6ec470feea', + '172.25.0.2', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=71dc9b0a338729b4, RefType=CHILD_OF}', + ], + [], + false, + '', + 'Unset', + 'Unspecified', + ], + [ + 1721304359454, + '67e901d53c8f11d4', + '000000000000000071dc9b0a338729b4', + 'mysql', + 'SQL SELECT', + '3', + '348855000', + [ + 'host.name', + 'peer.service', + 'request', + 'signoz.collector.id', + 'sql.query', + 'client-uuid', + 'ip', + 'opencensus.exporterversion', + 'service.name', + ], + [ + '4f6ec470feea', + 'mysql', + '', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + 'SELECT * FROM customer WHERE customer_id=731', + '3441c794741337d0', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + 'mysql', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=74fff7b42cbc923b, RefType=CHILD_OF}', + ], + [ + '{"name":"Waiting for lock behind 1 transactions","timeUnixNano":1721304359454335000,"attributeMap":{"blockers":"[]"}}', + '{"name":"Acquired lock with 0 transactions waiting behind","timeUnixNano":1721304359529107000}', + ], + false, + '', + 'Unset', + 'Client', + ], + [ + 1721304359803, + '541274ec900e06ae', + '000000000000000071dc9b0a338729b4', + 'redis', + 'FindDriverIDs', + '3', + '15030000', + [ + 'service.name', + 'signoz.collector.id', + 'client-uuid', + 'host.name', + 'ip', + 'opencensus.exporterversion', + 'param.location', + ], + [ + 'redis', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '1b45eecbdc9fca62', + '4f6ec470feea', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + '728,326', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=22eadd8a0f27a552, RefType=CHILD_OF}', + ], + [ + '{"name":"Found drivers","timeUnixNano":1721304359818819000,"attributeMap":{"level":"info"}}', + ], + false, + '', + 'Unset', + 'Client', + ], + [ + 1721304359818, + '14a6cad2442f62ae', + '000000000000000071dc9b0a338729b4', + 'redis', + 'GetDriver', + '3', + '7524000', + [ + 'client-uuid', + 'host.name', + 'ip', + 'opencensus.exporterversion', + 'param.driverID', + 'service.name', + 'signoz.collector.id', + ], + [ + '1b45eecbdc9fca62', + '4f6ec470feea', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + 'T746480C', + 'redis', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=22eadd8a0f27a552, RefType=CHILD_OF}', + ], + [], + false, + '', + 'Unset', + 'Client', + ], + [ + 1721304359826, + '49771a34b0287913', + '000000000000000071dc9b0a338729b4', + 'redis', + 'GetDriver', + '3', + '10207000', + [ + 'param.driverID', + 'service.name', + 'signoz.collector.id', + 'client-uuid', + 'host.name', + 'ip', + 'opencensus.exporterversion', + ], + [ + 'T787177C', + 'redis', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '1b45eecbdc9fca62', + '4f6ec470feea', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=22eadd8a0f27a552, RefType=CHILD_OF}', + ], + [], + false, + '', + 'Unset', + 'Client', + ], + [ + 1721304359836, + '53e1096a98361b25', + '000000000000000071dc9b0a338729b4', + 'redis', + 'GetDriver', + '3', + '32032000', + [ + 'param.driverID', + 'service.name', + 'signoz.collector.id', + 'client-uuid', + 'host.name', + 'ip', + 'opencensus.exporterversion', + ], + [ + 'T718678C', + 'redis', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '1b45eecbdc9fca62', + '4f6ec470feea', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=22eadd8a0f27a552, RefType=CHILD_OF}', + ], + [ + '{"name":"redis timeout","timeUnixNano":1721304359868650000,"attributeMap":{"driver_id":"T718678C","error":"redis timeout","level":"error"}}', + ], + true, + '', + 'Error', + 'Client', + ], + [ + 1721304359868, + '62f28274cd882905', + '000000000000000071dc9b0a338729b4', + 'redis', + 'GetDriver', + '3', + '9969000', + [ + 'client-uuid', + 'host.name', + 'ip', + 'opencensus.exporterversion', + 'param.driverID', + 'service.name', + 'signoz.collector.id', + ], + [ + '1b45eecbdc9fca62', + '4f6ec470feea', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + 'T718678C', + 'redis', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=22eadd8a0f27a552, RefType=CHILD_OF}', + ], + [], + false, + '', + 'Unset', + 'Client', + ], + [ + 1721304359878, + '273c24835b4733f7', + '000000000000000071dc9b0a338729b4', + 'redis', + 'GetDriver', + '3', + '6974000', + [ + 'opencensus.exporterversion', + 'param.driverID', + 'service.name', + 'signoz.collector.id', + 'client-uuid', + 'host.name', + 'ip', + ], + [ + 'Jaeger-Go-2.30.0', + 'T778818C', + 'redis', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '1b45eecbdc9fca62', + '4f6ec470feea', + '172.25.0.2', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=22eadd8a0f27a552, RefType=CHILD_OF}', + ], + [], + false, + '', + 'Unset', + 'Client', + ], + [ + 1721304359885, + '1fee566b7d89250d', + '000000000000000071dc9b0a338729b4', + 'redis', + 'GetDriver', + '3', + '11594000', + [ + 'host.name', + 'ip', + 'opencensus.exporterversion', + 'param.driverID', + 'service.name', + 'signoz.collector.id', + 'client-uuid', + ], + [ + '4f6ec470feea', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + 'T730205C', + 'redis', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '1b45eecbdc9fca62', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=22eadd8a0f27a552, RefType=CHILD_OF}', + ], + [], + false, + '', + 'Unset', + 'Client', + ], + [ + 1721304359897, + '6b7bfcbede8a2e10', + '000000000000000071dc9b0a338729b4', + 'redis', + 'GetDriver', + '3', + '9198000', + [ + 'client-uuid', + 'host.name', + 'ip', + 'opencensus.exporterversion', + 'param.driverID', + 'service.name', + 'signoz.collector.id', + ], + [ + '1b45eecbdc9fca62', + '4f6ec470feea', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + 'T708669C', + 'redis', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=22eadd8a0f27a552, RefType=CHILD_OF}', + ], + [], + false, + '', + 'Unset', + 'Client', + ], + [ + 1721304359906, + '707b2e18e9f4b36d', + '000000000000000071dc9b0a338729b4', + 'redis', + 'GetDriver', + '3', + '28706000', + [ + 'client-uuid', + 'host.name', + 'ip', + 'opencensus.exporterversion', + 'param.driverID', + 'service.name', + 'signoz.collector.id', + ], + [ + '1b45eecbdc9fca62', + '4f6ec470feea', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + 'T740153C', + 'redis', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=22eadd8a0f27a552, RefType=CHILD_OF}', + ], + [ + '{"name":"redis timeout","timeUnixNano":1721304359935174000,"attributeMap":{"driver_id":"T740153C","error":"redis timeout","level":"error"}}', + ], + true, + '', + 'Error', + 'Client', + ], + [ + 1721304359935, + '759867234892fa23', + '000000000000000071dc9b0a338729b4', + 'redis', + 'GetDriver', + '3', + '10115000', + [ + 'ip', + 'opencensus.exporterversion', + 'param.driverID', + 'service.name', + 'signoz.collector.id', + 'client-uuid', + 'host.name', + ], + [ + '172.25.0.2', + 'Jaeger-Go-2.30.0', + 'T740153C', + 'redis', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '1b45eecbdc9fca62', + '4f6ec470feea', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=22eadd8a0f27a552, RefType=CHILD_OF}', + ], + [], + false, + '', + 'Unset', + 'Client', + ], + [ + 1721304359945, + '40e021cf08a5849d', + '000000000000000071dc9b0a338729b4', + 'redis', + 'GetDriver', + '3', + '13283000', + [ + 'client-uuid', + 'host.name', + 'ip', + 'opencensus.exporterversion', + 'param.driverID', + 'service.name', + 'signoz.collector.id', + ], + [ + '1b45eecbdc9fca62', + '4f6ec470feea', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + 'T739323C', + 'redis', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=22eadd8a0f27a552, RefType=CHILD_OF}', + ], + [], + false, + '', + 'Unset', + 'Client', + ], + [ + 1721304359958, + '53a1db7583207833', + '000000000000000071dc9b0a338729b4', + 'redis', + 'GetDriver', + '3', + '7573000', + [ + 'opencensus.exporterversion', + 'param.driverID', + 'service.name', + 'signoz.collector.id', + 'client-uuid', + 'host.name', + 'ip', + ], + [ + 'Jaeger-Go-2.30.0', + 'T731135C', + 'redis', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '1b45eecbdc9fca62', + '4f6ec470feea', + '172.25.0.2', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=22eadd8a0f27a552, RefType=CHILD_OF}', + ], + [], + false, + '', + 'Unset', + 'Client', + ], + [ + 1721304359966, + '46aaaa6f2a2fc84f', + '000000000000000071dc9b0a338729b4', + 'redis', + 'GetDriver', + '3', + '9238000', + [ + 'param.driverID', + 'service.name', + 'signoz.collector.id', + 'client-uuid', + 'host.name', + 'ip', + 'opencensus.exporterversion', + ], + [ + 'T760263C', + 'redis', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '1b45eecbdc9fca62', + '4f6ec470feea', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=22eadd8a0f27a552, RefType=CHILD_OF}', + ], + [], + false, + '', + 'Unset', + 'Client', + ], + [ + 1721304359803, + '22eadd8a0f27a552', + '000000000000000071dc9b0a338729b4', + 'driver', + '/driver.DriverService/FindNearest', + '2', + '172564000', + [ + 'opencensus.exporterversion', + 'service.name', + 'signoz.collector.id', + 'client-uuid', + 'component', + 'host.name', + 'ip', + ], + [ + 'Jaeger-Go-2.30.0', + 'driver', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '46ade24719771fcb', + 'gRPC', + '4f6ec470feea', + '172.25.0.2', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=0b67082081caa21d, RefType=CHILD_OF}', + ], + [ + '{"name":"Searching for nearby drivers","timeUnixNano":1721304359803853000,"attributeMap":{"level":"info","location":"728,326"}}', + '{"name":"Retrying GetDriver after error","timeUnixNano":1721304359868728000,"attributeMap":{"error":"redis timeout","level":"error","retry_no":"1"}}', + '{"name":"Retrying GetDriver after error","timeUnixNano":1721304359935265000,"attributeMap":{"error":"redis timeout","level":"error","retry_no":"1"}}', + '{"name":"Search successful","timeUnixNano":1721304359975617000,"attributeMap":{"level":"info","num_drivers":"10"}}', + ], + false, + '', + 'Unset', + 'Server', + ], + [ + 1721304359803, + '0b67082081caa21d', + '000000000000000071dc9b0a338729b4', + 'frontend', + '/driver.DriverService/FindNearest', + '3', + '173105000', + [ + 'component', + 'host.name', + 'ip', + 'opencensus.exporterversion', + 'service.name', + 'signoz.collector.id', + 'client-uuid', + ], + [ + 'gRPC', + '4f6ec470feea', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + 'frontend', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '6fb81b8ca91b2b4d', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=71dc9b0a338729b4, RefType=CHILD_OF}', + ], + [], + false, + '', + 'Unset', + 'Client', + ], + [ + 1721304359978, + '3ed7b128c9d765bf', + '000000000000000071dc9b0a338729b4', + 'frontend', + 'HTTP GET', + '3', + '54072000', + [ + 'component', + 'http.method', + 'http.status_code', + 'http.url', + 'net/http.reused', + 'net/http.was_idle', + 'service.name', + 'client-uuid', + 'signoz.collector.id', + 'ip', + 'opencensus.exporterversion', + 'host.name', + ], + [ + 'net/http', + 'GET', + '200', + '0.0.0.0:8083', + 'true', + 'true', + 'frontend', + '6fb81b8ca91b2b4d', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + '4f6ec470feea', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=0ac9396a0836beef, RefType=CHILD_OF}', + ], + [ + '{"name":"GetConn","timeUnixNano":1721304359978461000}', + '{"name":"GotConn","timeUnixNano":1721304359978470000}', + '{"name":"WroteHeaders","timeUnixNano":1721304359978490000}', + '{"name":"WroteRequest","timeUnixNano":1721304359978492000}', + '{"name":"GotFirstResponseByte","timeUnixNano":1721304360032438000}', + '{"name":"PutIdleConn","timeUnixNano":1721304360032487000}', + '{"name":"ClosedBody","timeUnixNano":1721304360032511000}', + ], + false, + '', + 'Unset', + 'Client', + ], + [ + 1721304359978, + '0ac9396a0836beef', + '000000000000000071dc9b0a338729b4', + 'frontend', + 'HTTP GET: /route', + '0', + '54081000', + [ + 'service.name', + 'signoz.collector.id', + 'client-uuid', + 'host.name', + 'ip', + 'opencensus.exporterversion', + ], + [ + 'frontend', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '6fb81b8ca91b2b4d', + '4f6ec470feea', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=71dc9b0a338729b4, RefType=CHILD_OF}', + ], + [], + false, + '', + 'Unset', + 'Unspecified', + ], + [ + 1721304359978, + '2b837048c74fc169', + '000000000000000071dc9b0a338729b4', + 'frontend', + 'HTTP GET', + '3', + '64945000', + [ + 'ip', + 'service.name', + 'client-uuid', + 'component', + 'http.method', + 'http.status_code', + 'opencensus.exporterversion', + 'signoz.collector.id', + 'host.name', + 'http.url', + 'net/http.reused', + 'net/http.was_idle', + ], + [ + '172.25.0.2', + 'frontend', + '6fb81b8ca91b2b4d', + 'net/http', + 'GET', + '200', + 'Jaeger-Go-2.30.0', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '4f6ec470feea', + '0.0.0.0:8083', + 'false', + 'false', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=1e320f1c93026e9d, RefType=CHILD_OF}', + ], + [ + '{"name":"GetConn","timeUnixNano":1721304359979001000}', + '{"name":"ConnectStart","timeUnixNano":1721304359979065000,"attributeMap":{"addr":"0.0.0.0:8083","network":"tcp"}}', + '{"name":"ConnectDone","timeUnixNano":1721304359979262000,"attributeMap":{"addr":"0.0.0.0:8083","network":"tcp"}}', + '{"name":"GotConn","timeUnixNano":1721304359979287000}', + '{"name":"WroteHeaders","timeUnixNano":1721304359979341000}', + '{"name":"WroteRequest","timeUnixNano":1721304359979342000}', + '{"name":"GotFirstResponseByte","timeUnixNano":1721304360043858000}', + '{"name":"PutIdleConn","timeUnixNano":1721304360043897000}', + '{"name":"ClosedBody","timeUnixNano":1721304360043919000}', + ], + false, + '', + 'Unset', + 'Client', + ], + [ + 1721304359978, + '1e320f1c93026e9d', + '000000000000000071dc9b0a338729b4', + 'frontend', + 'HTTP GET: /route', + '0', + '64954000', + [ + 'client-uuid', + 'host.name', + 'ip', + 'opencensus.exporterversion', + 'service.name', + 'signoz.collector.id', + ], + [ + '6fb81b8ca91b2b4d', + '4f6ec470feea', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + 'frontend', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=71dc9b0a338729b4, RefType=CHILD_OF}', + ], + [], + false, + '', + 'Unset', + 'Unspecified', + ], + [ + 1721304359977, + '3d559df27da11f0f', + '000000000000000071dc9b0a338729b4', + 'frontend', + 'HTTP GET', + '3', + '76137000', + [ + 'net/http.reused', + 'net/http.was_idle', + 'component', + 'host.name', + 'http.url', + 'ip', + 'service.name', + 'signoz.collector.id', + 'client-uuid', + 'http.method', + 'http.status_code', + 'opencensus.exporterversion', + ], + [ + 'true', + 'true', + 'net/http', + '4f6ec470feea', + '0.0.0.0:8083', + '172.25.0.2', + 'frontend', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '6fb81b8ca91b2b4d', + 'GET', + '200', + 'Jaeger-Go-2.30.0', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=6acf3a3fd51f294a, RefType=CHILD_OF}', + ], + [ + '{"name":"GetConn","timeUnixNano":1721304359977905000}', + '{"name":"GotConn","timeUnixNano":1721304359977914000}', + '{"name":"WroteHeaders","timeUnixNano":1721304359977939000}', + '{"name":"WroteRequest","timeUnixNano":1721304359977941000}', + '{"name":"GotFirstResponseByte","timeUnixNano":1721304360053941000}', + '{"name":"PutIdleConn","timeUnixNano":1721304360053982000}', + '{"name":"ClosedBody","timeUnixNano":1721304360054009000}', + ], + false, + '', + 'Unset', + 'Client', + ], + [ + 1721304359977, + '6acf3a3fd51f294a', + '000000000000000071dc9b0a338729b4', + 'frontend', + 'HTTP GET: /route', + '0', + '76161000', + [ + 'host.name', + 'ip', + 'opencensus.exporterversion', + 'service.name', + 'signoz.collector.id', + 'client-uuid', + ], + [ + '4f6ec470feea', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + 'frontend', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '6fb81b8ca91b2b4d', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=71dc9b0a338729b4, RefType=CHILD_OF}', + ], + [], + false, + '', + 'Unset', + 'Unspecified', + ], + [ + 1721304360033, + '4f50f3f099502699', + '000000000000000071dc9b0a338729b4', + 'frontend', + 'HTTP GET', + '3', + '69683000', + [ + 'client-uuid', + 'component', + 'http.status_code', + 'http.url', + 'net/http.reused', + 'net/http.was_idle', + 'signoz.collector.id', + 'host.name', + 'http.method', + 'ip', + 'opencensus.exporterversion', + 'service.name', + ], + [ + '6fb81b8ca91b2b4d', + 'net/http', + '200', + '0.0.0.0:8083', + 'true', + 'true', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '4f6ec470feea', + 'GET', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + 'frontend', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=0f064e2af29e5e3d, RefType=CHILD_OF}', + ], + [ + '{"name":"GetConn","timeUnixNano":1721304360033336000}', + '{"name":"GotConn","timeUnixNano":1721304360033345000}', + '{"name":"WroteHeaders","timeUnixNano":1721304360033369000}', + '{"name":"WroteRequest","timeUnixNano":1721304360033372000}', + '{"name":"GotFirstResponseByte","timeUnixNano":1721304360102925000}', + '{"name":"PutIdleConn","timeUnixNano":1721304360102965000}', + '{"name":"ClosedBody","timeUnixNano":1721304360102988000}', + ], + false, + '', + 'Unset', + 'Client', + ], + [ + 1721304360033, + '0f064e2af29e5e3d', + '000000000000000071dc9b0a338729b4', + 'frontend', + 'HTTP GET: /route', + '0', + '69697000', + [ + 'client-uuid', + 'host.name', + 'ip', + 'opencensus.exporterversion', + 'service.name', + 'signoz.collector.id', + ], + [ + '6fb81b8ca91b2b4d', + '4f6ec470feea', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + 'frontend', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=71dc9b0a338729b4, RefType=CHILD_OF}', + ], + [], + false, + '', + 'Unset', + 'Unspecified', + ], + [ + 1721304360054, + '1968d4e502b91ac4', + '000000000000000071dc9b0a338729b4', + 'frontend', + 'HTTP GET', + '3', + '54187000', + [ + 'client-uuid', + 'component', + 'http.status_code', + 'net/http.was_idle', + 'opencensus.exporterversion', + 'signoz.collector.id', + 'host.name', + 'http.method', + 'http.url', + 'ip', + 'net/http.reused', + 'service.name', + ], + [ + '6fb81b8ca91b2b4d', + 'net/http', + '200', + 'true', + 'Jaeger-Go-2.30.0', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '4f6ec470feea', + 'GET', + '0.0.0.0:8083', + '172.25.0.2', + 'true', + 'frontend', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=4984e1953fd99760, RefType=CHILD_OF}', + ], + [ + '{"name":"GetConn","timeUnixNano":1721304360054823000}', + '{"name":"GotConn","timeUnixNano":1721304360054831000}', + '{"name":"WroteHeaders","timeUnixNano":1721304360054855000}', + '{"name":"WroteRequest","timeUnixNano":1721304360054857000}', + '{"name":"GotFirstResponseByte","timeUnixNano":1721304360108921000}', + '{"name":"PutIdleConn","timeUnixNano":1721304360108961000}', + '{"name":"ClosedBody","timeUnixNano":1721304360108984000}', + ], + false, + '', + 'Unset', + 'Client', + ], + [ + 1721304360054, + '4984e1953fd99760', + '000000000000000071dc9b0a338729b4', + 'frontend', + 'HTTP GET: /route', + '0', + '54200000', + [ + 'client-uuid', + 'host.name', + 'ip', + 'opencensus.exporterversion', + 'service.name', + 'signoz.collector.id', + ], + [ + '6fb81b8ca91b2b4d', + '4f6ec470feea', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + 'frontend', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=71dc9b0a338729b4, RefType=CHILD_OF}', + ], + [], + false, + '', + 'Unset', + 'Unspecified', + ], + [ + 1721304360044, + '032263185bbbf38c', + '000000000000000071dc9b0a338729b4', + 'frontend', + 'HTTP GET', + '3', + '69252000', + [ + 'host.name', + 'ip', + 'net/http.was_idle', + 'service.name', + 'client-uuid', + 'component', + 'http.method', + 'http.status_code', + 'http.url', + 'net/http.reused', + 'opencensus.exporterversion', + 'signoz.collector.id', + ], + [ + '4f6ec470feea', + '172.25.0.2', + 'true', + 'frontend', + '6fb81b8ca91b2b4d', + 'net/http', + 'GET', + '200', + '0.0.0.0:8083', + 'true', + 'Jaeger-Go-2.30.0', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=54965682749b13bf, RefType=CHILD_OF}', + ], + [ + '{"name":"GetConn","timeUnixNano":1721304360044759000}', + '{"name":"GotConn","timeUnixNano":1721304360044768000}', + '{"name":"WroteHeaders","timeUnixNano":1721304360044793000}', + '{"name":"WroteRequest","timeUnixNano":1721304360044796000}', + '{"name":"GotFirstResponseByte","timeUnixNano":1721304360113908000}', + '{"name":"PutIdleConn","timeUnixNano":1721304360113948000}', + '{"name":"ClosedBody","timeUnixNano":1721304360113971000}', + ], + false, + '', + 'Unset', + 'Client', + ], + [ + 1721304360044, + '54965682749b13bf', + '000000000000000071dc9b0a338729b4', + 'frontend', + 'HTTP GET: /route', + '0', + '69266000', + [ + 'client-uuid', + 'host.name', + 'ip', + 'opencensus.exporterversion', + 'service.name', + 'signoz.collector.id', + ], + [ + '6fb81b8ca91b2b4d', + '4f6ec470feea', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + 'frontend', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=71dc9b0a338729b4, RefType=CHILD_OF}', + ], + [], + false, + '', + 'Unset', + 'Unspecified', + ], + [ + 1721304360109, + '255866d4d4b6ec08', + '000000000000000071dc9b0a338729b4', + 'frontend', + 'HTTP GET', + '3', + '41317000', + [ + 'component', + 'host.name', + 'http.method', + 'http.status_code', + 'net/http.reused', + 'net/http.was_idle', + 'opencensus.exporterversion', + 'client-uuid', + 'http.url', + 'ip', + 'service.name', + 'signoz.collector.id', + ], + [ + 'net/http', + '4f6ec470feea', + 'GET', + '200', + 'true', + 'true', + 'Jaeger-Go-2.30.0', + '6fb81b8ca91b2b4d', + '0.0.0.0:8083', + '172.25.0.2', + 'frontend', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=690f0b793f302403, RefType=CHILD_OF}', + ], + [ + '{"name":"GetConn","timeUnixNano":1721304360109790000}', + '{"name":"GotConn","timeUnixNano":1721304360109798000}', + '{"name":"WroteHeaders","timeUnixNano":1721304360109823000}', + '{"name":"WroteRequest","timeUnixNano":1721304360109826000}', + '{"name":"GotFirstResponseByte","timeUnixNano":1721304360151018000}', + '{"name":"PutIdleConn","timeUnixNano":1721304360151059000}', + '{"name":"ClosedBody","timeUnixNano":1721304360151081000}', + ], + false, + '', + 'Unset', + 'Client', + ], + [ + 1721304360109, + '690f0b793f302403', + '000000000000000071dc9b0a338729b4', + 'frontend', + 'HTTP GET: /route', + '0', + '41332000', + [ + 'service.name', + 'signoz.collector.id', + 'client-uuid', + 'host.name', + 'ip', + 'opencensus.exporterversion', + ], + [ + 'frontend', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '6fb81b8ca91b2b4d', + '4f6ec470feea', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=71dc9b0a338729b4, RefType=CHILD_OF}', + ], + [], + false, + '', + 'Unset', + 'Unspecified', + ], + [ + 1721304360103, + '26fbd8f91831c199', + '000000000000000071dc9b0a338729b4', + 'frontend', + 'HTTP GET', + '3', + '48308000', + [ + 'http.method', + 'ip', + 'opencensus.exporterversion', + 'service.name', + 'signoz.collector.id', + 'client-uuid', + 'component', + 'http.url', + 'net/http.reused', + 'net/http.was_idle', + 'host.name', + 'http.status_code', + ], + [ + 'GET', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + 'frontend', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '6fb81b8ca91b2b4d', + 'net/http', + '0.0.0.0:8083', + 'true', + 'true', + '4f6ec470feea', + '200', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=55e1875d184a70fe, RefType=CHILD_OF}', + ], + [ + '{"name":"GetConn","timeUnixNano":1721304360103810000}', + '{"name":"GotConn","timeUnixNano":1721304360103818000}', + '{"name":"WroteHeaders","timeUnixNano":1721304360103842000}', + '{"name":"WroteRequest","timeUnixNano":1721304360103844000}', + '{"name":"GotFirstResponseByte","timeUnixNano":1721304360152042000}', + '{"name":"PutIdleConn","timeUnixNano":1721304360152070000}', + '{"name":"ClosedBody","timeUnixNano":1721304360152091000}', + ], + false, + '', + 'Unset', + 'Client', + ], + [ + 1721304360103, + '55e1875d184a70fe', + '000000000000000071dc9b0a338729b4', + 'frontend', + 'HTTP GET: /route', + '0', + '48325000', + [ + 'client-uuid', + 'host.name', + 'ip', + 'opencensus.exporterversion', + 'service.name', + 'signoz.collector.id', + ], + [ + '6fb81b8ca91b2b4d', + '4f6ec470feea', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + 'frontend', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=71dc9b0a338729b4, RefType=CHILD_OF}', + ], + [], + false, + '', + 'Unset', + 'Unspecified', + ], + [ + 1721304360114, + '2903e6c8cbe5f8c6', + '000000000000000071dc9b0a338729b4', + 'frontend', + 'HTTP GET', + '3', + '65640000', + [ + 'http.status_code', + 'ip', + 'signoz.collector.id', + 'client-uuid', + 'component', + 'http.method', + 'net/http.was_idle', + 'opencensus.exporterversion', + 'service.name', + 'host.name', + 'http.url', + 'net/http.reused', + ], + [ + '200', + '172.25.0.2', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '6fb81b8ca91b2b4d', + 'net/http', + 'GET', + 'true', + 'Jaeger-Go-2.30.0', + 'frontend', + '4f6ec470feea', + '0.0.0.0:8083', + 'true', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=5bcb6dfccf311afe, RefType=CHILD_OF}', + ], + [ + '{"name":"GetConn","timeUnixNano":1721304360114775000}', + '{"name":"GotConn","timeUnixNano":1721304360114783000}', + '{"name":"WroteHeaders","timeUnixNano":1721304360114809000}', + '{"name":"WroteRequest","timeUnixNano":1721304360114812000}', + '{"name":"GotFirstResponseByte","timeUnixNano":1721304360180281000}', + '{"name":"PutIdleConn","timeUnixNano":1721304360180362000}', + '{"name":"ClosedBody","timeUnixNano":1721304360180389000}', + ], + false, + '', + 'Unset', + 'Client', + ], + [ + 1721304360114, + '5bcb6dfccf311afe', + '000000000000000071dc9b0a338729b4', + 'frontend', + 'HTTP GET: /route', + '0', + '65664000', + [ + 'client-uuid', + 'host.name', + 'ip', + 'opencensus.exporterversion', + 'service.name', + 'signoz.collector.id', + ], + [ + '6fb81b8ca91b2b4d', + '4f6ec470feea', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + 'frontend', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=71dc9b0a338729b4, RefType=CHILD_OF}', + ], + [], + false, + '', + 'Unset', + 'Unspecified', + ], + [ + 1721304360151, + '3ed0b66560a03e14', + '000000000000000071dc9b0a338729b4', + 'frontend', + 'HTTP GET', + '3', + '78546000', + [ + 'http.status_code', + 'service.name', + 'signoz.collector.id', + 'client-uuid', + 'component', + 'host.name', + 'http.method', + 'http.url', + 'ip', + 'net/http.reused', + 'net/http.was_idle', + 'opencensus.exporterversion', + ], + [ + '200', + 'frontend', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '6fb81b8ca91b2b4d', + 'net/http', + '4f6ec470feea', + 'GET', + '0.0.0.0:8083', + '172.25.0.2', + 'true', + 'true', + 'Jaeger-Go-2.30.0', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=352ac0b61c8c826f, RefType=CHILD_OF}', + ], + [ + '{"name":"GetConn","timeUnixNano":1721304360151933000}', + '{"name":"GotConn","timeUnixNano":1721304360151942000}', + '{"name":"WroteHeaders","timeUnixNano":1721304360151967000}', + '{"name":"WroteRequest","timeUnixNano":1721304360151969000}', + '{"name":"GotFirstResponseByte","timeUnixNano":1721304360230321000}', + '{"name":"PutIdleConn","timeUnixNano":1721304360230365000}', + '{"name":"ClosedBody","timeUnixNano":1721304360230449000}', + ], + false, + '', + 'Unset', + 'Client', + ], + [ + 1721304360151, + '352ac0b61c8c826f', + '000000000000000071dc9b0a338729b4', + 'frontend', + 'HTTP GET: /route', + '0', + '78561000', + [ + 'client-uuid', + 'host.name', + 'ip', + 'opencensus.exporterversion', + 'service.name', + 'signoz.collector.id', + ], + [ + '6fb81b8ca91b2b4d', + '4f6ec470feea', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + 'frontend', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=71dc9b0a338729b4, RefType=CHILD_OF}', + ], + [], + false, + '', + 'Unset', + 'Unspecified', + ], + [ + 1721304359453, + '71dc9b0a338729b4', + '000000000000000071dc9b0a338729b4', + 'frontend', + 'HTTP GET /dispatch', + '2', + '776756000', + [ + 'http.status_code', + 'http.url', + 'opencensus.exporterversion', + 'sampler.param', + 'client-uuid', + 'component', + 'host.name', + 'http.method', + 'signoz.collector.id', + 'ip', + 'sampler.type', + 'service.name', + ], + [ + '200', + '/dispatch?customer=731\u0026nonse=0.8022286220408668', + 'Jaeger-Go-2.30.0', + 'true', + '6fb81b8ca91b2b4d', + 'net/http', + '4f6ec470feea', + 'GET', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '172.25.0.2', + 'const', + 'frontend', + ], + ['{TraceId=000000000000000071dc9b0a338729b4, SpanId=, RefType=CHILD_OF}'], + [ + '{"name":"HTTP request received","timeUnixNano":1721304359453904000,"attributeMap":{"level":"info","method":"GET","url":"/dispatch?customer=731\\u0026nonse=0.8022286220408668"}}', + '{"name":"Getting customer","timeUnixNano":1721304359453990000,"attributeMap":{"customer_id":"731","level":"info"}}', + '{"name":"Found customer","timeUnixNano":1721304359803486000,"attributeMap":{"level":"info"}}', + '{"name":"baggage","timeUnixNano":1721304359803546000,"attributeMap":{"key":"customer","value":"Japanese Desserts"}}', + '{"name":"Finding nearest drivers","timeUnixNano":1721304359803551000,"attributeMap":{"level":"info","location":"728,326"}}', + '{"name":"Found drivers","timeUnixNano":1721304359976695000,"attributeMap":{"level":"info"}}', + '{"name":"Finding route","timeUnixNano":1721304359977409000,"attributeMap":{"dropoff":"728,326","level":"info","pickup":"165,543"}}', + '{"name":"Finding route","timeUnixNano":1721304359977989000,"attributeMap":{"dropoff":"728,326","level":"info","pickup":"720,72"}}', + '{"name":"Finding route","timeUnixNano":1721304359978530000,"attributeMap":{"dropoff":"728,326","level":"info","pickup":"530,911"}}', + '{"name":"Finding route","timeUnixNano":1721304360032532000,"attributeMap":{"dropoff":"728,326","level":"info","pickup":"686,946"}}', + '{"name":"Finding route","timeUnixNano":1721304360043941000,"attributeMap":{"dropoff":"728,326","level":"info","pickup":"222,675"}}', + '{"name":"Finding route","timeUnixNano":1721304360054028000,"attributeMap":{"dropoff":"728,326","level":"info","pickup":"660,997"}}', + '{"name":"Finding route","timeUnixNano":1721304360103005000,"attributeMap":{"dropoff":"728,326","level":"info","pickup":"609,233"}}', + '{"name":"Finding route","timeUnixNano":1721304360109005000,"attributeMap":{"dropoff":"728,326","level":"info","pickup":"516,970"}}', + '{"name":"Finding route","timeUnixNano":1721304360113986000,"attributeMap":{"dropoff":"728,326","level":"info","pickup":"524,763"}}', + '{"name":"Finding route","timeUnixNano":1721304360151099000,"attributeMap":{"dropoff":"728,326","level":"info","pickup":"267,822"}}', + '{"name":"Found routes","timeUnixNano":1721304360230473000,"attributeMap":{"level":"info"}}', + '{"name":"Dispatch successful","timeUnixNano":1721304360230582000,"attributeMap":{"driver":"T718678C","eta":"2m0s","level":"info"}}', + ], + false, + '', + 'Unset', + 'Server', + ], + [ + 1721304359980, + '472ffaa577ce6b0e', + '000000000000000071dc9b0a338729b4', + 'route', + 'HTTP GET /route', + '2', + '52268000', + [ + 'component', + 'host.name', + 'http.url', + 'opencensus.exporterversion', + 'service.name', + 'client-uuid', + 'http.method', + 'http.status_code', + 'ip', + 'signoz.collector.id', + ], + [ + 'net/http', + '4f6ec470feea', + '/route?dropoff=728%2C326\u0026pickup=720%2C72', + 'Jaeger-Go-2.30.0', + 'route', + '64a18ffd5f8adbfb', + 'GET', + '200', + '172.25.0.2', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=3ed7b128c9d765bf, RefType=CHILD_OF}', + ], + [ + '{"name":"HTTP request received","timeUnixNano":1721304359980063000,"attributeMap":{"level":"info","method":"GET","url":"/route?dropoff=728%2C326\\u0026pickup=720%2C72"}}', + ], + false, + '', + 'Unset', + 'Server', + ], + [ + 1721304359979, + '2a835737138b4671', + '000000000000000071dc9b0a338729b4', + 'route', + 'HTTP GET /route', + '2', + '64209000', + [ + 'service.name', + 'signoz.collector.id', + 'client-uuid', + 'component', + 'host.name', + 'http.url', + 'opencensus.exporterversion', + 'http.method', + 'http.status_code', + 'ip', + ], + [ + 'route', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '64a18ffd5f8adbfb', + 'net/http', + '4f6ec470feea', + '/route?dropoff=728%2C326\u0026pickup=530%2C911', + 'Jaeger-Go-2.30.0', + 'GET', + '200', + '172.25.0.2', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=2b837048c74fc169, RefType=CHILD_OF}', + ], + [ + '{"name":"HTTP request received","timeUnixNano":1721304359979511000,"attributeMap":{"level":"info","method":"GET","url":"/route?dropoff=728%2C326\\u0026pickup=530%2C911"}}', + ], + false, + '', + 'Unset', + 'Server', + ], + [ + 1721304359980, + '28a8a67365d0bd8b', + '000000000000000071dc9b0a338729b4', + 'route', + 'HTTP GET /route', + '2', + '73145000', + [ + 'http.method', + 'http.status_code', + 'opencensus.exporterversion', + 'signoz.collector.id', + 'component', + 'host.name', + 'ip', + 'service.name', + 'client-uuid', + 'http.url', + ], + [ + 'GET', + '200', + 'Jaeger-Go-2.30.0', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + 'net/http', + '4f6ec470feea', + '172.25.0.2', + 'route', + '64a18ffd5f8adbfb', + '/route?dropoff=728%2C326\u0026pickup=165%2C543', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=3d559df27da11f0f, RefType=CHILD_OF}', + ], + [ + '{"name":"HTTP request received","timeUnixNano":1721304359980599000,"attributeMap":{"level":"info","method":"GET","url":"/route?dropoff=728%2C326\\u0026pickup=165%2C543"}}', + ], + false, + '', + 'Unset', + 'Server', + ], + [ + 1721304360033, + '38de18f04671e5db', + '000000000000000071dc9b0a338729b4', + 'route', + 'HTTP GET /route', + '2', + '69230000', + [ + 'http.status_code', + 'ip', + 'opencensus.exporterversion', + 'service.name', + 'signoz.collector.id', + 'client-uuid', + 'component', + 'http.method', + 'host.name', + 'http.url', + ], + [ + '200', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + 'route', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '64a18ffd5f8adbfb', + 'net/http', + 'GET', + '4f6ec470feea', + '/route?dropoff=728%2C326\u0026pickup=686%2C946', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=4f50f3f099502699, RefType=CHILD_OF}', + ], + [ + '{"name":"HTTP request received","timeUnixNano":1721304360033555000,"attributeMap":{"level":"info","method":"GET","url":"/route?dropoff=728%2C326\\u0026pickup=686%2C946"}}', + ], + false, + '', + 'Unset', + 'Server', + ], + [ + 1721304360054, + '3420998a476237ce', + '000000000000000071dc9b0a338729b4', + 'route', + 'HTTP GET /route', + '2', + '53811000', + [ + 'client-uuid', + 'host.name', + 'http.status_code', + 'http.url', + 'opencensus.exporterversion', + 'service.name', + 'component', + 'http.method', + 'ip', + 'signoz.collector.id', + ], + [ + '64a18ffd5f8adbfb', + '4f6ec470feea', + '200', + '/route?dropoff=728%2C326\u0026pickup=660%2C997', + 'Jaeger-Go-2.30.0', + 'route', + 'net/http', + 'GET', + '172.25.0.2', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=1968d4e502b91ac4, RefType=CHILD_OF}', + ], + [ + '{"name":"HTTP request received","timeUnixNano":1721304360054989000,"attributeMap":{"level":"info","method":"GET","url":"/route?dropoff=728%2C326\\u0026pickup=660%2C997"}}', + ], + false, + '', + 'Unset', + 'Server', + ], + [ + 1721304360044, + '0c5a7edd2e365047', + '000000000000000071dc9b0a338729b4', + 'route', + 'HTTP GET /route', + '2', + '68850000', + [ + 'opencensus.exporterversion', + 'service.name', + 'signoz.collector.id', + 'client-uuid', + 'ip', + 'http.method', + 'http.status_code', + 'http.url', + 'component', + 'host.name', + ], + [ + 'Jaeger-Go-2.30.0', + 'route', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '64a18ffd5f8adbfb', + '172.25.0.2', + 'GET', + '200', + '/route?dropoff=728%2C326\u0026pickup=222%2C675', + 'net/http', + '4f6ec470feea', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=032263185bbbf38c, RefType=CHILD_OF}', + ], + [ + '{"name":"HTTP request received","timeUnixNano":1721304360044934000,"attributeMap":{"level":"info","method":"GET","url":"/route?dropoff=728%2C326\\u0026pickup=222%2C675"}}', + ], + false, + '', + 'Unset', + 'Server', + ], + [ + 1721304360103, + '538b56106b402474', + '000000000000000071dc9b0a338729b4', + 'route', + 'HTTP GET /route', + '2', + '46813000', + [ + 'http.url', + 'opencensus.exporterversion', + 'client-uuid', + 'component', + 'host.name', + 'http.method', + 'http.status_code', + 'ip', + 'service.name', + 'signoz.collector.id', + ], + [ + '/route?dropoff=728%2C326\u0026pickup=609%2C233', + 'Jaeger-Go-2.30.0', + '64a18ffd5f8adbfb', + 'net/http', + '4f6ec470feea', + 'GET', + '200', + '172.25.0.2', + 'route', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=26fbd8f91831c199, RefType=CHILD_OF}', + ], + [ + '{"name":"HTTP request received","timeUnixNano":1721304360103976000,"attributeMap":{"level":"info","method":"GET","url":"/route?dropoff=728%2C326\\u0026pickup=609%2C233"}}', + ], + false, + '', + 'Unset', + 'Server', + ], + [ + 1721304360109, + '668e5fc06e78796b', + '000000000000000071dc9b0a338729b4', + 'route', + 'HTTP GET /route', + '2', + '41012000', + [ + 'component', + 'http.method', + 'service.name', + 'signoz.collector.id', + 'client-uuid', + 'host.name', + 'http.status_code', + 'http.url', + 'ip', + 'opencensus.exporterversion', + ], + [ + 'net/http', + 'GET', + 'route', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '64a18ffd5f8adbfb', + '4f6ec470feea', + '200', + '/route?dropoff=728%2C326\u0026pickup=516%2C970', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=255866d4d4b6ec08, RefType=CHILD_OF}', + ], + [ + '{"name":"HTTP request received","timeUnixNano":1721304360109961000,"attributeMap":{"level":"info","method":"GET","url":"/route?dropoff=728%2C326\\u0026pickup=516%2C970"}}', + ], + false, + '', + 'Unset', + 'Server', + ], + [ + 1721304360114, + '7eda7467eedf3b92', + '000000000000000071dc9b0a338729b4', + 'route', + 'HTTP GET /route', + '2', + '65157000', + [ + 'host.name', + 'ip', + 'service.name', + 'http.status_code', + 'http.url', + 'opencensus.exporterversion', + 'signoz.collector.id', + 'client-uuid', + 'component', + 'http.method', + ], + [ + '4f6ec470feea', + '172.25.0.2', + 'route', + '200', + '/route?dropoff=728%2C326\u0026pickup=524%2C763', + 'Jaeger-Go-2.30.0', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '64a18ffd5f8adbfb', + 'net/http', + 'GET', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=2903e6c8cbe5f8c6, RefType=CHILD_OF}', + ], + [ + '{"name":"HTTP request received","timeUnixNano":1721304360114939000,"attributeMap":{"level":"info","method":"GET","url":"/route?dropoff=728%2C326\\u0026pickup=524%2C763"}}', + ], + false, + '', + 'Unset', + 'Server', + ], + [ + 1721304360152, + '231d5099ef1d826f', + '000000000000000071dc9b0a338729b4', + 'route', + 'HTTP GET /route', + '2', + '77985000', + [ + 'host.name', + 'http.method', + 'http.status_code', + 'component', + 'http.url', + 'ip', + 'opencensus.exporterversion', + 'service.name', + 'signoz.collector.id', + 'client-uuid', + ], + [ + '4f6ec470feea', + 'GET', + '200', + 'net/http', + '/route?dropoff=728%2C326\u0026pickup=267%2C822', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + 'route', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '64a18ffd5f8adbfb', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=3ed0b66560a03e14, RefType=CHILD_OF}', + ], + [ + '{"name":"HTTP request received","timeUnixNano":1721304360152182000,"attributeMap":{"level":"info","method":"GET","url":"/route?dropoff=728%2C326\\u0026pickup=267%2C822"}}', + ], + false, + '', + 'Unset', + 'Server', + ], + ], + isSubTree: false, + }, +]; diff --git a/frontend/src/mocks-server/handlers.ts b/frontend/src/mocks-server/handlers.ts index 87287a0d1a..55b5842612 100644 --- a/frontend/src/mocks-server/handlers.ts +++ b/frontend/src/mocks-server/handlers.ts @@ -13,6 +13,7 @@ import { membersResponse } from './__mockdata__/members'; import { queryRangeSuccessResponse } from './__mockdata__/query_range'; import { serviceSuccessResponse } from './__mockdata__/services'; import { topLevelOperationSuccessResponse } from './__mockdata__/top_level_operations'; +import { traceDetailResponse } from './__mockdata__/tracedetail'; export const handlers = [ rest.post('http://localhost/api/v3/query_range', (req, res, ctx) => @@ -210,6 +211,16 @@ export const handlers = [ res(ctx.status(200), ctx.json(explorerView)), ), + rest.post('http://localhost/api/v1/explorer/views', (req, res, ctx) => + res( + ctx.status(200), + ctx.json({ + status: 'success', + data: '7731ece1-3fa3-4ed4-8b1c-58b4c28723b2', + }), + ), + ), + rest.post('http://localhost/api/v1/event', (req, res, ctx) => res( ctx.status(200), @@ -220,6 +231,12 @@ export const handlers = [ }), ), ), + + rest.get( + 'http://localhost/api/v1/traces/000000000000000071dc9b0a338729b4', + (req, res, ctx) => res(ctx.status(200), ctx.json(traceDetailResponse)), + ), + rest.post('http://localhost/api/v1//channels', (_, res, ctx) => res(ctx.status(200), ctx.json(allAlertChannels)), ), diff --git a/frontend/src/pages/LogsExplorer/index.tsx b/frontend/src/pages/LogsExplorer/index.tsx index d4422f58fe..8873d04e39 100644 --- a/frontend/src/pages/LogsExplorer/index.tsx +++ b/frontend/src/pages/LogsExplorer/index.tsx @@ -9,7 +9,7 @@ import RightToolbarActions from 'container/QueryBuilder/components/ToolbarAction import Toolbar from 'container/Toolbar/Toolbar'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback'; -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { DataSource } from 'types/common/queryBuilder'; import { WrapperStyled } from './styles'; @@ -23,6 +23,12 @@ function LogsExplorer(): JSX.Element { const { handleRunQuery, currentQuery } = useQueryBuilder(); + const listQueryKeyRef = useRef(); + + const chartQueryKeyRef = useRef(); + + const [isLoadingQueries, setIsLoadingQueries] = useState(false); + const handleToggleShowFrequencyChart = (): void => { setShowFrequencyChart(!showFrequencyChart); }; @@ -36,21 +42,34 @@ function LogsExplorer(): JSX.Element { if (currentQuery.builder.queryData.length > 1) { handleChangeSelectedView(SELECTED_VIEWS.QUERY_BUILDER); } - }, [currentQuery.builder.queryData.length]); + if ( + currentQuery.builder.queryData.length === 1 && + currentQuery.builder.queryData[0].groupBy.length > 0 + ) { + handleChangeSelectedView(SELECTED_VIEWS.QUERY_BUILDER); + } + }, [currentQuery.builder.queryData, currentQuery.builder.queryData.length]); const isMultipleQueries = useMemo( () => - currentQuery.builder.queryData.length > 1 || - currentQuery.builder.queryFormulas.length > 0, + currentQuery.builder.queryData?.length > 1 || + currentQuery.builder.queryFormulas?.length > 0, [currentQuery], ); + const isGroupByPresent = useMemo( + () => + currentQuery.builder.queryData?.length === 1 && + currentQuery.builder.queryData?.[0]?.groupBy?.length > 0, + [currentQuery.builder.queryData], + ); + const toolbarViews = useMemo( () => ({ search: { name: 'search', label: 'Search', - disabled: isMultipleQueries, + disabled: isMultipleQueries || isGroupByPresent, show: true, }, queryBuilder: { @@ -66,7 +85,7 @@ function LogsExplorer(): JSX.Element { show: false, }, }), - [isMultipleQueries], + [isGroupByPresent, isMultipleQueries], ); return ( @@ -82,7 +101,14 @@ function LogsExplorer(): JSX.Element { showFrequencyChart={showFrequencyChart} /> } - rightActions={} + rightActions={ + + } showOldCTA /> @@ -97,6 +123,9 @@ function LogsExplorer(): JSX.Element {
diff --git a/frontend/src/pages/MessagingQueues/MQCommon/MQCommon.styles.scss b/frontend/src/pages/MessagingQueues/MQCommon/MQCommon.styles.scss new file mode 100644 index 0000000000..426be83a43 --- /dev/null +++ b/frontend/src/pages/MessagingQueues/MQCommon/MQCommon.styles.scss @@ -0,0 +1,34 @@ +.coming-soon { + display: inline-flex; + padding: 4px 8px; + border-radius: 20px; + border: 1px solid rgba(173, 127, 88, 0.2); + background: rgba(173, 127, 88, 0.1); + justify-content: center; + align-items: center; + gap: 5px; + + &__text { + color: var(--text-sienna-400); + font-size: 10px; + font-weight: 500; + letter-spacing: -0.05px; + line-height: normal; + } + &__icon { + display: flex; + } +} + +.tooltip-overlay { + text-wrap: nowrap; + .ant-tooltip-inner { + width: max-content; + } +} + +.select-label-with-coming-soon { + display: flex; + align-items: center; + justify-content: space-between; +} diff --git a/frontend/src/pages/MessagingQueues/MQCommon/MQCommon.tsx b/frontend/src/pages/MessagingQueues/MQCommon/MQCommon.tsx new file mode 100644 index 0000000000..ce6fd1e96f --- /dev/null +++ b/frontend/src/pages/MessagingQueues/MQCommon/MQCommon.tsx @@ -0,0 +1,58 @@ +/* eslint-disable react/destructuring-assignment */ +import './MQCommon.styles.scss'; + +import { Color } from '@signozhq/design-tokens'; +import { Tooltip } from 'antd'; +import { DefaultOptionType } from 'antd/es/select'; +import { Info } from 'lucide-react'; + +export function ComingSoon(): JSX.Element { + return ( + + Join our Slack community for more details:{' '} + e.stopPropagation()} + > + SigNoz Community + +
+ } + placement="top" + overlayClassName="tooltip-overlay" + > +
+
Coming Soon
+
+ +
+
+ + ); +} + +export function SelectMaxTagPlaceholder( + omittedValues: Partial[], +): JSX.Element { + return ( + value).join(', ')}> + + {omittedValues.length} + + ); +} + +export function SelectLabelWithComingSoon({ + label, +}: { + label: string; +}): JSX.Element { + return ( +
+ {label} +
+ ); +} diff --git a/frontend/src/pages/MessagingQueues/MQDetailPage/MQDetailPage.tsx b/frontend/src/pages/MessagingQueues/MQDetailPage/MQDetailPage.tsx new file mode 100644 index 0000000000..f82a2f605d --- /dev/null +++ b/frontend/src/pages/MessagingQueues/MQDetailPage/MQDetailPage.tsx @@ -0,0 +1,84 @@ +import '../MessagingQueues.styles.scss'; + +import { Select, Typography } from 'antd'; +import ROUTES from 'constants/routes'; +import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2'; +import { ListMinus } from 'lucide-react'; +import { useHistory } from 'react-router-dom'; + +import { MessagingQueuesViewType } from '../MessagingQueuesUtils'; +import { SelectLabelWithComingSoon } from '../MQCommon/MQCommon'; +import MessagingQueuesDetails from '../MQDetails/MQDetails'; +import MessagingQueuesConfigOptions from '../MQGraph/MQConfigOptions'; +import MessagingQueuesGraph from '../MQGraph/MQGraph'; + +function MQDetailPage(): JSX.Element { + const history = useHistory(); + + return ( +
+
+ + history.push(ROUTES.MESSAGING_QUEUES)} + className="message-queue-text" + > + Messaging Queues + +
+
+
+ Kafka / views / + + Loading... + + ) : ( + No Consumer Groups found + ) + } + onChange={(value): void => { + handleConsumerGrpSearch(''); + setQueryParamsForConfigOptions( + value, + urlQuery, + history, + location, + QueryParams.consumerGrp, + ); + resetTabularConfigDetailsOnChange(); + }} + /> + + Loading... + + ) : ( + No Partitions found + ) + } + onChange={(value): void => { + handlePartitionSearch(''); + setQueryParamsForConfigOptions( + value, + urlQuery, + history, + location, + QueryParams.partition, + ); + resetTabularConfigDetailsOnChange(); + }} + /> +
+ +
+ ); +} + +export default MessagingQueuesConfigOptions; diff --git a/frontend/src/pages/MessagingQueues/MQGraph/MQGraph.tsx b/frontend/src/pages/MessagingQueues/MQGraph/MQGraph.tsx new file mode 100644 index 0000000000..cd7cdd74c4 --- /dev/null +++ b/frontend/src/pages/MessagingQueues/MQGraph/MQGraph.tsx @@ -0,0 +1,88 @@ +import { QueryParams } from 'constants/query'; +import { PANEL_TYPES } from 'constants/queryBuilder'; +import { ViewMenuAction } from 'container/GridCardLayout/config'; +import GridCard from 'container/GridCardLayout/GridCard'; +import { Card } from 'container/GridCardLayout/styles'; +import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory'; +import { useIsDarkMode } from 'hooks/useDarkMode'; +import useUrlQuery from 'hooks/useUrlQuery'; +import { useCallback, useMemo } from 'react'; +import { useDispatch } from 'react-redux'; +import { useHistory, useLocation } from 'react-router-dom'; +import { UpdateTimeInterval } from 'store/actions'; + +import { + getFiltersFromConfigOptions, + getWidgetQuery, + setSelectedTimelineQuery, +} from '../MessagingQueuesUtils'; + +function MessagingQueuesGraph(): JSX.Element { + const isDarkMode = useIsDarkMode(); + + const urlQuery = useUrlQuery(); + const consumerGrp = urlQuery.get(QueryParams.consumerGrp) || ''; + const topic = urlQuery.get(QueryParams.topic) || ''; + const partition = urlQuery.get(QueryParams.partition) || ''; + + const filterItems = useMemo( + () => getFiltersFromConfigOptions(consumerGrp, topic, partition), + [consumerGrp, topic, partition], + ); + + const widgetData = useMemo( + () => getWidgetQueryBuilder(getWidgetQuery({ filterItems })), + [filterItems], + ); + const history = useHistory(); + const location = useLocation(); + + const messagingQueueCustomTooltipText = (): HTMLDivElement => { + const customText = document.createElement('div'); + customText.textContent = 'Click on co-ordinate to view details'; + customText.style.paddingTop = '8px'; + customText.style.paddingBottom = '2px'; + customText.style.color = '#fff'; + return customText; + }; + + const { pathname } = useLocation(); + const dispatch = useDispatch(); + + const onDragSelect = useCallback( + (start: number, end: number) => { + const startTimestamp = Math.trunc(start); + const endTimestamp = Math.trunc(end); + + urlQuery.set(QueryParams.startTime, startTimestamp.toString()); + urlQuery.set(QueryParams.endTime, endTimestamp.toString()); + const generatedUrl = `${pathname}?${urlQuery.toString()}`; + history.push(generatedUrl); + + if (startTimestamp !== endTimestamp) { + dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp])); + } + }, + [dispatch, history, pathname, urlQuery], + ); + + return ( + + { + setSelectedTimelineQuery(urlQuery, xValue, location, history, data); + }} + onDragSelect={onDragSelect} + customTooltipElement={messagingQueueCustomTooltipText()} + /> + + ); +} + +export default MessagingQueuesGraph; diff --git a/frontend/src/pages/MessagingQueues/MQGraph/useGetAllConfigOptions.ts b/frontend/src/pages/MessagingQueues/MQGraph/useGetAllConfigOptions.ts new file mode 100644 index 0000000000..c11c0f85f5 --- /dev/null +++ b/frontend/src/pages/MessagingQueues/MQGraph/useGetAllConfigOptions.ts @@ -0,0 +1,49 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { DefaultOptionType } from 'antd/es/select'; +import { getAttributesValues } from 'api/queryBuilder/getAttributesValues'; +import { useQuery } from 'react-query'; +import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { DataSource } from 'types/common/queryBuilder'; + +export interface ConfigOptions { + attributeKey: string; + searchText?: string; +} + +export interface GetAllConfigOptionsResponse { + options: DefaultOptionType[]; + isFetching: boolean; +} + +export function useGetAllConfigOptions( + props: ConfigOptions, +): GetAllConfigOptionsResponse { + const { attributeKey, searchText } = props; + + const { data, isLoading } = useQuery( + ['attributesValues', attributeKey, searchText], + async () => { + const { payload } = await getAttributesValues({ + aggregateOperator: 'avg', + dataSource: DataSource.METRICS, + aggregateAttribute: 'kafka_consumer_group_lag', + attributeKey, + searchText: searchText ?? '', + filterAttributeKeyDataType: DataTypes.String, + tagType: 'tag', + }); + + if (payload) { + const values = Object.values(payload).find((el) => !!el) || []; + const options: DefaultOptionType[] = values.map((val: string) => ({ + label: val, + value: val, + })); + return options; + } + return []; + }, + ); + + return { options: data ?? [], isFetching: isLoading }; +} diff --git a/frontend/src/pages/MessagingQueues/MessagingQueues.styles.scss b/frontend/src/pages/MessagingQueues/MessagingQueues.styles.scss new file mode 100644 index 0000000000..be2a27b50b --- /dev/null +++ b/frontend/src/pages/MessagingQueues/MessagingQueues.styles.scss @@ -0,0 +1,424 @@ +.messaging-queue-container { + .messaging-breadcrumb { + display: flex; + padding: 0px 16px; + align-items: center; + gap: 8px; + padding-top: 10px; + padding-bottom: 8px; + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; + letter-spacing: -0.07px; + + border-bottom: 1px solid var(--bg-slate-400); + + .message-queue-text { + cursor: pointer; + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; + letter-spacing: -0.07px; + } + } + + .messaging-header { + display: flex; + min-height: 48px; + padding: 10px 16px; + justify-content: space-between; + align-items: center; + + color: var(--bg-vanilla-400); + font-family: 'Geist Mono'; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; + letter-spacing: 0.25px; + + border-bottom: 1px solid var(--bg-slate-500); + + .header-config { + display: flex; + gap: 10px; + align-items: center; + + .messaging-queue-options { + .ant-select-selector { + display: flex; + height: 32px; + padding: 6px 6px 6px 8px; + align-items: center; + gap: 4px; + + border-radius: 2px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); + } + } + } + } + + .messaging-queue-main-graph { + display: flex; + padding: 24px 16px; + flex-direction: column; + gap: 16px; + + .config-options { + display: flex; + align-items: center; + gap: 8px; + + .config-select-option { + .ant-select-selector { + display: flex; + min-height: 32px; + align-items: center; + gap: 16px; + min-width: 164px; + + border-radius: 2px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); + } + } + } + + .mq-graph { + height: 420px; + padding: 24px 24px 0 24px; + } + + border-bottom: 1px solid var(--bg-slate-500); + } + + .messaging-queue-details { + display: flex; + padding: 16px; + + .mq-details-options { + letter-spacing: -0.06px; + .ant-radio-button-wrapper { + border-color: var(--bg-slate-400); + color: var(--bg-vanilla-400); + } + .ant-radio-button-wrapper-checked { + background: var(--bg-slate-400); + color: var(--bg-vanilla-100); + } + .ant-radio-button-wrapper-disabled { + background: var(--bg-ink-400); + color: var(--bg-slate-200); + } + .ant-radio-button-wrapper::before { + width: 0px; + } + + .disabled-option { + .coming-soon { + margin-left: 8px; + } + } + } + } +} + +.messaging-queue-options-popup { + width: 264px !important; +} + +.messaging-overview { + padding: 24px 16px 10px 16px; + + .overview-text { + color: var(--bg-vanilla-100); + font-family: Inter; + font-size: 16px; + font-style: normal; + font-weight: 500; + line-height: 24px; + letter-spacing: -0.08px; + margin: 0; + } + + .overview-subtext { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; + letter-spacing: -0.07px; + margin: 0; + margin-top: 4px; + } + + .overview-doc-area { + margin: 16px 0 28px 0; + display: flex; + + .middle-card { + border-left: none !important; + border-right: none !important; + } + + .overview-info-card { + display: flex; + width: 376px; + min-height: 176px; + padding: 18px 20px 20px 20px; + flex-direction: column; + justify-content: space-between; + border: 1px solid var(--bg-slate-500); + border-radius: 2px; + + .card-title { + color: var(--bg-vanilla-100); + font-family: Inter; + font-size: 13px; + font-style: normal; + font-weight: 600; + line-height: 22px; + letter-spacing: 0.52px; + text-transform: uppercase; + margin: 0; + } + + .card-info-text { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; + letter-spacing: -0.07px; + margin: 0; + margin-top: 4px; + } + + .button-grp { + display: flex; + gap: 8px; + + .ant-btn { + min-width: 80px; + } + + .ant-btn-default { + background-color: var(--bg-slate-400); + border: none; + box-shadow: none; + } + } + } + } + + .summary-section { + display: flex; + + .summary-card { + display: flex; + padding: 12px; + flex-direction: column; + align-items: flex-start; + border: 1px solid var(--bg-slate-500); + background: var(--bg-ink-400); + width: 337px; + height: 283px; + border-radius: 2px; + + .summary-title { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + height: 24px; + + > p { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 13px; + font-style: normal; + font-weight: 500; /* 169.231% */ + letter-spacing: 0.52px; + text-transform: uppercase; + margin: 0; + } + + .time-value { + display: flex; + gap: 4px; + align-items: center; + + > p { + color: var(--bg-slate-200); + font-family: Inter; + font-size: 12px; + font-style: normal; + font-weight: 600; + line-height: 22px; + letter-spacing: 0.48px; + text-transform: uppercase; + } + } + } + + .view-detail-btn { + height: 100%; + width: 100%; + display: flex; + justify-content: center; + align-items: center; + } + } + } + + .coming-soon-card { + background: var(--bg-ink-500) !important; + border-left: none !important; + } +} + +.overview-confirm-modal { + background-color: var(--bg-ink-500); + padding: 0; + border-radius: 4px; + + .ant-modal-content { + background-color: var(--bg-ink-300); + .ant-modal-confirm-content { + color: var(--bg-vanilla-100); + } + + .ant-modal-confirm-body-wrapper { + display: flex; + flex-direction: column; + height: 150px; + justify-content: space-between; + } + } +} + +.lightMode { + .messaging-queue-container { + .messaging-breadcrumb { + color: var(--bg-ink-400); + border-bottom: 1px solid var(--bg-vanilla-300); + } + .messaging-header { + color: var(--bg-ink-400); + border-bottom: 1px solid var(--bg-vanilla-300); + + .header-config { + .messaging-queue-options { + .ant-select-selector { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-100); + } + } + } + } + + .messaging-queue-main-graph { + .config-options { + .config-select-option { + .ant-select-selector { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-100); + } + } + } + + border-bottom: 1px solid var(--bg-vanilla-300); + } + + .messaging-queue-details { + .mq-details-options { + .ant-radio-button-wrapper { + border-color: var(--bg-vanilla-300); + color: var(--bg-slate-200); + } + .ant-radio-button-wrapper-checked { + color: var(--bg-slate-200); + background: var(--bg-vanilla-300); + } + .ant-radio-button-wrapper-disabled { + background: var(--bg-vanilla-100); + color: var(--bg-vanilla-400); + } + } + } + } + + .messaging-overview { + .overview-text { + color: var(--bg-slate-200); + } + + .overview-subtext { + color: var(--bg-slate-300); + } + + .overview-doc-area { + .overview-info-card { + border: 1px solid var(--bg-vanilla-300); + + .card-title { + color: var(--bg-slate-200); + } + + .card-info-text { + color: var(--bg-slate-300); + } + + .button-grp { + .ant-btn-default { + background-color: var(--bg-vanilla-100); + } + } + } + } + + .summary-section { + .summary-card { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-100); + + .summary-title { + > p { + color: var(--bg-slate-200); + } + + .time-value { + > p { + color: var(--bg-slate-200); + } + } + } + } + } + + .coming-soon-card { + background: var(--bg-vanilla-200) !important; + } + } + + .overview-confirm-modal { + background-color: var(--bg-vanilla-100); + + .ant-modal-content { + background-color: var(--bg-vanilla-100); + .ant-modal-confirm-content { + color: var(--bg-slate-200); + } + } + } +} diff --git a/frontend/src/pages/MessagingQueues/MessagingQueues.tsx b/frontend/src/pages/MessagingQueues/MessagingQueues.tsx new file mode 100644 index 0000000000..727dc514b9 --- /dev/null +++ b/frontend/src/pages/MessagingQueues/MessagingQueues.tsx @@ -0,0 +1,174 @@ +import './MessagingQueues.styles.scss'; + +import { ExclamationCircleFilled } from '@ant-design/icons'; +import { Color } from '@signozhq/design-tokens'; +import { Button, Modal } from 'antd'; +import ROUTES from 'constants/routes'; +import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2'; +import { Calendar, ListMinus } from 'lucide-react'; +import { useHistory } from 'react-router-dom'; +import { isCloudUser } from 'utils/app'; + +import { + KAFKA_SETUP_DOC_LINK, + MessagingQueuesViewType, +} from './MessagingQueuesUtils'; +import { ComingSoon } from './MQCommon/MQCommon'; + +function MessagingQueues(): JSX.Element { + const history = useHistory(); + + const { confirm } = Modal; + + const showConfirm = (): void => { + confirm({ + icon: , + content: + 'Before navigating to the details page, please make sure you have configured all the required setup to ensure correct data monitoring.', + className: 'overview-confirm-modal', + onOk() { + history.push(ROUTES.MESSAGING_QUEUES_DETAIL); + }, + okText: 'Proceed', + }); + }; + + const isCloudUserVal = isCloudUser(); + + const getStartedRedirect = (link: string): void => { + if (isCloudUserVal) { + history.push(link); + } else { + window.open(KAFKA_SETUP_DOC_LINK, '_blank'); + } + }; + + return ( +
+
+ + Messaging Queues +
+
+
Kafka / Overview
+ +
+
+

+ Start sending data in as little as 20 minutes +

+

Connect and Monitor Your Data Streams

+
+
+
+

Configure Consumer

+

+ Connect your consumer and producer data sources to start monitoring. +

+
+
+ +
+
+
+
+

Configure Producer

+

+ Connect your consumer and producer data sources to start monitoring. +

+
+
+ +
+
+
+
+

Monitor kafka

+

+ Set up your Kafka monitoring to track consumer and producer activities. +

+
+
+ +
+
+
+
+
+
+

{MessagingQueuesViewType.consumerLag.label}

+
+ +

1D

+
+
+
+ +
+
+
+
+

{MessagingQueuesViewType.partitionLatency.label}

+
+ +

1D

+
+
+
+ +
+
+
+
+

{MessagingQueuesViewType.producerLatency.label}

+
+ +

1D

+
+
+
+ +
+
+
+
+

{MessagingQueuesViewType.consumerLatency.label}

+
+ +

1D

+
+
+
+ +
+
+
+
+
+ ); +} + +export default MessagingQueues; diff --git a/frontend/src/pages/MessagingQueues/MessagingQueuesUtils.ts b/frontend/src/pages/MessagingQueues/MessagingQueuesUtils.ts new file mode 100644 index 0000000000..b0855ce524 --- /dev/null +++ b/frontend/src/pages/MessagingQueues/MessagingQueuesUtils.ts @@ -0,0 +1,225 @@ +import { QueryParams } from 'constants/query'; +import { PANEL_TYPES } from 'constants/queryBuilder'; +import { GetWidgetQueryBuilderProps } from 'container/MetricsApplication/types'; +import { History, Location } from 'history'; +import { isEmpty } from 'lodash-es'; +import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData'; +import { EQueryType } from 'types/common/dashboard'; +import { DataSource } from 'types/common/queryBuilder'; +import { v4 as uuid } from 'uuid'; + +export const KAFKA_SETUP_DOC_LINK = + 'https://github.com/shivanshuraj1333/kafka-opentelemetry-instrumentation/tree/master'; + +export function convertToTitleCase(text: string): string { + return text + .split('_') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) + .join(' '); +} + +export type RowData = { + key: string | number; + [key: string]: string | number; +}; + +export enum ConsumerLagDetailType { + ConsumerDetails = 'consumer-details', + ProducerDetails = 'producer-details', + NetworkLatency = 'network-latency', + PartitionHostMetrics = 'partition-host-metric', +} + +export const ConsumerLagDetailTitle: Record = { + 'consumer-details': 'Consumer Groups Details', + 'producer-details': 'Producer Details', + 'network-latency': 'Network Latency', + 'partition-host-metric': 'Partition Host Metrics', +}; + +export function createWidgetFilterItem( + key: string, + value: string, +): TagFilterItem { + const id = `${key}--string--tag--false`; + + return { + id: uuid(), + key: { + key, + dataType: DataTypes.String, + type: 'tag', + isColumn: false, + isJSON: false, + id, + }, + op: '=', + value, + }; +} + +export function getFiltersFromConfigOptions( + consumerGrp?: string, + topic?: string, + partition?: string, +): TagFilterItem[] { + const configOptions = [ + { key: 'group', values: consumerGrp?.split(',') }, + { key: 'topic', values: topic?.split(',') }, + { key: 'partition', values: partition?.split(',') }, + ]; + return configOptions.reduce( + (accumulator, { key, values }) => { + if (values && !isEmpty(values.filter((item) => item !== ''))) { + accumulator.push( + ...values.map((value) => createWidgetFilterItem(key, value)), + ); + } + return accumulator; + }, + [], + ); +} + +export function getWidgetQuery({ + filterItems, +}: { + filterItems: TagFilterItem[]; +}): GetWidgetQueryBuilderProps { + return { + title: 'Consumer Lag', + panelTypes: PANEL_TYPES.TIME_SERIES, + fillSpans: false, + yAxisUnit: 'none', + query: { + queryType: EQueryType.QUERY_BUILDER, + promql: [], + builder: { + queryData: [ + { + aggregateAttribute: { + dataType: DataTypes.Float64, + id: 'kafka_consumer_group_lag--float64--Gauge--true', + isColumn: true, + isJSON: false, + key: 'kafka_consumer_group_lag', + type: 'Gauge', + }, + aggregateOperator: 'max', + dataSource: DataSource.METRICS, + disabled: false, + expression: 'A', + filters: { + items: filterItems || [], + op: 'AND', + }, + functions: [], + groupBy: [ + { + dataType: DataTypes.String, + id: 'group--string--tag--false', + isColumn: false, + isJSON: false, + key: 'group', + type: 'tag', + }, + { + dataType: DataTypes.String, + id: 'topic--string--tag--false', + isColumn: false, + isJSON: false, + key: 'topic', + type: 'tag', + }, + { + dataType: DataTypes.String, + id: 'partition--string--tag--false', + isColumn: false, + isJSON: false, + key: 'partition', + type: 'tag', + }, + ], + having: [], + legend: '{{group}}-{{topic}}-{{partition}}', + limit: null, + orderBy: [], + queryName: 'A', + reduceTo: 'avg', + spaceAggregation: 'avg', + stepInterval: 60, + timeAggregation: 'max', + }, + ], + queryFormulas: [], + }, + clickhouse_sql: [], + id: uuid(), + }, + }; +} + +export const convertToNanoseconds = (timestamp: number): bigint => + BigInt((timestamp * 1e9).toFixed(0)); + +export const getStartAndEndTimesInMilliseconds = ( + timestamp: number, +): { start: number; end: number } => { + const FIVE_MINUTES_IN_MILLISECONDS = 5 * 60 * 1000; // 5 minutes in milliseconds - check with Shivanshu once + + const start = Math.floor(timestamp); + const end = Math.floor(start + FIVE_MINUTES_IN_MILLISECONDS); + + return { start, end }; +}; + +export interface SelectedTimelineQuery { + group?: string; + partition?: string; + topic?: string; + start?: number; + end?: number; +} + +export function setSelectedTimelineQuery( + urlQuery: URLSearchParams, + timestamp: number, + location: Location, + history: History, + data?: { + [key: string]: string; + }, +): void { + const selectedTimelineQuery: SelectedTimelineQuery = { + group: data?.group, + partition: data?.partition, + topic: data?.topic, + ...getStartAndEndTimesInMilliseconds(timestamp), + }; + urlQuery.set( + QueryParams.selectedTimelineQuery, + encodeURIComponent(JSON.stringify(selectedTimelineQuery)), + ); + const generatedUrl = `${location.pathname}?${urlQuery.toString()}`; + history.replace(generatedUrl); +} + +export const MessagingQueuesViewType = { + consumerLag: { + label: 'Consumer Lag view', + value: 'consumerLag', + }, + partitionLatency: { + label: 'Partition Latency view', + value: 'partitionLatency', + }, + producerLatency: { + label: 'Producer Latency view', + value: 'producerLatency', + }, + consumerLatency: { + label: 'Consumer latency view', + value: 'consumerLatency', + }, +}; diff --git a/frontend/src/pages/MessagingQueues/index.tsx b/frontend/src/pages/MessagingQueues/index.tsx new file mode 100644 index 0000000000..cc59152b17 --- /dev/null +++ b/frontend/src/pages/MessagingQueues/index.tsx @@ -0,0 +1,3 @@ +import MessagingQueues from './MessagingQueues'; + +export default MessagingQueues; diff --git a/frontend/src/pages/SaveView/__test__/SaveView.test.tsx b/frontend/src/pages/SaveView/__test__/SaveView.test.tsx new file mode 100644 index 0000000000..fee0d3d6f0 --- /dev/null +++ b/frontend/src/pages/SaveView/__test__/SaveView.test.tsx @@ -0,0 +1,172 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable sonarjs/no-duplicate-string */ +import ROUTES from 'constants/routes'; +import { explorerView } from 'mocks-server/__mockdata__/explorer_views'; +import { server } from 'mocks-server/server'; +import { rest } from 'msw'; +import { MemoryRouter, Route } from 'react-router-dom'; +import { fireEvent, render, screen, waitFor, within } from 'tests/test-utils'; + +import SaveView from '..'; + +const handleExplorerTabChangeTest = jest.fn(); +jest.mock('hooks/useHandleExplorerTabChange', () => ({ + useHandleExplorerTabChange: () => ({ + handleExplorerTabChange: handleExplorerTabChangeTest, + }), +})); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useLocation: jest.fn().mockReturnValue({ + pathname: `${ROUTES.TRACES_SAVE_VIEWS}`, + }), +})); + +describe('SaveView', () => { + it('should render the SaveView component', async () => { + render(); + expect(await screen.findByText('Table View')).toBeInTheDocument(); + + const savedViews = screen.getAllByRole('row'); + expect(savedViews).toHaveLength(2); + + // assert row 1 + expect( + within(document.querySelector('.view-tag') as HTMLElement).getByText('T'), + ).toBeInTheDocument(); + expect(screen.getByText('test-user-1')).toBeInTheDocument(); + + // assert row 2 + expect(screen.getByText('R-test panel')).toBeInTheDocument(); + expect(screen.getByText('test-user-test')).toBeInTheDocument(); + }); + + it('explorer icon should take the user to the related explorer page', async () => { + render( + + + + + , + ); + + expect(await screen.findByText('Table View')).toBeInTheDocument(); + + const explorerIcon = await screen.findAllByTestId('go-to-explorer'); + expect(explorerIcon[0]).toBeInTheDocument(); + + // Simulate click on explorer icon + fireEvent.click(explorerIcon[0]); + + await waitFor(() => + expect(handleExplorerTabChangeTest).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + '/traces-explorer', // Asserts the third argument is '/traces-explorer' + ), + ); + }); + + it('should render the SaveView component with a search input', async () => { + render(); + const searchInput = screen.getByPlaceholderText('Search for views...'); + expect(await screen.findByText('Table View')).toBeInTheDocument(); + + expect(searchInput).toBeInTheDocument(); + + // search for 'R-test panel' + searchInput.focus(); + (searchInput as HTMLInputElement).setSelectionRange( + 0, + (searchInput as HTMLInputElement).value.length, + ); + + fireEvent.change(searchInput, { target: { value: 'R-test panel' } }); + expect(searchInput).toHaveValue('R-test panel'); + searchInput.blur(); + + expect(await screen.findByText('R-test panel')).toBeInTheDocument(); + + // Table View should not be present now + const savedViews = screen.getAllByRole('row'); + expect(savedViews).toHaveLength(1); + }); + + it('should be able to edit name of view', async () => { + server.use( + rest.put( + 'http://localhost/api/v1/explorer/views/test-uuid-1', + // eslint-disable-next-line no-return-assign + (_req, res, ctx) => + res( + ctx.status(200), + ctx.json({ + ...explorerView, + data: [ + ...explorerView.data, + (explorerView.data[0].name = 'New Table View'), + ], + }), + ), + ), + ); + render(); + + const editButton = await screen.findAllByTestId('edit-view'); + fireEvent.click(editButton[0]); + + const viewName = await screen.findByTestId('view-name'); + expect(viewName).toBeInTheDocument(); + expect(viewName).toHaveValue('Table View'); + + const newViewName = 'New Table View'; + fireEvent.change(viewName, { target: { value: newViewName } }); + expect(viewName).toHaveValue(newViewName); + + const saveButton = await screen.findByTestId('save-view'); + fireEvent.click(saveButton); + + await waitFor(() => + expect(screen.getByText(newViewName)).toBeInTheDocument(), + ); + }); + + it('should be able to delete a view', async () => { + server.use( + rest.delete( + 'http://localhost/api/v1/explorer/views/test-uuid-1', + (_req, res, ctx) => res(ctx.status(200), ctx.json({ status: 'success' })), + ), + ); + + render(); + + const deleteButton = await screen.findAllByTestId('delete-view'); + fireEvent.click(deleteButton[0]); + + expect(await screen.findByText('delete_confirm_message')).toBeInTheDocument(); + + const confirmButton = await screen.findByTestId('confirm-delete'); + fireEvent.click(confirmButton); + + await waitFor(() => expect(screen.queryByText('Table View')).toBeNull()); + }); + + it('should render empty state', async () => { + server.use( + rest.get('http://localhost/api/v1/explorer/views', (_req, res, ctx) => + res( + ctx.status(200), + ctx.json({ + status: 'success', + data: [], + }), + ), + ), + ); + render(); + + expect(screen.getByText('No data')).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/pages/SaveView/index.tsx b/frontend/src/pages/SaveView/index.tsx index 7088c4a78c..03c76151b7 100644 --- a/frontend/src/pages/SaveView/index.tsx +++ b/frontend/src/pages/SaveView/index.tsx @@ -263,13 +263,19 @@ function SaveView(): JSX.Element { handleEditModelOpen(view, bgColor)} /> - handleRedirectQuery(view)} /> + handleRedirectQuery(view)} + data-testid="go-to-explorer" + /> handleDeleteModelOpen(view.uuid, view.name)} />
@@ -347,6 +353,7 @@ function SaveView(): JSX.Element { onClick={onDeleteHandler} className="delete-btn" disabled={isDeleteLoading} + data-testid="confirm-delete" > Delete view , @@ -371,6 +378,7 @@ function SaveView(): JSX.Element { icon={} onClick={onUpdateQueryHandler} disabled={isViewUpdating} + data-testid="save-view" > Save changes , @@ -385,6 +393,7 @@ function SaveView(): JSX.Element { setNewViewName(e.target.value)} />
diff --git a/frontend/src/pages/Support/Support.tsx b/frontend/src/pages/Support/Support.tsx index ad13c3993d..0dbd7a9526 100644 --- a/frontend/src/pages/Support/Support.tsx +++ b/frontend/src/pages/Support/Support.tsx @@ -20,7 +20,7 @@ import { } from 'lucide-react'; import { useEffect, useState } from 'react'; import { useMutation } from 'react-query'; -import { useHistory } from 'react-router-dom'; +import { useHistory, useLocation } from 'react-router-dom'; import { ErrorResponse, SuccessResponse } from 'types/api'; import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout'; import { License } from 'types/api/licenses/def'; @@ -105,6 +105,7 @@ export default function Support(): JSX.Element { false, ); + const { pathname } = useLocation(); const handleChannelWithRedirects = (url: string): void => { window.open(url, '_blank'); }; @@ -181,8 +182,8 @@ export default function Support(): JSX.Element { const handleAddCreditCard = (): void => { logEvent('Add Credit card modal: Clicked', { - source: `chat`, - page: 'support', + source: `help & support`, + page: pathname, }); updateCreditCard({ @@ -194,6 +195,10 @@ export default function Support(): JSX.Element { const handleChat = (): void => { if (showAddCreditCardModal) { + logEvent('Disabled Chat Support: Clicked', { + source: `help & support`, + page: pathname, + }); setIsAddCreditCardModalOpen(true); } else if (window.Intercom) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment diff --git a/frontend/src/pages/TraceDetail/__test__/TraceDetail.test.tsx b/frontend/src/pages/TraceDetail/__test__/TraceDetail.test.tsx new file mode 100644 index 0000000000..b16bb6c735 --- /dev/null +++ b/frontend/src/pages/TraceDetail/__test__/TraceDetail.test.tsx @@ -0,0 +1,214 @@ +/* eslint-disable sonarjs/no-duplicate-string */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import ROUTES from 'constants/routes'; +import { MemoryRouter, Route } from 'react-router-dom'; +import { fireEvent, render, screen } from 'tests/test-utils'; + +import TraceDetail from '..'; + +window.HTMLElement.prototype.scrollIntoView = jest.fn(); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useLocation: (): { pathname: string; search: string } => ({ + pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.TRACE_DETAIL}`, + search: '?spanId=28a8a67365d0bd8b&levelUp=0&levelDown=0', + }), + + useParams: jest.fn().mockReturnValue({ + id: '000000000000000071dc9b0a338729b4', + }), +})); + +jest.mock('container/TraceFlameGraph/index.tsx', () => ({ + __esModule: true, + default: (): JSX.Element =>
TraceFlameGraph
, +})); + +describe('TraceDetail', () => { + it('should render tracedetail', async () => { + const { findByText, getByText, getAllByText, getByPlaceholderText } = render( + + + + + , + , + ); + expect(await findByText('Trace Details')).toBeInTheDocument(); + + // as we have an active spanId, it should scroll to the selected span + expect(window.HTMLElement.prototype.scrollIntoView).toHaveBeenCalled(); + + // assertions + expect(getByText('TraceFlameGraph')).toBeInTheDocument(); + expect(getByText('Focus on selected span')).toBeInTheDocument(); + + // span action buttons + expect(getByText('Reset Focus')).toBeInTheDocument(); + expect(getByText('50 Spans')).toBeInTheDocument(); + + // trace span detail - parent -> child + expect(getAllByText('frontend')[0]).toBeInTheDocument(); + expect(getByText('776.76 ms')).toBeInTheDocument(); + [ + { trace: 'HTTP GET /dispatch', duration: '776.76 ms', count: '50' }, + { trace: 'HTTP GET: /customer', duration: '349.44 ms', count: '4' }, + { + trace: '/driver.DriverService/FindNearest', + duration: '173.10 ms', + count: '15', + }, + // and so on ... + ].forEach((traceDetail) => { + expect(getByText(traceDetail.trace)).toBeInTheDocument(); + expect(getByText(traceDetail.duration)).toBeInTheDocument(); + expect(getByText(traceDetail.count)).toBeInTheDocument(); + }); + + // Details for selected Span + expect(getByText('Details for selected Span')).toBeInTheDocument(); + ['Service', 'Operation', 'SpanKind', 'StatusCodeString'].forEach((detail) => { + expect(getByText(detail)).toBeInTheDocument(); + }); + + // go to related logs button + const goToRelatedLogsButton = getByText('Go to Related logs'); + expect(goToRelatedLogsButton).toBeInTheDocument(); + + // Tag and Event tabs + expect(getByText('Tags')).toBeInTheDocument(); + expect(getByText('Events')).toBeInTheDocument(); + expect(getByPlaceholderText('traceDetails:search_tags')).toBeInTheDocument(); + + // Tag details + [ + { title: 'client-uuid', value: '64a18ffd5f8adbfb' }, + { title: 'component', value: 'net/http' }, + { title: 'host.name', value: '4f6ec470feea' }, + { title: 'http.method', value: 'GET' }, + { title: 'http.url', value: '/route?dropoff=728%2C326&pickup=165%2C543' }, + { title: 'http.status_code', value: '200' }, + { title: 'ip', value: '172.25.0.2' }, + { title: 'opencensus.exporterversion', value: 'Jaeger-Go-2.30.0' }, + ].forEach((tag) => { + expect(getByText(tag.title)).toBeInTheDocument(); + expect(getByText(tag.value)).toBeInTheDocument(); + }); + + // see full value + expect(getAllByText('View full value')[0]).toBeInTheDocument(); + }); + + it('should render tracedetail events tab', async () => { + const { findByText, getByText } = render( + + + + + , + , + ); + expect(await findByText('Trace Details')).toBeInTheDocument(); + + fireEvent.click(getByText('Events')); + + expect(await screen.findByText('HTTP request received')).toBeInTheDocument(); + + // event details + [ + { title: 'Event Start Time', value: '527.60 ms' }, + { title: 'level', value: 'info' }, + ].forEach((tag) => { + expect(getByText(tag.title)).toBeInTheDocument(); + expect(getByText(tag.value)).toBeInTheDocument(); + }); + + expect(getByText('View full log event message')).toBeInTheDocument(); + }); + + it('should toggle slider - selected span details', async () => { + const { findByTestId, queryByText } = render( + + + + + , + , + ); + const slider = await findByTestId('span-details-sider'); + expect(slider).toBeInTheDocument(); + + fireEvent.click( + slider.querySelector('.ant-layout-sider-trigger') as HTMLElement, + ); + + expect(queryByText('Details for selected Span')).not.toBeInTheDocument(); + }); + + it('should be able to selected another span and see its detail', async () => { + const { getByText } = render( + + + + + , + , + ); + + expect(await screen.findByText('Trace Details')).toBeInTheDocument(); + + const spanTitle = getByText('/driver.DriverService/FindNearest'); + expect(spanTitle).toBeInTheDocument(); + fireEvent.click(spanTitle); + + // Tag details + [ + { title: 'client-uuid', value: '6fb81b8ca91b2b4d' }, + { title: 'component', value: 'gRPC' }, + { title: 'host.name', value: '4f6ec470feea' }, + ].forEach((tag) => { + expect(getByText(tag.title)).toBeInTheDocument(); + expect(getByText(tag.value)).toBeInTheDocument(); + }); + }); + + it('focus on selected span and reset focus action', async () => { + const { getByText, getAllByText } = render( + + + + + , + , + ); + + expect(await screen.findByText('Trace Details')).toBeInTheDocument(); + + const spanTitle = getByText('/driver.DriverService/FindNearest'); + expect(spanTitle).toBeInTheDocument(); + fireEvent.click(spanTitle); + + expect(await screen.findByText('6fb81b8ca91b2b4d')).toBeInTheDocument(); + + // focus on selected span + const focusButton = getByText('Focus on selected span'); + expect(focusButton).toBeInTheDocument(); + fireEvent.click(focusButton); + + // assert selected span + expect(getByText('15 Spans')).toBeInTheDocument(); + expect(getAllByText('/driver.DriverService/FindNearest')).toHaveLength(3); + expect(getByText('173.10 ms')).toBeInTheDocument(); + + // reset focus + expect(screen.queryByText('HTTP GET /dispatch')).not.toBeInTheDocument(); + + const resetFocusButton = getByText('Reset Focus'); + expect(resetFocusButton).toBeInTheDocument(); + fireEvent.click(resetFocusButton); + + expect(window.HTMLElement.prototype.scrollIntoView).toHaveBeenCalled(); + expect(screen.queryByText('HTTP GET /dispatch')).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/pages/TracesExplorer/__test__/TracesExplorer.test.tsx b/frontend/src/pages/TracesExplorer/__test__/TracesExplorer.test.tsx index 1dcaaaa4cf..a28776f0d0 100644 --- a/frontend/src/pages/TracesExplorer/__test__/TracesExplorer.test.tsx +++ b/frontend/src/pages/TracesExplorer/__test__/TracesExplorer.test.tsx @@ -1,27 +1,56 @@ /* eslint-disable sonarjs/no-duplicate-string */ -/* eslint-disable no-restricted-syntax */ -/* eslint-disable no-await-in-loop */ import userEvent from '@testing-library/user-event'; import { initialQueriesMap, initialQueryBuilderFormValues, + PANEL_TYPES, } from 'constants/queryBuilder'; import ROUTES from 'constants/routes'; import * as compositeQueryHook from 'hooks/queryBuilder/useGetCompositeQueryParam'; +import { + queryRangeForListView, + queryRangeForTableView, + queryRangeForTimeSeries, + queryRangeForTraceView, +} from 'mocks-server/__mockdata__/query_range'; +import { server } from 'mocks-server/server'; +import { rest } from 'msw'; import { QueryBuilderContext } from 'providers/QueryBuilder'; -import { fireEvent, render, screen, waitFor, within } from 'tests/test-utils'; -import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; -import { Query } from 'types/api/queryBuilder/queryBuilderData'; +import { + act, + fireEvent, + render, + screen, + waitFor, + within, +} from 'tests/test-utils'; import TracesExplorer from '..'; import { Filter } from '../Filter/Filter'; import { AllTraceFilterKeyValue } from '../Filter/filterUtils'; +import { + checkForSectionContent, + checkIfSectionIsNotOpen, + checkIfSectionIsOpen, + compositeQuery, + defaultClosedSections, + defaultOpenSections, + optionMenuReturn, + qbProviderValue, + redirectWithQueryBuilderData, +} from './testUtils'; + +const historyPush = jest.fn(); jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useLocation: (): { pathname: string } => ({ pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.TRACES_EXPLORER}/`, }), + useHistory: (): any => ({ + ...jest.requireActual('react-router-dom').useHistory(), + push: historyPush, + }), })); jest.mock('uplot', () => { @@ -40,6 +69,25 @@ jest.mock('uplot', () => { }; }); +jest.mock( + 'components/Uplot/Uplot', + () => + function MockUplot(): JSX.Element { + return
MockUplot
; + }, +); + +const successNotification = jest.fn(); +jest.mock('hooks/useNotifications', () => ({ + __esModule: true, + useNotifications: jest.fn(() => ({ + notifications: { + success: successNotification, + error: jest.fn(), + }, + })), +})); + jest.mock( 'container/TopNav/DateTimeSelectionV2/index.tsx', () => @@ -48,84 +96,12 @@ jest.mock( }, ); -function checkIfSectionIsOpen( - getByTestId: (testId: string) => HTMLElement, - panelName: string, -): void { - const section = getByTestId(`collapse-${panelName}`); - expect(section.querySelector('.ant-collapse-item-active')).not.toBeNull(); -} +jest.mock('container/OptionsMenu/useOptionsMenu', () => ({ + __esModule: true, + default: (): any => optionMenuReturn, +})); -function checkIfSectionIsNotOpen( - getByTestId: (testId: string) => HTMLElement, - panelName: string, -): void { - const section = getByTestId(`collapse-${panelName}`); - expect(section.querySelector('.ant-collapse-item-active')).toBeNull(); -} - -const defaultOpenSections = ['hasError', 'durationNano', 'serviceName']; - -const defaultClosedSections = Object.keys(AllTraceFilterKeyValue).filter( - (section) => - ![...defaultOpenSections, 'durationNanoMin', 'durationNanoMax'].includes( - section, - ), -); - -async function checkForSectionContent(values: string[]): Promise { - for (const val of values) { - const sectionContent = await screen.findByText(val); - await waitFor(() => expect(sectionContent).toBeInTheDocument()); - } -} - -const redirectWithQueryBuilderData = jest.fn(); - -const compositeQuery: Query = { - ...initialQueriesMap.traces, - builder: { - ...initialQueriesMap.traces.builder, - queryData: [ - { - ...initialQueryBuilderFormValues, - filters: { - items: [ - { - id: '95564eb1', - key: { - key: 'name', - dataType: DataTypes.String, - type: 'tag', - isColumn: true, - isJSON: false, - id: 'name--string--tag--true', - }, - op: 'in', - value: ['HTTP GET /customer'], - }, - { - id: '3337951c', - key: { - key: 'serviceName', - dataType: DataTypes.String, - type: 'tag', - isColumn: true, - isJSON: false, - id: 'serviceName--string--tag--true', - }, - op: 'in', - value: ['demo-app'], - }, - ], - op: 'AND', - }, - }, - ], - }, -}; - -describe('TracesExplorer - ', () => { +describe('TracesExplorer - Filters', () => { // Initial filter panel rendering // Test the initial state like which filters section are opened, default state of duration slider, etc. it('should render the Trace filter', async () => { @@ -457,3 +433,253 @@ describe('TracesExplorer - ', () => { ).toBeInTheDocument(); }); }); + +const handleExplorerTabChangeTest = jest.fn(); +jest.mock('hooks/useHandleExplorerTabChange', () => ({ + useHandleExplorerTabChange: jest.fn(() => ({ + handleExplorerTabChange: handleExplorerTabChangeTest, + })), +})); + +describe('TracesExplorer - ', () => { + it('should render the traces explorer page', async () => { + server.use( + rest.post('http://localhost/api/v3/query_range', (req, res, ctx) => + res(ctx.status(200), ctx.json(queryRangeForTimeSeries)), + ), + ); + const { findByText, getByText } = render(); + + // assert mocked date time selection + expect(await findByText('MockDateTimeSelection')).toBeInTheDocument(); + + // assert stage&Btn + expect(getByText('Stage & Run Query')).toBeInTheDocument(); + + // assert QB - will not write tests for QB as that would be covererd in QB tests separately + expect( + getByText( + 'Search Filter : select options from suggested values, for IN/NOT IN operators - press "Enter" after selecting options', + ), + ).toBeInTheDocument(); + expect(getByText('AGGREGATION INTERVAL')).toBeInTheDocument(); + expect(getByText('Metrics name')).toBeInTheDocument(); + expect(getByText('WHERE')).toBeInTheDocument(); + expect(getByText('Legend Format')).toBeInTheDocument(); + + // assert timeseries chart mock + expect(await screen.findByText('MockUplot')).toBeInTheDocument(); + }); + + it('check tab navigation', async () => { + const { getByText } = render(); + + // switch to list view + const listViewBtn = getByText('List View'); + expect(listViewBtn).toBeInTheDocument(); + fireEvent.click(listViewBtn); + + expect(handleExplorerTabChangeTest).toBeCalledWith(PANEL_TYPES.LIST); + + // switch to traces view + const tracesBtn = getByText('Traces'); + expect(tracesBtn).toBeInTheDocument(); + fireEvent.click(tracesBtn); + + expect(handleExplorerTabChangeTest).toBeCalledWith(PANEL_TYPES.TRACE); + + // switch to Table view + const TableBtn = getByText('Table View'); + expect(TableBtn).toBeInTheDocument(); + fireEvent.click(TableBtn); + + expect(handleExplorerTabChangeTest).toBeCalledWith(PANEL_TYPES.TABLE); + }); + + it('trace explorer - list view', async () => { + server.use( + rest.post('http://localhost/api/v3/query_range', (req, res, ctx) => + res(ctx.status(200), ctx.json(queryRangeForListView)), + ), + ); + + const { getByText } = render( + + + , + ); + + expect(await screen.findByText('Timestamp')).toBeInTheDocument(); + expect(getByText('options_menu.options')).toBeInTheDocument(); + + // test if pagination is there + expect(getByText('Previous')).toBeInTheDocument(); + expect(getByText('Next')).toBeInTheDocument(); + + // column interaction is covered in E2E tests as its a complex interaction + }); + + it('trace explorer - table view', async () => { + server.use( + rest.post('http://localhost/api/v3/query_range', (req, res, ctx) => + res(ctx.status(200), ctx.json(queryRangeForTableView)), + ), + ); + render( + + + , + ); + + expect(await screen.findByText('count')).toBeInTheDocument(); + expect(screen.getByText('87798.00')).toBeInTheDocument(); + }); + + it('trace explorer - trace view', async () => { + server.use( + rest.post('http://localhost/api/v3/query_range', (req, res, ctx) => + res(ctx.status(200), ctx.json(queryRangeForTraceView)), + ), + ); + const { getByText, getAllByText } = render( + + + , + ); + + expect(await screen.findByText('Root Service Name')).toBeInTheDocument(); + + // assert table headers + expect(getByText('Root Operation Name')).toBeInTheDocument(); + expect(getByText('Root Duration (in ms)')).toBeInTheDocument(); + expect(getByText('TraceID')).toBeInTheDocument(); + expect(getByText('No of Spans')).toBeInTheDocument(); + + // assert row values + ['demo-app', 'home', '8'].forEach((val) => + expect(getAllByText(val)[0]).toBeInTheDocument(), + ); + expect(getByText('7245.23ms')).toBeInTheDocument(); + + // assert traceId and redirection to trace details + const traceId = getByText('5765b60ba7cc4ddafe8bdaa9c1b4b246'); + fireEvent.click(traceId); + + // assert redirection - should go to /trace/:traceId + expect(window.location.href).toEqual( + 'http://localhost/trace/5765b60ba7cc4ddafe8bdaa9c1b4b246', + ); + }); + + it('test for explorer options', async () => { + const { getByText, getByTestId } = render(); + + // assert explorer options - action btns + [ + 'Save this view', + 'Create an Alert', + 'Add to Dashboard', + 'Select a view', + ].forEach((val) => expect(getByText(val)).toBeInTheDocument()); + + const hideExplorerOption = getByTestId('hide-toolbar'); + expect(hideExplorerOption).toBeInTheDocument(); + fireEvent.click(hideExplorerOption); + + // explorer options should hide and show btn should be present + expect(await screen.findByTestId('show-explorer-option')).toBeInTheDocument(); + expect(screen.queryByTestId('hide-toolbar')).toBeNull(); + + // show explorer options + const showExplorerOption = screen.getByTestId('show-explorer-option'); + expect(showExplorerOption).toBeInTheDocument(); + fireEvent.click(showExplorerOption); + + // explorer options should show and hide btn should be present + expect(await screen.findByTestId('hide-toolbar')).toBeInTheDocument(); + }); + + it('select a view options - assert and save this view', async () => { + const { container } = render(); + + await act(async () => { + fireEvent.mouseDown( + container.querySelector( + '.view-options .ant-select-selection-search-input', + ) as HTMLElement, + ); + }); + + const viewListOptions = await screen.findByRole('listbox'); + expect(viewListOptions).toBeInTheDocument(); + + expect(within(viewListOptions).getByText('R-test panel')).toBeInTheDocument(); + + expect(within(viewListOptions).getByText('Table View')).toBeInTheDocument(); + + // save this view + fireEvent.click(screen.getByText('Save this view')); + + const saveViewModalInput = await screen.findByPlaceholderText( + 'e.g. External http method view', + ); + expect(saveViewModalInput).toBeInTheDocument(); + + const saveViewModal = document.querySelector( + '.ant-modal-content', + ) as HTMLElement; + expect(saveViewModal).toBeInTheDocument(); + + await act(async () => + fireEvent.change(saveViewModalInput, { target: { value: 'test view' } }), + ); + + expect(saveViewModalInput).toHaveValue('test view'); + await act(async () => { + fireEvent.click(within(saveViewModal).getByTestId('save-view-btn')); + }); + + expect(successNotification).toHaveBeenCalledWith({ + message: 'View Saved Successfully', + }); + }); + + it('create a dashboard btn assert', async () => { + const { getByText } = render(); + + const createDashboardBtn = getByText('Add to Dashboard'); + expect(createDashboardBtn).toBeInTheDocument(); + fireEvent.click(createDashboardBtn); + + expect(await screen.findByText('Export Panel')).toBeInTheDocument(); + const createDashboardModal = document.querySelector( + '.ant-modal-content', + ) as HTMLElement; + expect(createDashboardModal).toBeInTheDocument(); + + // assert modal content + expect( + within(createDashboardModal).getByText('Select Dashboard'), + ).toBeInTheDocument(); + + expect( + within(createDashboardModal).getByText('New Dashboard'), + ).toBeInTheDocument(); + }); + + it('create an alert btn assert', async () => { + const { getByText } = render(); + + const createAlertBtn = getByText('Create an Alert'); + expect(createAlertBtn).toBeInTheDocument(); + fireEvent.click(createAlertBtn); + + expect(historyPush).toHaveBeenCalledWith( + expect.stringContaining(`${ROUTES.ALERTS_NEW}`), + ); + }); +}); diff --git a/frontend/src/pages/TracesExplorer/__test__/testUtils.ts b/frontend/src/pages/TracesExplorer/__test__/testUtils.ts new file mode 100644 index 0000000000..80d96c9cf3 --- /dev/null +++ b/frontend/src/pages/TracesExplorer/__test__/testUtils.ts @@ -0,0 +1,261 @@ +/* eslint-disable sonarjs/no-duplicate-string */ +/* eslint-disable no-restricted-syntax */ +/* eslint-disable no-await-in-loop */ +import { + initialQueriesMap, + initialQueryBuilderFormValues, + PANEL_TYPES, +} from 'constants/queryBuilder'; +import { noop } from 'lodash-es'; +import { screen, waitFor } from 'tests/test-utils'; +import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { Query } from 'types/api/queryBuilder/queryBuilderData'; + +import { AllTraceFilterKeyValue } from '../Filter/filterUtils'; + +export const optionMenuReturn = { + options: { + 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', + }, + { + key: 'statusCode', + dataType: 'float64', + type: 'tag', + isColumn: true, + isJSON: false, + id: 'statusCode--float64--tag--true', + }, + { + key: 'dbName', + dataType: 'string', + type: 'tag', + isColumn: true, + isJSON: false, + id: 'dbName--string--tag--true', + }, + ], + maxLines: 2, + format: 'list', + }, + handleOptionsChange: jest.fn(), + config: { + addColumn: { + isFetching: false, + value: [ + { + 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', + }, + { + key: 'statusCode', + dataType: 'float64', + type: 'tag', + isColumn: true, + isJSON: false, + id: 'statusCode--float64--tag--true', + }, + { + key: 'dbName', + dataType: 'string', + type: 'tag', + isColumn: true, + isJSON: false, + id: 'dbName--string--tag--true', + }, + ], + options: [], + }, + format: { + value: 'list', + }, + maxLines: { + value: 2, + }, + }, +}; + +export const compositeQuery: Query = { + ...initialQueriesMap.traces, + builder: { + ...initialQueriesMap.traces.builder, + queryData: [ + { + ...initialQueryBuilderFormValues, + filters: { + items: [ + { + id: '95564eb1', + key: { + key: 'name', + dataType: DataTypes.String, + type: 'tag', + isColumn: true, + isJSON: false, + id: 'name--string--tag--true', + }, + op: 'in', + value: ['HTTP GET /customer'], + }, + { + id: '3337951c', + key: { + key: 'serviceName', + dataType: DataTypes.String, + type: 'tag', + isColumn: true, + isJSON: false, + id: 'serviceName--string--tag--true', + }, + op: 'in', + value: ['demo-app'], + }, + ], + op: 'AND', + }, + }, + ], + }, +}; + +export const redirectWithQueryBuilderData = jest.fn(); + +export const qbProviderValue = { + currentQuery: { + ...initialQueriesMap.traces, + builder: { + ...initialQueriesMap.traces.builder, + queryData: [initialQueryBuilderFormValues], + }, + }, + redirectWithQueryBuilderData, + panelType: PANEL_TYPES.LIST, + setSupersetQuery: jest.fn(), + supersetQuery: initialQueriesMap.traces, + stagedQuery: initialQueriesMap.traces, + initialDataSource: null, + isEnabledQuery: false, + handleSetQueryData: noop, + handleSetFormulaData: noop, + handleSetQueryItemData: noop, + handleSetConfig: noop, + removeQueryBuilderEntityByIndex: noop, + removeQueryTypeItemByIndex: noop, + addNewBuilderQuery: noop, + cloneQuery: noop, + addNewFormula: noop, + addNewQueryItem: noop, + handleRunQuery: noop, + resetQuery: noop, + updateAllQueriesOperators: (): Query => initialQueriesMap.traces, + updateQueriesData: (): Query => initialQueriesMap.traces, + initQueryBuilderData: noop, + handleOnUnitsChange: noop, + isStagedQueryUpdated: (): boolean => false, +} as any; + +export function checkIfSectionIsOpen( + getByTestId: (testId: string) => HTMLElement, + panelName: string, +): void { + const section = getByTestId(`collapse-${panelName}`); + expect(section.querySelector('.ant-collapse-item-active')).not.toBeNull(); +} + +export function checkIfSectionIsNotOpen( + getByTestId: (testId: string) => HTMLElement, + panelName: string, +): void { + const section = getByTestId(`collapse-${panelName}`); + expect(section.querySelector('.ant-collapse-item-active')).toBeNull(); +} + +export const defaultOpenSections = ['hasError', 'durationNano', 'serviceName']; + +export const defaultClosedSections = Object.keys(AllTraceFilterKeyValue).filter( + (section) => + ![...defaultOpenSections, 'durationNanoMin', 'durationNanoMax'].includes( + section, + ), +); + +export async function checkForSectionContent(values: string[]): Promise { + for (const val of values) { + const sectionContent = await screen.findByText(val); + await waitFor(() => expect(sectionContent).toBeInTheDocument()); + } +} diff --git a/frontend/src/types/api/queryBuilder/getAttributeSuggestions.ts b/frontend/src/types/api/queryBuilder/getAttributeSuggestions.ts new file mode 100644 index 0000000000..b30f386382 --- /dev/null +++ b/frontend/src/types/api/queryBuilder/getAttributeSuggestions.ts @@ -0,0 +1,15 @@ +import { DataSource } from 'types/common/queryBuilder'; + +import { BaseAutocompleteData } from './queryAutocompleteResponse'; +import { TagFilter } from './queryBuilderData'; + +export interface IGetAttributeSuggestionsPayload { + dataSource: DataSource; + searchText: string; + filters: TagFilter; +} + +export interface IGetAttributeSuggestionsSuccessResponse { + attributes: BaseAutocompleteData[]; + example_queries: TagFilter[]; +} diff --git a/frontend/src/types/api/queryBuilder/queryAutocompleteResponse.ts b/frontend/src/types/api/queryBuilder/queryAutocompleteResponse.ts index 67503761ca..a8d5f0f324 100644 --- a/frontend/src/types/api/queryBuilder/queryAutocompleteResponse.ts +++ b/frontend/src/types/api/queryBuilder/queryAutocompleteResponse.ts @@ -21,6 +21,7 @@ export interface BaseAutocompleteData { key: string; type: AutocompleteType | string | null; isJSON?: boolean; + isIndexed?: boolean; } export interface IQueryAutocompleteResponse { diff --git a/frontend/src/typings/environment.ts b/frontend/src/typings/environment.ts index c77832ce43..58d01f2f69 100644 --- a/frontend/src/typings/environment.ts +++ b/frontend/src/typings/environment.ts @@ -3,6 +3,7 @@ declare global { namespace NodeJS { interface ProcessEnv { FRONTEND_API_ENDPOINT: string | undefined; + WEBSOCKET_API_ENDPOINT: string | undefined; } } } diff --git a/frontend/src/utils/permission/index.ts b/frontend/src/utils/permission/index.ts index 8af1c68f3f..1845e77941 100644 --- a/frontend/src/utils/permission/index.ts +++ b/frontend/src/utils/permission/index.ts @@ -52,6 +52,8 @@ export const routePermission: Record = { ALL_CHANNELS: ['ADMIN', 'EDITOR', 'VIEWER'], INGESTION_SETTINGS: ['ADMIN', 'EDITOR', 'VIEWER'], ALL_DASHBOARD: ['ADMIN', 'EDITOR', 'VIEWER'], + MESSAGING_QUEUES: ['ADMIN', 'EDITOR', 'VIEWER'], + MESSAGING_QUEUES_DETAIL: ['ADMIN', 'EDITOR', 'VIEWER'], ALL_ERROR: ['ADMIN', 'EDITOR', 'VIEWER'], APPLICATION: ['ADMIN', 'EDITOR', 'VIEWER'], CHANNELS_EDIT: ['ADMIN'], @@ -95,7 +97,7 @@ export const routePermission: Record = { TRACES_SAVE_VIEWS: ['ADMIN', 'EDITOR', 'VIEWER'], API_KEYS: ['ADMIN'], LOGS_BASE: [], - OLD_LOGS_EXPLORER: [], + OLD_LOGS_EXPLORER: ['ADMIN', 'EDITOR', 'VIEWER'], SHORTCUTS: ['ADMIN', 'EDITOR', 'VIEWER'], INTEGRATIONS: ['ADMIN', 'EDITOR', 'VIEWER'], SERVICE_TOP_LEVEL_OPERATIONS: ['ADMIN', 'EDITOR', 'VIEWER'], diff --git a/frontend/webpack.config.js b/frontend/webpack.config.js index 2e5c0d0f4e..8fc1188448 100644 --- a/frontend/webpack.config.js +++ b/frontend/webpack.config.js @@ -38,6 +38,7 @@ const plugins = [ 'process.env': JSON.stringify({ NODE_ENV: process.env.NODE_ENV, FRONTEND_API_ENDPOINT: process.env.FRONTEND_API_ENDPOINT, + WEBSOCKET_API_ENDPOINT: process.env.WEBSOCKET_API_ENDPOINT, INTERCOM_APP_ID: process.env.INTERCOM_APP_ID, SEGMENT_ID: process.env.SEGMENT_ID, POSTHOG_KEY: process.env.POSTHOG_KEY, diff --git a/frontend/webpack.config.prod.js b/frontend/webpack.config.prod.js index 87ef8b7143..f9af80bcf5 100644 --- a/frontend/webpack.config.prod.js +++ b/frontend/webpack.config.prod.js @@ -48,6 +48,7 @@ const plugins = [ new webpack.DefinePlugin({ 'process.env': JSON.stringify({ FRONTEND_API_ENDPOINT: process.env.FRONTEND_API_ENDPOINT, + WEBSOCKET_API_ENDPOINT: process.env.WEBSOCKET_API_ENDPOINT, INTERCOM_APP_ID: process.env.INTERCOM_APP_ID, SEGMENT_ID: process.env.SEGMENT_ID, POSTHOG_KEY: process.env.POSTHOG_KEY, diff --git a/go.mod b/go.mod index 6f13479066..c976fa2739 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/ClickHouse/clickhouse-go/v2 v2.23.2 github.com/DATA-DOG/go-sqlmock v1.5.2 github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd - github.com/SigNoz/signoz-otel-collector v0.102.4 + github.com/SigNoz/signoz-otel-collector v0.102.7 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 @@ -38,7 +38,7 @@ require ( github.com/opentracing/opentracing-go v1.2.0 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/pkg/errors v0.9.1 - github.com/prometheus/common v0.54.0 + github.com/prometheus/common v0.55.0 github.com/prometheus/prometheus v2.5.0+incompatible github.com/rs/cors v1.11.0 github.com/russellhaering/gosaml2 v0.9.0 @@ -49,21 +49,26 @@ require ( github.com/soheilhy/cmux v0.1.5 github.com/srikanthccv/ClickHouse-go-mock v0.8.0 github.com/stretchr/testify v1.9.0 - go.opentelemetry.io/collector/component v0.102.1 - go.opentelemetry.io/collector/confmap v0.102.1 - go.opentelemetry.io/collector/confmap/converter/expandconverter v0.102.0 - go.opentelemetry.io/collector/confmap/provider/fileprovider v0.102.0 - go.opentelemetry.io/collector/connector v0.102.0 - go.opentelemetry.io/collector/consumer v0.102.1 - go.opentelemetry.io/collector/exporter v0.102.0 - go.opentelemetry.io/collector/extension v0.102.1 - go.opentelemetry.io/collector/otelcol v0.102.0 - go.opentelemetry.io/collector/pdata v1.9.0 - go.opentelemetry.io/collector/processor v0.102.0 - go.opentelemetry.io/collector/receiver v0.102.0 - go.opentelemetry.io/collector/service v0.102.0 - go.opentelemetry.io/otel v1.27.0 - go.opentelemetry.io/otel/sdk v1.27.0 + go.opentelemetry.io/collector/component v0.103.0 + go.opentelemetry.io/collector/confmap v0.103.0 + go.opentelemetry.io/collector/confmap/converter/expandconverter v0.103.0 + go.opentelemetry.io/collector/confmap/provider/fileprovider v0.103.0 + go.opentelemetry.io/collector/connector v0.103.0 + go.opentelemetry.io/collector/consumer v0.103.0 + go.opentelemetry.io/collector/exporter v0.103.0 + go.opentelemetry.io/collector/extension v0.103.0 + go.opentelemetry.io/collector/otelcol v0.103.0 + go.opentelemetry.io/collector/pdata v1.10.0 + go.opentelemetry.io/collector/processor v0.103.0 + go.opentelemetry.io/collector/receiver v0.103.0 + go.opentelemetry.io/collector/service v0.103.0 + go.opentelemetry.io/contrib/bridges/otelzap v0.0.0-20240820072021-3fab5f5f20fb + go.opentelemetry.io/contrib/config v0.8.0 + go.opentelemetry.io/otel v1.28.0 + go.opentelemetry.io/otel/log v0.4.0 + go.opentelemetry.io/otel/metric v1.28.0 + go.opentelemetry.io/otel/sdk v1.28.0 + go.opentelemetry.io/otel/trace v1.28.0 go.uber.org/multierr v1.11.0 go.uber.org/zap v1.27.0 golang.org/x/crypto v0.24.0 @@ -71,8 +76,8 @@ require ( golang.org/x/net v0.26.0 golang.org/x/oauth2 v0.21.0 golang.org/x/text v0.16.0 - google.golang.org/grpc v1.64.1 - google.golang.org/protobuf v1.34.1 + google.golang.org/grpc v1.65.0 + google.golang.org/protobuf v1.34.2 gopkg.in/segmentio/analytics-go.v3 v3.1.0 gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 @@ -105,10 +110,10 @@ require ( github.com/go-faster/errors v0.7.1 // indirect github.com/go-jose/go-jose/v4 v4.0.1 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect - github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.2.6 // indirect - github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect + github.com/go-viper/mapstructure/v2 v2.0.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect @@ -139,6 +144,7 @@ require ( github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect github.com/oklog/run v1.1.0 // indirect github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.102.0 // indirect @@ -150,11 +156,11 @@ require ( github.com/prometheus/client_golang v1.19.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common/sigv4 v0.1.0 // indirect - github.com/prometheus/procfs v0.15.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect github.com/segmentio/asm v1.2.0 // indirect github.com/segmentio/backo-go v1.0.1 // indirect - github.com/shirou/gopsutil/v3 v3.24.4 // indirect + github.com/shirou/gopsutil/v4 v4.24.5 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect @@ -168,36 +174,35 @@ require ( github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/collector v0.102.1 // indirect - go.opentelemetry.io/collector/config/configtelemetry v0.102.1 // indirect - go.opentelemetry.io/collector/confmap/provider/envprovider v0.102.0 // indirect - go.opentelemetry.io/collector/confmap/provider/httpprovider v0.102.0 // indirect - go.opentelemetry.io/collector/confmap/provider/httpsprovider v0.102.0 // indirect - go.opentelemetry.io/collector/confmap/provider/yamlprovider v0.102.0 // indirect - go.opentelemetry.io/collector/featuregate v1.9.0 // indirect - go.opentelemetry.io/collector/semconv v0.102.0 // indirect - go.opentelemetry.io/contrib/config v0.7.0 // indirect + go.opentelemetry.io/collector v0.103.0 // indirect + go.opentelemetry.io/collector/config/configtelemetry v0.103.0 // indirect + go.opentelemetry.io/collector/confmap/provider/envprovider v0.103.0 // indirect + go.opentelemetry.io/collector/confmap/provider/httpprovider v0.103.0 // indirect + go.opentelemetry.io/collector/confmap/provider/httpsprovider v0.103.0 // indirect + go.opentelemetry.io/collector/confmap/provider/yamlprovider v0.103.0 // indirect + go.opentelemetry.io/collector/featuregate v1.13.0 // indirect + go.opentelemetry.io/collector/semconv v0.103.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 // indirect go.opentelemetry.io/contrib/propagators/b3 v1.27.0 // indirect go.opentelemetry.io/otel/bridge/opencensus v1.27.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.27.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.27.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 // indirect - go.opentelemetry.io/otel/exporters/prometheus v0.49.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.27.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.27.0 // indirect - go.opentelemetry.io/otel/metric v1.27.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.27.0 // indirect - go.opentelemetry.io/otel/trace v1.27.0 // indirect - go.opentelemetry.io/proto/otlp v1.2.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.4.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.28.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.28.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 // indirect + go.opentelemetry.io/otel/exporters/prometheus v0.50.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.28.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.28.0 // indirect + go.opentelemetry.io/otel/sdk/log v0.4.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.28.0 // indirect + go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/atomic v1.11.0 // indirect golang.org/x/sys v0.21.0 // indirect golang.org/x/time v0.5.0 // indirect gonum.org/v1/gonum v0.15.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect gopkg.in/ini.v1 v1.67.0 // indirect k8s.io/client-go v0.29.3 // indirect k8s.io/klog/v2 v2.120.1 // indirect diff --git a/go.sum b/go.sum index f19e9bcb46..3807486316 100644 --- a/go.sum +++ b/go.sum @@ -64,8 +64,8 @@ github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd h1:Bk43AsDYe0fhkb github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd/go.mod h1:nxRcH/OEdM8QxzH37xkGzomr1O0JpYBRS6pwjsWW6Pc= github.com/SigNoz/prometheus v1.11.1 h1:roM8ugYf4UxaeKKujEeBvoX7ybq3IrS+TB26KiRtIJg= github.com/SigNoz/prometheus v1.11.1/go.mod h1:uv4mQwZQtx7y4GQ6EdHOi8Wsk07uHNn2XHd1zM85m6I= -github.com/SigNoz/signoz-otel-collector v0.102.4 h1:098regGkGcrv0eSGElD4uEyuChxLM9VUoeWMW6KrUTI= -github.com/SigNoz/signoz-otel-collector v0.102.4/go.mod h1:3s9cSL8yexkBBMfK9mC3WWrAPm8oMtlZhvBxvt+Ziag= +github.com/SigNoz/signoz-otel-collector v0.102.7 h1:UBjO88GNCGZuWKl1LFukRahR1cu9AGwFHyObo07RrYA= +github.com/SigNoz/signoz-otel-collector v0.102.7/go.mod h1:3s9cSL8yexkBBMfK9mC3WWrAPm8oMtlZhvBxvt+Ziag= 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= @@ -131,8 +131,8 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/xds/go v0.0.0-20240318125728-8a4994d93e50 h1:DBmgJDC9dTfkVyGgipamEh2BpGYxScCH1TOF1LL1cXc= -github.com/cncf/xds/go v0.0.0-20240318125728-8a4994d93e50/go.mod h1:5e1+Vvlzido69INQaVO6d87Qn543Xr6nooe9Kz7oBFM= +github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b h1:ga8SEFjZ60pxLcmhnThWgvH2wg8376yUJmPhEH4H3kw= +github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/coreos/go-oidc/v3 v3.10.0 h1:tDnXHnLyiTVyT/2zLDGj09pFPkhND8Gl8lnTRhoEaJU= github.com/coreos/go-oidc/v3 v3.10.0/go.mod h1:5j11xcw0D3+SGxn6Z/WFADsgcWVMyNAlSQupk0KK3ac= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= @@ -216,8 +216,8 @@ github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= @@ -240,8 +240,8 @@ github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqw github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= -github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 h1:TQcrn6Wq+sKGkpyPvppOz99zsMBaUOKXq6HSv655U1c= -github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc= +github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-zookeeper/zk v1.0.3 h1:7M2kwOsc//9VeeFiPtf+uSJlVpU66x9Ba5+8XK7/TDg= github.com/go-zookeeper/zk v1.0.3/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -486,7 +486,6 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/linode/linodego v1.35.0 h1:rIhUeCHBLEDlkoRnOTwzSGzljQ3ksXwLxacmXnrV+Do= github.com/linode/linodego v1.35.0/go.mod h1:JxuhOEAMfSxun6RU5/MgTKH2GGTmFrhKRj3wL1NFin0= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c h1:VtwQ41oftZwlMnOEbMWQtSEUgU64U4s+GHk7hZK+jtY= github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= @@ -624,7 +623,6 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= -github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c h1:NRoLoZvkBTKvR5gQLgA3e0hqjkY9u1wm+iOL45VN/qI= github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= @@ -644,16 +642,16 @@ github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8 github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/common v0.29.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/common v0.54.0 h1:ZlZy0BgJhTwVZUn7dLOkwCZHUkrAqd3WYtcFCWnM1D8= -github.com/prometheus/common v0.54.0/go.mod h1:/TQgMJP5CuVYveyT7n/0Ix8yLNNXy9yRSkhnLTHPDIQ= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= github.com/prometheus/common/sigv4 v0.1.0 h1:qoVebwtwwEhS85Czm2dSROY5fTo2PAPEVdDeppTwGX4= github.com/prometheus/common/sigv4 v0.1.0/go.mod h1:2Jkxxk9yYvCkE5G1sQT7GuEXm57JrvHu9k5YwTjsNtI= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.15.0 h1:A82kmvXJq2jTu5YUhSGNlYoxh85zLnKgPz4bMZgI5Ek= -github.com/prometheus/procfs v0.15.0/go.mod h1:Y0RJ/Y5g5wJpkTisOtqwDSo4HwhGmLB4VQSw2sQJLHk= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= @@ -685,8 +683,8 @@ github.com/segmentio/backo-go v1.0.1 h1:68RQccglxZeyURy93ASB/2kc9QudzgIDexJ927N+ github.com/segmentio/backo-go v1.0.1/go.mod h1:9/Rh6yILuLysoQnZ2oNooD2g7aBnvM7r/fNVxRNWfBc= github.com/sethvargo/go-password v0.2.0 h1:BTDl4CC/gjf/axHMaDQtw507ogrXLci6XRiLc7i/UHI= github.com/sethvargo/go-password v0.2.0/go.mod h1:Ym4Mr9JXLBycr02MFuVQ/0JHidNetSgbzutTr3zsYXE= -github.com/shirou/gopsutil/v3 v3.24.4 h1:dEHgzZXt4LMNm+oYELpzl9YCqV65Yr/6SfrvgRBtXeU= -github.com/shirou/gopsutil/v3 v3.24.4/go.mod h1:lTd2mdiOspcqLgAnr9/nGi71NkeMpWKdmhuxm9GusH8= +github.com/shirou/gopsutil/v4 v4.24.5 h1:gGsArG5K6vmsh5hcFOHaPm87UD003CaDMkAOweSQjhM= +github.com/shirou/gopsutil/v4 v4.24.5/go.mod h1:aoebb2vxetJ/yIDZISmduFvVNPHqXQ9SEJwRXxkf0RA= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= @@ -734,7 +732,6 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= @@ -774,92 +771,112 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/collector v0.102.1 h1:M/ciCcReQsSDYG9bJ2Qwqk7pQILDJ2bM/l0MdeCAvJE= -go.opentelemetry.io/collector v0.102.1/go.mod h1:yF1lDRgL/Eksb4/LUnkMjvLvHHpi6wqBVlzp+dACnPM= -go.opentelemetry.io/collector/component v0.102.1 h1:66z+LN5dVCXhvuVKD1b56/3cYLK+mtYSLIwlskYA9IQ= -go.opentelemetry.io/collector/component v0.102.1/go.mod h1:XfkiSeImKYaewT2DavA80l0VZ3JjvGndZ8ayPXfp8d0= -go.opentelemetry.io/collector/config/confignet v0.102.1 h1:nSiAFQMzNCO4sDBztUxY73qFw4Vh0hVePq8+3wXUHtU= -go.opentelemetry.io/collector/config/confignet v0.102.1/go.mod h1:pfOrCTfSZEB6H2rKtx41/3RN4dKs+X2EKQbw3MGRh0E= -go.opentelemetry.io/collector/config/configtelemetry v0.102.1 h1:f/CYcrOkaHd+COIJ2lWnEgBCHfhEycpbow4ZhrGwAlA= -go.opentelemetry.io/collector/config/configtelemetry v0.102.1/go.mod h1:WxWKNVAQJg/Io1nA3xLgn/DWLE/W1QOB2+/Js3ACi40= -go.opentelemetry.io/collector/confmap v0.102.1 h1:wZuH+d/P11Suz8wbp+xQCJ0BPE9m5pybtUe74c+rU7E= -go.opentelemetry.io/collector/confmap v0.102.1/go.mod h1:KgpS7UxH5rkd69CzAzlY2I1heH8Z7eNCZlHmwQBMxNg= -go.opentelemetry.io/collector/confmap/converter/expandconverter v0.102.0 h1:8Ne/oL6M4kMWK0P3FKV9EduQa+1UOGyVAnFHfSo4c1A= -go.opentelemetry.io/collector/confmap/converter/expandconverter v0.102.0/go.mod h1:Xj4Ld/RriP/Bj+5oPpaYJsLNs2wWRDN2TvzX3Lbi6+M= -go.opentelemetry.io/collector/confmap/provider/envprovider v0.102.0 h1:o1iKqN+oM+TZqHoGdnKw1Am2BQlIGCYbxCRzU8T3jbM= -go.opentelemetry.io/collector/confmap/provider/envprovider v0.102.0/go.mod h1:JpCemLtL/sXQ2Rk3Bx7OPPA7Qt/9NVH91q0bR655gSo= -go.opentelemetry.io/collector/confmap/provider/fileprovider v0.102.0 h1:SfASE6lxXjrmYj/UibcWdOiFWvRG0zt4hJgenloEhlY= -go.opentelemetry.io/collector/confmap/provider/fileprovider v0.102.0/go.mod h1:+Ku0Fvdb5f6e9UkfqJXAV5FaUJVxxg6Ykfx7Js8y+V4= -go.opentelemetry.io/collector/confmap/provider/httpprovider v0.102.0 h1:GwJQTXs7pYPUv/fVf+0nBgsJdlrTuY/PfwQ/TRA/sIk= -go.opentelemetry.io/collector/confmap/provider/httpprovider v0.102.0/go.mod h1:PGE3DcRgqYWWC2cq2hYZoET1d3Q8JZyPNmgvqXPFWEU= -go.opentelemetry.io/collector/confmap/provider/httpsprovider v0.102.0 h1:zdYZLiHHtDf4Kk9WU7mW9dW6WAXtBF54I5jmTMRJtiw= -go.opentelemetry.io/collector/confmap/provider/httpsprovider v0.102.0/go.mod h1:yFsgUM0PbUJkPlbpJfOG6da+YiF0Z80tv7YcnL3qwv4= -go.opentelemetry.io/collector/confmap/provider/yamlprovider v0.102.0 h1:Y4H+GaCQl2URp9mEJMV5CYOhw+erONqNyvtFoKQfIoA= -go.opentelemetry.io/collector/confmap/provider/yamlprovider v0.102.0/go.mod h1:g1RjfVD0gHAf/mPOIs3zBoKBeuDsN+rc5x0lZtgA8tI= -go.opentelemetry.io/collector/connector v0.102.0 h1:IvAsVfYRxP0ajmKbUovF8qugkcUtHq6RuYNtjcMa63E= -go.opentelemetry.io/collector/connector v0.102.0/go.mod h1:f4M7wZ/9+XtgTE0fivBFH3WlwntaEd0qFFA0giFkdnY= -go.opentelemetry.io/collector/consumer v0.102.1 h1:0CkgHhxwx4lI/m+hWjh607xyjooW5CObZ8hFQy5vvo0= -go.opentelemetry.io/collector/consumer v0.102.1/go.mod h1:HoXqmrRV13jLnP3/Gg3fYNdRkDPoO7UW58hKiLyFF60= -go.opentelemetry.io/collector/exporter v0.102.0 h1:hvyTyyGVx5FIikA6HzlTeZHILJ62hrIBsoZCoKlpX3A= -go.opentelemetry.io/collector/exporter v0.102.0/go.mod h1:JWE+1qNoSVBSelzhI3Iao/VkYVssY+sXaTPK1JOmpQ0= -go.opentelemetry.io/collector/extension v0.102.1 h1:gAvE3w15q+Vv0Tj100jzcDpeMTyc8dAiemHRtJbspLg= -go.opentelemetry.io/collector/extension v0.102.1/go.mod h1:XBxUOXjZpwYLZYOK5u3GWlbBTOKmzStY5eU1R/aXkIo= -go.opentelemetry.io/collector/extension/zpagesextension v0.102.0 h1:BPq98py8nwzaV7KAsxt4ZZAF9LiSRu7ZjHNGavFNyKo= -go.opentelemetry.io/collector/extension/zpagesextension v0.102.0/go.mod h1:P86HW3x3epDS5F4yP0gAvsZiw4xxP1OupTEx2o6UqjY= -go.opentelemetry.io/collector/featuregate v1.9.0 h1:mC4/HnR5cx/kkG1RKOQAvHxxg5Ktmd9gpFdttPEXQtA= -go.opentelemetry.io/collector/featuregate v1.9.0/go.mod h1:PsOINaGgTiFc+Tzu2K/X2jP+Ngmlp7YKGV1XrnBkH7U= -go.opentelemetry.io/collector/otelcol v0.102.0 h1:HuE+ok4iUjOrmYhQBSWpG5kBTVhcA24ljPL4pBERZ5E= -go.opentelemetry.io/collector/otelcol v0.102.0/go.mod h1:w8pCRu2nM/jAkLlEAS6ccKtJv5ylUQe6Ugl98zzTfyE= -go.opentelemetry.io/collector/pdata v1.9.0 h1:qyXe3HEVYYxerIYu0rzgo1Tx2d1Zs6iF+TCckbHLFOw= -go.opentelemetry.io/collector/pdata v1.9.0/go.mod h1:vk7LrfpyVpGZrRWcpjyy0DDZzL3SZiYMQxfap25551w= -go.opentelemetry.io/collector/pdata/testdata v0.102.1 h1:S3idZaJxy8M7mCC4PG4EegmtiSaOuh6wXWatKIui8xU= -go.opentelemetry.io/collector/pdata/testdata v0.102.1/go.mod h1:JEoSJTMgeTKyGxoMRy48RMYyhkA5vCCq/abJq9B6vXs= -go.opentelemetry.io/collector/processor v0.102.0 h1:JsjTlpBRmoSYxcu3cAbKBchOmL6aNUxLa03ZkWIqZr8= -go.opentelemetry.io/collector/processor v0.102.0/go.mod h1:IaCSDcfy75uQTaOM+LgR1bMf/bUw2eFfzn20uvWYfLQ= -go.opentelemetry.io/collector/receiver v0.102.0 h1:8rHNjWjV90bL0dgvKVc/7D10NCbM7bXCiqpcLRz5jBI= -go.opentelemetry.io/collector/receiver v0.102.0/go.mod h1:bYDwYItMrj7Drx0Pn4wZQ8Ii67lp9Nta62gbau93FhA= -go.opentelemetry.io/collector/semconv v0.102.0 h1:VEOdog9IbSfaGR7yg4AVmT54MwHAgH9lzITH6C33uyc= -go.opentelemetry.io/collector/semconv v0.102.0/go.mod h1:yMVUCNoQPZVq/IPfrHrnntZTWsLf5YGZ7qwKulIl5hw= -go.opentelemetry.io/collector/service v0.102.0 h1:B5nfyQZF7eB/y+yucl9G/7VsusbXixYXWingXn7VszM= -go.opentelemetry.io/collector/service v0.102.0/go.mod h1:c+0n0DfQeCjgrdplNHYwYbG/5aupTZVYU/50nMQraoc= -go.opentelemetry.io/contrib/config v0.7.0 h1:b1rK5tGTuhhPirJiMxOcyQfZs76j2VapY6ODn3b2Dbs= -go.opentelemetry.io/contrib/config v0.7.0/go.mod h1:8tdiFd8N5etOi3XzBmAoMxplEzI3TcL8dU5rM5/xcOQ= +go.opentelemetry.io/collector v0.103.0 h1:mssWo1y31p1F/SRsSBnVUX6YocgawCqM1blpE+hkWog= +go.opentelemetry.io/collector v0.103.0/go.mod h1:mgqdTFB7QCYiOeEdJSSEktovPqy+2fw4oTKJzyeSB0U= +go.opentelemetry.io/collector/component v0.103.0 h1:j52YAsp8EmqYUotVUwhovkqFZGuxArEkk65V4TI46NE= +go.opentelemetry.io/collector/component v0.103.0/go.mod h1:jKs19tGtCO8Hr5/YM0F+PoFcl8SVe/p4Ge30R6srkbc= +go.opentelemetry.io/collector/config/configauth v0.103.0 h1:tv2Ilj0X9T8ZsDd4mB8Sl+nXQ8CG8MJVQ1Lo4mmE0Pk= +go.opentelemetry.io/collector/config/configauth v0.103.0/go.mod h1:VIo8DpFeyOOCMUVoQsBdq3t2snUiBBECP0UxW1bwz/o= +go.opentelemetry.io/collector/config/configcompression v1.10.0 h1:ClkAY1rzaxFawmC53BUf3TjTWKOGx+2xnpqOJIkg6Tk= +go.opentelemetry.io/collector/config/configcompression v1.10.0/go.mod h1:6+m0GKCv7JKzaumn7u80A2dLNCuYf5wdR87HWreoBO0= +go.opentelemetry.io/collector/config/confighttp v0.103.0 h1:tgCWMKuIorSr4+iQOv0A8Ya/8do73hiG5KHinWaz63Q= +go.opentelemetry.io/collector/config/confighttp v0.103.0/go.mod h1:xMXoLsTGTJlftu+VAL3iadEs4gkmqFrvuPPnpNi6ETo= +go.opentelemetry.io/collector/config/configopaque v1.10.0 h1:FAxj6ggLpJE/kFnR1ezYwjRdo6gHo2+CjlIsHVCFVnQ= +go.opentelemetry.io/collector/config/configopaque v1.10.0/go.mod h1:0xURn2sOy5j4fbaocpEYfM97HPGsiffkkVudSPyTJlM= +go.opentelemetry.io/collector/config/configtelemetry v0.103.0 h1:KLbhkFqdw9D31t0IhJ/rnhMRvz/s14eie0fKfm5xWns= +go.opentelemetry.io/collector/config/configtelemetry v0.103.0/go.mod h1:WxWKNVAQJg/Io1nA3xLgn/DWLE/W1QOB2+/Js3ACi40= +go.opentelemetry.io/collector/config/configtls v0.103.0 h1:nbk8sJIHoYYQbpZtUkUQceTbjC4wEjoePKJ15v8cCcU= +go.opentelemetry.io/collector/config/configtls v0.103.0/go.mod h1:046dfdfHW8wWCMhzUaWJo7guRiCoSz5QzVjCSDzymdU= +go.opentelemetry.io/collector/config/internal v0.103.0 h1:pimS3uLHfOBbConZrviGoTwu+bkTNDoQBtbeWCg8U8k= +go.opentelemetry.io/collector/config/internal v0.103.0/go.mod h1:kJRkB+PgamWqPi/GWbYWvnRzVzS1rwDUh6+VSz4C7NQ= +go.opentelemetry.io/collector/confmap v0.103.0 h1:qKKZyWzropSKfgtGv12JzADOXNgThqH1Vx6qzblBE24= +go.opentelemetry.io/collector/confmap v0.103.0/go.mod h1:TlOmqe/Km3K6WgxyhEAdCb/V1Yp6eSU76fCoiluEa88= +go.opentelemetry.io/collector/confmap/converter/expandconverter v0.103.0 h1:zApcKLSosuu9I/4IRHTqlE1H6XNiZNAgd26YbzHwkto= +go.opentelemetry.io/collector/confmap/converter/expandconverter v0.103.0/go.mod h1:hoel+3CPjRhPSHzCrE1E+wCyoSLHlgW7662Ntwx2ujM= +go.opentelemetry.io/collector/confmap/provider/envprovider v0.103.0 h1:0XHQ/ffxSUx3sMbnYSf8a4jnVYLUrxo+/XwdhXkizgs= +go.opentelemetry.io/collector/confmap/provider/envprovider v0.103.0/go.mod h1:NiE4Fe42Sew1TyXuU1YEd0xZBDNI+w6IRkC2OTlJUak= +go.opentelemetry.io/collector/confmap/provider/fileprovider v0.103.0 h1:5dB2G7d6RKmWS8ptuAWvAEKGYODk2DTRm84bU9HooLQ= +go.opentelemetry.io/collector/confmap/provider/fileprovider v0.103.0/go.mod h1:GT/GBk17lDhc27762w6PNHvKYbA+TnHvNEyQHUsjKpY= +go.opentelemetry.io/collector/confmap/provider/httpprovider v0.103.0 h1:Hrp+nw4W9/jeJfi3GfJW6EYh7DeNkaC1wojOh4x8CbI= +go.opentelemetry.io/collector/confmap/provider/httpprovider v0.103.0/go.mod h1:kUst0pGVBlKDSlvJYDclrsApbkMv7ahRDh6/pE4LsBc= +go.opentelemetry.io/collector/confmap/provider/httpsprovider v0.103.0 h1:JUDRYsMOhkIBxZqZli0BU+64zahIUgnEPZSe9wo2T0Q= +go.opentelemetry.io/collector/confmap/provider/httpsprovider v0.103.0/go.mod h1:+mUrWjpdGIdSKMeeplLO+qXFSBc287as2oIPVdKMTxc= +go.opentelemetry.io/collector/confmap/provider/yamlprovider v0.103.0 h1:boTv+ZRkn1h5eUbt5sLSU5lCrItCCxCen/laRmsHLyg= +go.opentelemetry.io/collector/confmap/provider/yamlprovider v0.103.0/go.mod h1:0pZ7RD7SPg+yklgGPN+74Zzbps4R9x5bRPZX1D1gtGM= +go.opentelemetry.io/collector/connector v0.103.0 h1:jwmrgCT6ftz3U4o8mAqP+/yaQ5KsLMFXo2+OHXhy+tE= +go.opentelemetry.io/collector/connector v0.103.0/go.mod h1:6RDaeDMiXTKEXSy1eIaO0EiM+/91NVHdBxOc9e2++2A= +go.opentelemetry.io/collector/consumer v0.103.0 h1:L/7SA/U2ua5L4yTLChnI9I+IFGKYU5ufNQ76QKYcPYs= +go.opentelemetry.io/collector/consumer v0.103.0/go.mod h1:7jdYb9kSSOsu2R618VRX0VJ+Jt3OrDvvUsDToHTEOLI= +go.opentelemetry.io/collector/exporter v0.103.0 h1:g0nF/FAwuA7tTJf5uo1PvlQl7xFqCgvfH+FYqufBSiw= +go.opentelemetry.io/collector/exporter v0.103.0/go.mod h1:PC2OvciPEew2kaA/ZMyxRqfeOW8Wgi0CYR614PEyS/w= +go.opentelemetry.io/collector/extension v0.103.0 h1:vTsd+GElvT7qKk9Y9d6UKuuT2Ngx0mai8Q48hkKQMwM= +go.opentelemetry.io/collector/extension v0.103.0/go.mod h1:rp2l3xskNKWv0yBCyU69Pv34TnP1QVD1ijr0zSndnsM= +go.opentelemetry.io/collector/extension/auth v0.103.0 h1:i7cQl+Ewpve/DIN4rFMg1GiyUPE14LZsYWrJ1RqtP84= +go.opentelemetry.io/collector/extension/auth v0.103.0/go.mod h1:JdYBS/EkPAz2APAi8g7xTiSRlZTc7c4H82AQM9epzxw= +go.opentelemetry.io/collector/extension/zpagesextension v0.103.0 h1:jgSEQY++zOI6hFQygwuvS6ulJ/Yu4xXgUg+Ijoxx51I= +go.opentelemetry.io/collector/extension/zpagesextension v0.103.0/go.mod h1:2OUi0Hp+3zPUJmi7goJ6d1/kGgFAw3SDESRX7xQ0QHE= +go.opentelemetry.io/collector/featuregate v1.13.0 h1:rc84eCf5hesXQ8/bP6Zc15wqthbomfLBHmox5tT7AwM= +go.opentelemetry.io/collector/featuregate v1.13.0/go.mod h1:PsOINaGgTiFc+Tzu2K/X2jP+Ngmlp7YKGV1XrnBkH7U= +go.opentelemetry.io/collector/otelcol v0.103.0 h1:Skqnc2mxDdk3eiYioUuG7ST6ur5k83SOv7mIBt60fBw= +go.opentelemetry.io/collector/otelcol v0.103.0/go.mod h1:iJF3ghCv+nRZI6+hI7z3kGRZrgH///Fd9tNXY82X90g= +go.opentelemetry.io/collector/pdata v1.10.0 h1:oLyPLGvPTQrcRT64ZVruwvmH/u3SHTfNo01pteS4WOE= +go.opentelemetry.io/collector/pdata v1.10.0/go.mod h1:IHxHsp+Jq/xfjORQMDJjSH6jvedOSTOyu3nbxqhWSYE= +go.opentelemetry.io/collector/pdata/testdata v0.103.0 h1:iI6NOE0L2je/bxlWzAWHQ/yCtnGupgv42Hl9Al1q/g4= +go.opentelemetry.io/collector/pdata/testdata v0.103.0/go.mod h1:tLzRhb/h37/9wFRQVr+CxjKi5qmhSRpCAiOlhwRkeEk= +go.opentelemetry.io/collector/processor v0.103.0 h1:YZ+LRuHKtOam7SCeLkJAP6bS1d6XxeYP22OyMN3VP0s= +go.opentelemetry.io/collector/processor v0.103.0/go.mod h1:/mxyh0NpJgpZycm7iHDpM7i5PdtWvKKdCZf0cyADJfU= +go.opentelemetry.io/collector/receiver v0.103.0 h1:V3JBKkX+7e/NYpDDZVyeu2VQB1/lLFuoJFPfupdCcZs= +go.opentelemetry.io/collector/receiver v0.103.0/go.mod h1:Yybv4ynKFdMOYViWWPMmjkugR89FSQN0P37wP6mX6qM= +go.opentelemetry.io/collector/semconv v0.103.0 h1:5tlVoZlo9USHAU2Bz4YrEste0Vm5AMufXkYJhAVve1Q= +go.opentelemetry.io/collector/semconv v0.103.0/go.mod h1:yMVUCNoQPZVq/IPfrHrnntZTWsLf5YGZ7qwKulIl5hw= +go.opentelemetry.io/collector/service v0.103.0 h1:e4Eri4jo+YOuEK0+/JE9SUdT/NZaJ2jz/ROJlmLn96s= +go.opentelemetry.io/collector/service v0.103.0/go.mod h1:p1mlniiC1MuPN5FANYJYgf5V5CGFP0hNqWfI8t7Aw8M= +go.opentelemetry.io/contrib/bridges/otelzap v0.0.0-20240820072021-3fab5f5f20fb h1:ZqncifxU0B1q64FRbhKxsJugRsrEToakmYUsgQ5tGbY= +go.opentelemetry.io/contrib/bridges/otelzap v0.0.0-20240820072021-3fab5f5f20fb/go.mod h1:mzv0k5dTnSUE5/ZerXUwGiNKzcPJTakuCh6Wm1emNvU= +go.opentelemetry.io/contrib/config v0.8.0 h1:OD7aDMhL+2EpzdSHfkDmcdD/uUA+PgKM5faFyF9XFT0= +go.opentelemetry.io/contrib/config v0.8.0/go.mod h1:dGeVZWE//3wrxYHHP0iCBYJU1QmOmPcbV+FNB7pjDYI= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 h1:9l89oX4ba9kHbBol3Xin3leYJ+252h0zszDtBwyKe2A= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0/go.mod h1:XLZfZboOJWHNKUv7eH0inh0E9VV6eWDFB/9yJyTLPp0= go.opentelemetry.io/contrib/propagators/b3 v1.27.0 h1:IjgxbomVrV9za6bRi8fWCNXENs0co37SZedQilP2hm0= go.opentelemetry.io/contrib/propagators/b3 v1.27.0/go.mod h1:Dv9obQz25lCisDvvs4dy28UPh974CxkahRDUPsY7y9E= go.opentelemetry.io/contrib/zpages v0.52.0 h1:MPgkMy0Cp3O5EdfVXP0ss3ujhEibysTM4eszx7E7d+E= go.opentelemetry.io/contrib/zpages v0.52.0/go.mod h1:fqG5AFdoYru3A3DnhibVuaaEfQV2WKxE7fYE1jgDRwk= -go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg= -go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ= +go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= +go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= go.opentelemetry.io/otel/bridge/opencensus v1.27.0 h1:ao9aGGHd+G4YfjBpGs6vbkvt5hoC67STlJA9fCnOAcs= go.opentelemetry.io/otel/bridge/opencensus v1.27.0/go.mod h1:uRvWtAAXzyVOST0WMPX5JHGBaAvBws+2F8PcC5gMnTk= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.27.0 h1:bFgvUr3/O4PHj3VQcFEuYKvRZJX1SJDQ+11JXuSB3/w= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.27.0/go.mod h1:xJntEd2KL6Qdg5lwp97HMLQDVeAhrYxmzFseAMDPQ8I= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.27.0 h1:CIHWikMsN3wO+wq1Tp5VGdVRTcON+DmOJSfDjXypKOc= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.27.0/go.mod h1:TNupZ6cxqyFEpLXAZW7On+mLFL0/g0TE3unIYL91xWc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 h1:R9DE4kQ4k+YtfLI2ULwX82VtNQ2J8yZmA7ZIF/D+7Mc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0/go.mod h1:OQFyQVrDlbe+R7xrEyDr/2Wr67Ol0hRUgsfA+V5A95s= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 h1:QY7/0NeRPKlzusf40ZE4t1VlMKbqSNT7cJRYzWuja0s= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0/go.mod h1:HVkSiDhTM9BoUJU8qE6j2eSWLLXvi1USXjyd2BXT8PY= -go.opentelemetry.io/otel/exporters/prometheus v0.49.0 h1:Er5I1g/YhfYv9Affk9nJLfH/+qCCVVg1f2R9AbJfqDQ= -go.opentelemetry.io/otel/exporters/prometheus v0.49.0/go.mod h1:KfQ1wpjf3zsHjzP149P4LyAwWRupc6c7t1ZJ9eXpKQM= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.27.0 h1:/jlt1Y8gXWiHG9FBx6cJaIC5hYx5Fe64nC8w5Cylt/0= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.27.0/go.mod h1:bmToOGOBZ4hA9ghphIc1PAf66VA8KOtsuy3+ScStG20= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.27.0 h1:/0YaXu3755A/cFbtXp+21lkXgI0QE5avTWA2HjU9/WE= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.27.0/go.mod h1:m7SFxp0/7IxmJPLIY3JhOcU9CoFzDaCPL6xxQIxhA+o= -go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0FxV/ik= -go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak= -go.opentelemetry.io/otel/sdk v1.27.0 h1:mlk+/Y1gLPLn84U4tI8d3GNJmGT/eXe3ZuOXN9kTWmI= -go.opentelemetry.io/otel/sdk v1.27.0/go.mod h1:Ha9vbLwJE6W86YstIywK2xFfPjbWlCuwPtMkKdz/Y4A= -go.opentelemetry.io/otel/sdk/metric v1.27.0 h1:5uGNOlpXi+Hbo/DRoI31BSb1v+OGcpv2NemcCrOL8gI= -go.opentelemetry.io/otel/sdk/metric v1.27.0/go.mod h1:we7jJVrYN2kh3mVBlswtPU22K0SA+769l93J6bsyvqw= -go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw= -go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4= -go.opentelemetry.io/proto/otlp v1.2.0 h1:pVeZGk7nXDC9O2hncA6nHldxEjm6LByfA2aN8IOkz94= -go.opentelemetry.io/proto/otlp v1.2.0/go.mod h1:gGpR8txAl5M03pDhMC79G6SdqNV26naRm/KDsgaHD8A= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.4.0 h1:zBPZAISA9NOc5cE8zydqDiS0itvg/P/0Hn9m72a5gvM= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.4.0/go.mod h1:gcj2fFjEsqpV3fXuzAA+0Ze1p2/4MJ4T7d77AmkvueQ= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.28.0 h1:U2guen0GhqH8o/G2un8f/aG/y++OuW6MyCo6hT9prXk= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.28.0/go.mod h1:yeGZANgEcpdx/WK0IvvRFC+2oLiMS2u4L/0Rj2M2Qr0= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.28.0 h1:aLmmtjRke7LPDQ3lvpFz+kNEH43faFhzW7v8BFIEydg= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.28.0/go.mod h1:TC1pyCt6G9Sjb4bQpShH+P5R53pO6ZuGnHuuln9xMeE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 h1:R3X6ZXmNPRR8ul6i3WgFURCHzaXjHdm0karRG/+dj3s= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0/go.mod h1:QWFXnDavXWwMx2EEcZsf3yxgEKAqsxQ+Syjp+seyInw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 h1:j9+03ymgYhPKmeXGk5Zu+cIZOlVzd9Zv7QIiyItjFBU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0/go.mod h1:Y5+XiUG4Emn1hTfciPzGPJaSI+RpDts6BnCIir0SLqk= +go.opentelemetry.io/otel/exporters/prometheus v0.50.0 h1:2Ewsda6hejmbhGFyUvWZjUThC98Cf8Zy6g0zkIimOng= +go.opentelemetry.io/otel/exporters/prometheus v0.50.0/go.mod h1:pMm5PkUo5YwbLiuEf7t2xg4wbP0/eSJrMxIMxKosynY= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.28.0 h1:BJee2iLkfRfl9lc7aFmBwkWxY/RI1RDdXepSF6y8TPE= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.28.0/go.mod h1:DIzlHs3DRscCIBU3Y9YSzPfScwnYnzfnCd4g8zA7bZc= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.28.0 h1:EVSnY9JbEEW92bEkIYOVMw4q1WJxIAGoFTrtYOzWuRQ= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.28.0/go.mod h1:Ea1N1QQryNXpCD0I1fdLibBAIpQuBkznMmkdKrapk1Y= +go.opentelemetry.io/otel/log v0.4.0 h1:/vZ+3Utqh18e8TPjuc3ecg284078KWrR8BRz+PQAj3o= +go.opentelemetry.io/otel/log v0.4.0/go.mod h1:DhGnQvky7pHy82MIRV43iXh3FlKN8UUKftn0KbLOq6I= +go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= +go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= +go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= +go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= +go.opentelemetry.io/otel/sdk/log v0.4.0 h1:1mMI22L82zLqf6KtkjrRy5BbagOTWdJsqMY/HSqILAA= +go.opentelemetry.io/otel/sdk/log v0.4.0/go.mod h1:AYJ9FVF0hNOgAVzUG/ybg/QttnXhUePWAupmCqtdESo= +go.opentelemetry.io/otel/sdk/metric v1.28.0 h1:OkuaKgKrgAbYrrY0t92c+cC+2F6hsFNnCQArXCKlg08= +go.opentelemetry.io/otel/sdk/metric v1.28.0/go.mod h1:cWPjykihLAPvXKi4iZc1dpER3Jdq2Z0YLse3moQUCpg= +go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= +go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= @@ -1046,7 +1063,6 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -1176,10 +1192,10 @@ google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 h1:7whR9kGa5LUwFtpLm2ArCEejtnxlGeLbAyjFY8sGNFw= -google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 h1:Zy9XzmMEflZ/MAaA7vNcoebnRAld7FsPW1EeBB7V0m8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= +google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 h1:0+ozOGcrp+Y8Aq8TLNN2Aliibms5LEzsq99ZZmAGYm0= +google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094/go.mod h1:fJ/e3If/Q67Mj99hin0hMhiNyCRmt6BQ2aWIJshUSJw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= @@ -1197,8 +1213,8 @@ google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA= -google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= +google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= +google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -1212,8 +1228,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= -google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 0000000000..02cc7c37c2 --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,35 @@ +package config + +import ( + "context" + + "go.signoz.io/signoz/pkg/instrumentation" + "go.signoz.io/signoz/pkg/web" +) + +// Config defines the entire configuration of signoz. +type Config struct { + Instrumentation instrumentation.Config `mapstructure:"instrumentation"` + Web web.Config `mapstructure:"web"` +} + +func New(ctx context.Context, settings ProviderSettings) (*Config, error) { + provider, err := NewProvider(settings) + if err != nil { + return nil, err + } + + return provider.Get(ctx) +} + +func byName(name string) (any, bool) { + switch name { + case "instrumentation": + return &instrumentation.Config{}, true + case "web": + return &web.Config{}, true + default: + return nil, false + } + +} diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go new file mode 100644 index 0000000000..b3e3007bb4 --- /dev/null +++ b/pkg/config/config_test.go @@ -0,0 +1,61 @@ +package config + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/confmap" + contribsdkconfig "go.opentelemetry.io/contrib/config" + "go.signoz.io/signoz/pkg/confmap/provider/signozenvprovider" + "go.signoz.io/signoz/pkg/instrumentation" + "go.signoz.io/signoz/pkg/web" +) + +func TestNewWithSignozEnvProvider(t *testing.T) { + t.Setenv("SIGNOZ__INSTRUMENTATION__LOGS__ENABLED", "true") + t.Setenv("SIGNOZ__INSTRUMENTATION__LOGS__PROCESSORS__BATCH__EXPORTER__OTLP__ENDPOINT", "0.0.0.0:4317") + t.Setenv("SIGNOZ__INSTRUMENTATION__LOGS__PROCESSORS__BATCH__EXPORT_TIMEOUT", "10") + t.Setenv("SIGNOZ__WEB__PREFIX", "/web") + t.Setenv("SIGNOZ__WEB__DIRECTORY", "/build") + + config, err := New(context.Background(), ProviderSettings{ + ResolverSettings: confmap.ResolverSettings{ + URIs: []string{"signozenv:"}, + ProviderFactories: []confmap.ProviderFactory{ + signozenvprovider.NewFactory(), + }, + }, + }) + require.NoError(t, err) + + i := 10 + expected := &Config{ + Instrumentation: instrumentation.Config{ + Logs: instrumentation.LogsConfig{ + Enabled: true, + LoggerProvider: contribsdkconfig.LoggerProvider{ + Processors: []contribsdkconfig.LogRecordProcessor{ + { + Batch: &contribsdkconfig.BatchLogRecordProcessor{ + ExportTimeout: &i, + Exporter: contribsdkconfig.LogRecordExporter{ + OTLP: &contribsdkconfig.OTLP{ + Endpoint: "0.0.0.0:4317", + }, + }, + }, + }, + }, + }, + }, + }, + Web: web.Config{ + Prefix: "/web", + Directory: "/build", + }, + } + + assert.Equal(t, expected, config) +} diff --git a/pkg/config/doc.go b/pkg/config/doc.go new file mode 100644 index 0000000000..324bd656fe --- /dev/null +++ b/pkg/config/doc.go @@ -0,0 +1,4 @@ +// Package config provides the configuration management for the Signoz application. +// It includes functionality to define, load, and validate the application's configuration +// using various providers and formats. +package config diff --git a/pkg/config/provider.go b/pkg/config/provider.go new file mode 100644 index 0000000000..c0164d1d03 --- /dev/null +++ b/pkg/config/provider.go @@ -0,0 +1,52 @@ +package config + +import ( + "context" + "fmt" + + "go.opentelemetry.io/collector/confmap" +) + +// Provides the configuration for signoz. +type Provider interface { + // Get returns the configuration, or error otherwise. + Get(ctx context.Context) (*Config, error) +} + +type provider struct { + resolver *confmap.Resolver +} + +// ProviderSettings are the settings to configure the behavior of the Provider. +type ProviderSettings struct { + // ResolverSettings are the settings to configure the behavior of the confmap.Resolver. + ResolverSettings confmap.ResolverSettings +} + +// NewProvider returns a new Provider that provides the entire configuration. +// See https://github.com/open-telemetry/opentelemetry-collector/blob/main/otelcol/configprovider.go for +// more details +func NewProvider(settings ProviderSettings) (Provider, error) { + resolver, err := confmap.NewResolver(settings.ResolverSettings) + if err != nil { + return nil, err + } + + return &provider{ + resolver: resolver, + }, nil +} + +func (provider *provider) Get(ctx context.Context) (*Config, error) { + conf, err := provider.resolver.Resolve(ctx) + if err != nil { + return nil, fmt.Errorf("cannot resolve configuration: %w", err) + } + + config, err := unmarshal(conf) + if err != nil { + return nil, fmt.Errorf("cannot unmarshal configuration: %w", err) + } + + return config, nil +} diff --git a/pkg/config/unmarshaler.go b/pkg/config/unmarshaler.go new file mode 100644 index 0000000000..8392f83462 --- /dev/null +++ b/pkg/config/unmarshaler.go @@ -0,0 +1,58 @@ +package config + +import ( + "fmt" + + "go.opentelemetry.io/collector/confmap" + signozconfmap "go.signoz.io/signoz/pkg/confmap" +) + +// unmarshal converts a confmap.Conf into a Config struct. +// It splits the input confmap into a map of key-value pairs, fetches the corresponding +// signozconfmap.Config interface by name, merges it with the default config, validates it, +// and then creates a new confmap from the parsed map to unmarshal into the Config struct. +func unmarshal(conf *confmap.Conf) (*Config, error) { + raw := make(map[string]any) + if err := conf.Unmarshal(&raw); err != nil { + return nil, err + } + + parsed := make(map[string]any) + + for k := range raw { + e, ok := byName(k) + if !ok { + return nil, fmt.Errorf("cannot find config with name %q", k) + } + i, ok := e.(signozconfmap.Config) + if !ok { + return nil, fmt.Errorf("config %q does not implement \"signozconfmap.Config\"", k) + } + + sub, err := conf.Sub(k) + if err != nil { + return nil, fmt.Errorf("cannot read config for %q: %w", k, err) + } + + d := i.NewWithDefaults() + if err := sub.Unmarshal(&d); err != nil { + return nil, fmt.Errorf("cannot merge config for %q: %w", k, err) + } + + err = d.Validate() + if err != nil { + return nil, fmt.Errorf("failed to validate config for for %q: %w", k, err) + } + + parsed[k] = d + } + + parsedConf := confmap.NewFromStringMap(parsed) + config := new(Config) + err := parsedConf.Unmarshal(config) + if err != nil { + return nil, fmt.Errorf("cannot unmarshal config: %w", err) + } + + return config, nil +} diff --git a/pkg/config/unmarshaler_test.go b/pkg/config/unmarshaler_test.go new file mode 100644 index 0000000000..1627bbcf96 --- /dev/null +++ b/pkg/config/unmarshaler_test.go @@ -0,0 +1,34 @@ +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/confmap" + "go.signoz.io/signoz/pkg/instrumentation" +) + +func TestUnmarshal(t *testing.T) { + input := confmap.NewFromStringMap( + map[string]any{ + "instrumentation": map[string]any{ + "logs": map[string]bool{ + "enabled": true, + }, + }, + }, + ) + expected := &Config{ + Instrumentation: instrumentation.Config{ + Logs: instrumentation.LogsConfig{ + Enabled: true, + }, + }, + } + cfg, err := unmarshal(input) + require.NoError(t, err) + + assert.Equal(t, expected, cfg) + +} diff --git a/pkg/confmap/config.go b/pkg/confmap/config.go new file mode 100644 index 0000000000..f3425faf44 --- /dev/null +++ b/pkg/confmap/config.go @@ -0,0 +1,9 @@ +package confmap + +// Config is an interface that defines methods for creating and validating configurations. +type Config interface { + // New creates a new instance of the configuration with default values. + NewWithDefaults() Config + // Validate the configuration and returns an error if invalid. + Validate() error +} diff --git a/pkg/confmap/doc.go b/pkg/confmap/doc.go new file mode 100644 index 0000000000..de21ed5aa0 --- /dev/null +++ b/pkg/confmap/doc.go @@ -0,0 +1,3 @@ +// Package confmap is a wrapper on top of the confmap defined here: +// https://github.com/open-telemetry/opentelemetry-collector/blob/main/otelcol/configprovider.go/ +package confmap diff --git a/pkg/confmap/provider/signozenvprovider/provider.go b/pkg/confmap/provider/signozenvprovider/provider.go new file mode 100644 index 0000000000..aee4c9b499 --- /dev/null +++ b/pkg/confmap/provider/signozenvprovider/provider.go @@ -0,0 +1,94 @@ +package signozenvprovider + +import ( + "context" + "fmt" + "os" + "regexp" + "sort" + "strings" + + "go.opentelemetry.io/collector/confmap" + "go.uber.org/zap" + "gopkg.in/yaml.v3" +) + +const ( + schemeName string = "signozenv" + envPrefix string = "signoz" + separator string = "__" + envPrefixWithOneSeparator string = "signoz_" + envRegexString string = `^[a-zA-Z][a-zA-Z0-9_]*$` +) + +var ( + envRegex = regexp.MustCompile(envRegexString) +) + +type provider struct { + logger *zap.Logger +} + +// NewFactory returns a factory for a confmap.Provider that reads the configuration from the environment. +// All variables starting with `SIGNOZ__` are read from the environment. +// The separator is `__` (2 underscores) in order to incorporate env variables having keys with a single `_` +func NewFactory() confmap.ProviderFactory { + return confmap.NewProviderFactory(newProvider) +} + +func newProvider(settings confmap.ProviderSettings) confmap.Provider { + return &provider{ + logger: settings.Logger, + } +} + +func (provider *provider) Retrieve(_ context.Context, uri string, _ confmap.WatcherFunc) (*confmap.Retrieved, error) { + if !strings.HasPrefix(uri, schemeName+":") { + return nil, fmt.Errorf("%q uri is not supported by %q provider", uri, schemeName) + } + + // Read and Sort environment variables for consistent output + envvars := os.Environ() + sort.Strings(envvars) + + // Create a map m containing key value pairs + m := make(map[string]any) + for _, envvar := range envvars { + parts := strings.SplitN(envvar, "=", 2) + if len(parts) != 2 { + continue + } + key := strings.ToLower(parts[0]) + val := parts[1] + + if strings.HasPrefix(key, envPrefixWithOneSeparator) { + // Remove the envPrefix from the key + key = strings.Replace(key, envPrefix+separator, "", 1) + + // Check whether the resulting key matches with the regex + if !envRegex.MatchString(key) { + provider.logger.Warn("Configuration references invalid environment variable key", zap.String("key", key)) + continue + } + + // Convert key into yaml format + key = strings.ToLower(strings.ReplaceAll(key, separator, confmap.KeyDelimiter)) + m[key] = val + } + } + + out, err := yaml.Marshal(m) + if err != nil { + return nil, err + } + + return confmap.NewRetrievedFromYAML(out) +} + +func (*provider) Scheme() string { + return schemeName +} + +func (*provider) Shutdown(context.Context) error { + return nil +} diff --git a/pkg/confmap/provider/signozenvprovider/provider_test.go b/pkg/confmap/provider/signozenvprovider/provider_test.go new file mode 100644 index 0000000000..5a0ef1ea57 --- /dev/null +++ b/pkg/confmap/provider/signozenvprovider/provider_test.go @@ -0,0 +1,40 @@ +package signozenvprovider + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/confmap" + "go.opentelemetry.io/collector/confmap/confmaptest" +) + +func createProvider() confmap.Provider { + return NewFactory().Create(confmaptest.NewNopProviderSettings()) +} + +func TestValidateProviderScheme(t *testing.T) { + assert.NoError(t, confmaptest.ValidateProviderScheme(createProvider())) +} + +func TestRetrieve(t *testing.T) { + t.Setenv("SIGNOZ__STORAGE__DSN", "localhost:9000") + t.Setenv("SIGNOZ__SIGNOZ_ENABLED", "true") + t.Setenv("SIGNOZ__INSTRUMENTATION__LOGS__ENABLED", "true") + expected := confmap.NewFromStringMap(map[string]any{ + "storage::dsn": "localhost:9000", + "signoz_enabled": "true", + "instrumentation::logs::enabled": "true", + }) + + signoz := createProvider() + retrieved, err := signoz.Retrieve(context.Background(), schemeName+":", nil) + require.NoError(t, err) + + actual, err := retrieved.AsConf() + require.NoError(t, err) + + assert.Equal(t, expected.ToStringMap(), actual.ToStringMap()) + assert.NoError(t, signoz.Shutdown(context.Background())) +} diff --git a/pkg/errors/doc.go b/pkg/errors/doc.go new file mode 100644 index 0000000000..f9cd863c72 --- /dev/null +++ b/pkg/errors/doc.go @@ -0,0 +1,3 @@ +// package error contains error related utilities. Use this package when +// a well-defined error has to be shown. +package errors diff --git a/pkg/errors/errors.go b/pkg/errors/errors.go new file mode 100644 index 0000000000..6ed9728e4c --- /dev/null +++ b/pkg/errors/errors.go @@ -0,0 +1,122 @@ +package errors + +import ( + "fmt" +) + +const ( + codeUnknown string = "unknown" +) + +// base is the fundamental struct that implements the error interface. +// The order of the struct is 'TCMEUA'. +type base struct { + // t denotes the custom type of the error. + t typ + // c denotes the short code for the error message. + c string + // m contains error message passed through errors.New. + m string + // e is the actual error being wrapped. + e error + // u denotes the url for the documentation (if present) for the error. + u string + // a denotes any additional error messages (if present). + a []string +} + +// base implements Error interface. +func (b *base) Error() string { + if b.e != nil { + return b.e.Error() + } + + return fmt.Sprintf("%s(%s): %s", b.t.s, b.c, b.m) +} + +// New returns a base error. It requires type, code and message as input. +func New(t typ, code string, message string) *base { + return &base{ + t: t, + c: code, + m: message, + e: nil, + u: "", + a: []string{}, + } +} + +// Newf returns a new base by formatting the error message with the supplied format specifier. +func Newf(t typ, code string, format string, args ...interface{}) *base { + return &base{ + t: t, + c: code, + m: fmt.Sprintf(format, args...), + e: nil, + } +} + +// Wrapf returns a new error by formatting the error message with the supplied format specifier +// and wrapping another error with base. +func Wrapf(cause error, t typ, code string, format string, args ...interface{}) *base { + return &base{ + t: t, + c: code, + m: fmt.Sprintf(format, args...), + e: cause, + } +} + +// WithUrl adds a url to the base error and returns a new base error. +func (b *base) WithUrl(u string) *base { + return &base{ + t: b.t, + c: b.c, + m: b.m, + e: b.e, + u: u, + a: b.a, + } +} + +// WithUrl adds additional messages to the base error and returns a new base error. +func (b *base) WithAdditional(a ...string) *base { + return &base{ + t: b.t, + c: b.c, + m: b.m, + e: b.e, + u: b.u, + a: a, + } +} + +// Unwrapb is a combination of built-in errors.As and type casting. +// It finds the first error in cause that matches base, +// and if one is found, returns the individual fields of base. +// Otherwise, it returns TypeInternal, the original error string +// and the error itself. +// +//lint:ignore ST1008 we want to return arguments in the 'TCMEUA' order of the struct +func Unwrapb(cause error) (typ, string, string, error, string, []string) { + base, ok := cause.(*base) + if ok { + return base.t, base.c, base.m, base.e, base.u, base.a + } + + return TypeInternal, codeUnknown, cause.Error(), cause, "", []string{} +} + +// Ast checks if the provided error matches the specified custom error type. +func Ast(cause error, typ typ) bool { + t, _, _, _, _, _ := Unwrapb(cause) + + return t == typ +} + +// Ast checks if the provided error matches the specified custom error code. +func Asc(cause error, code string) bool { + _, c, _, _, _, _ := Unwrapb(cause) + + return c == code +} diff --git a/pkg/errors/errors_test.go b/pkg/errors/errors_test.go new file mode 100644 index 0000000000..4b04e876af --- /dev/null +++ b/pkg/errors/errors_test.go @@ -0,0 +1,53 @@ +package errors + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNew(t *testing.T) { + typ := typ{"test-error"} + err := New(typ, "code", "test error info") + assert.NotNil(t, err) +} + +func TestNewf(t *testing.T) { + typ := typ{"test-error"} + err := Newf(typ, "test-code", "test error info with %s", "string") + assert.NotNil(t, err) + assert.Equal(t, "test-error(test-code): test error info with string", err.Error()) +} + +func TestWrapf(t *testing.T) { + typ := typ{"test-error"} + err := Wrapf(errors.New("original error"), typ, "test-code", "info for err %d", 2) + assert.NotNil(t, err) +} + +func TestError(t *testing.T) { + typ := typ{"test-error"} + err1 := New(typ, "test-code", "info for err1") + assert.Equal(t, "test-error(test-code): info for err1", err1.Error()) + + err2 := Wrapf(err1, typ, "test-code", "info for err2") + assert.Equal(t, "test-error(test-code): info for err1", err2.Error()) +} + +func TestUnwrapb(t *testing.T) { + typ := typ{"test-error"} + oerr := errors.New("original error") + berr := Wrapf(oerr, typ, "test-code", "this is a base err").WithUrl("https://docs").WithAdditional("additional err") + + atyp, acode, amessage, aerr, au, aa := Unwrapb(berr) + assert.Equal(t, typ, atyp) + assert.Equal(t, "test-code", acode) + assert.Equal(t, "this is a base err", amessage) + assert.Equal(t, oerr, aerr) + assert.Equal(t, "https://docs", au) + assert.Equal(t, []string{"additional err"}, aa) + + atyp, _, _, _, _, _ = Unwrapb(oerr) + assert.Equal(t, TypeInternal, atyp) +} diff --git a/pkg/errors/type.go b/pkg/errors/type.go new file mode 100644 index 0000000000..6800a8bc71 --- /dev/null +++ b/pkg/errors/type.go @@ -0,0 +1,14 @@ +package errors + +var ( + TypeInvalidInput typ = typ{"invalid-input"} + TypeInternal = typ{"internal"} + TypeUnsupported = typ{"unsupported"} + TypeNotFound = typ{"not-found"} + TypeMethodNotAllowed = typ{"method-not-allowed"} + TypeAlreadyExists = typ{"already-exists"} + TypeUnauthenticated = typ{"unauthenticated"} +) + +// Defines custom error types +type typ struct{ s string } diff --git a/pkg/errors/type_test.go b/pkg/errors/type_test.go new file mode 100644 index 0000000000..7b7d3a26b3 --- /dev/null +++ b/pkg/errors/type_test.go @@ -0,0 +1,18 @@ +package errors + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestString(t *testing.T) { + typ := typ{"test-error"} + assert.Equal(t, typ.s, "test-error") +} + +func TestEquals(t *testing.T) { + typ1 := typ{"test-error"} + typ2 := typ{"test-error"} + assert.True(t, typ1 == typ2) +} diff --git a/pkg/http/doc.go b/pkg/http/doc.go new file mode 100644 index 0000000000..5e62c61b15 --- /dev/null +++ b/pkg/http/doc.go @@ -0,0 +1,3 @@ +// package http contains all http related functions such +// as servers, middlewares, routers and renders. +package http diff --git a/pkg/http/middleware/cache.go b/pkg/http/middleware/cache.go new file mode 100644 index 0000000000..ff66288354 --- /dev/null +++ b/pkg/http/middleware/cache.go @@ -0,0 +1,28 @@ +package middleware + +import ( + "net/http" + "strconv" + "time" +) + +type Cache struct { + maxAge time.Duration +} + +func NewCache(maxAge time.Duration) *Cache { + if maxAge == 0 { + maxAge = 7 * 24 * time.Hour + } + + return &Cache{ + maxAge: maxAge, + } +} + +func (middleware *Cache) Wrap(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.Header().Set("Cache-Control", "max-age="+strconv.Itoa(int(middleware.maxAge.Seconds()))) + next.ServeHTTP(rw, req) + }) +} diff --git a/pkg/http/middleware/cache_test.go b/pkg/http/middleware/cache_test.go new file mode 100644 index 0000000000..bef0e3b36b --- /dev/null +++ b/pkg/http/middleware/cache_test.go @@ -0,0 +1,56 @@ +package middleware + +import ( + "net" + "net/http" + "strconv" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestCache(t *testing.T) { + t.Parallel() + + age := 20 * 24 * time.Hour + m := NewCache(age) + + listener, err := net.Listen("tcp", "localhost:0") + require.NoError(t, err) + + server := &http.Server{ + Handler: m.Wrap(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(204) + })), + } + + go func() { + require.NoError(t, server.Serve(listener)) + }() + + testCases := []struct { + name string + age time.Duration + }{ + { + name: "Success", + age: age, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + req, err := http.NewRequest("GET", "http://"+listener.Addr().String(), nil) + require.NoError(t, err) + + res, err := http.DefaultClient.Do(req) + require.NoError(t, err) + + actual := res.Header.Get("Cache-control") + require.NoError(t, err) + + require.Equal(t, "max-age="+strconv.Itoa(int(age.Seconds())), string(actual)) + }) + } +} diff --git a/pkg/http/middleware/doc.go b/pkg/http/middleware/doc.go new file mode 100644 index 0000000000..911746777c --- /dev/null +++ b/pkg/http/middleware/doc.go @@ -0,0 +1,2 @@ +// package middleware contains an implementation of all middlewares. +package middleware diff --git a/pkg/http/middleware/logging.go b/pkg/http/middleware/logging.go new file mode 100644 index 0000000000..ef755f6648 --- /dev/null +++ b/pkg/http/middleware/logging.go @@ -0,0 +1,72 @@ +package middleware + +import ( + "bytes" + "net" + "net/http" + "time" + + "github.com/gorilla/mux" + semconv "go.opentelemetry.io/otel/semconv/v1.26.0" + "go.uber.org/zap" +) + +const ( + logMessage string = "::RECEIVED-REQUEST::" +) + +type Logging struct { + logger *zap.Logger +} + +func NewLogging(logger *zap.Logger) *Logging { + if logger == nil { + panic("cannot build logging, logger is empty") + } + + return &Logging{ + logger: logger.Named(pkgname), + } +} + +func (middleware *Logging) Wrap(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + ctx := req.Context() + start := time.Now() + host, port, _ := net.SplitHostPort(req.Host) + path, err := mux.CurrentRoute(req).GetPathTemplate() + if err != nil { + path = req.URL.Path + } + + fields := []zap.Field{ + zap.Any("context", ctx), + zap.String(string(semconv.ClientAddressKey), req.RemoteAddr), + zap.String(string(semconv.UserAgentOriginalKey), req.UserAgent()), + zap.String(string(semconv.ServerAddressKey), host), + zap.String(string(semconv.ServerPortKey), port), + zap.Int64(string(semconv.HTTPRequestSizeKey), req.ContentLength), + zap.String(string(semconv.HTTPRouteKey), path), + } + + buf := new(bytes.Buffer) + writer := newBadResponseLoggingWriter(rw, buf) + next.ServeHTTP(writer, req) + + statusCode, err := writer.StatusCode(), writer.WriteError() + fields = append(fields, + zap.Int(string(semconv.HTTPResponseStatusCodeKey), statusCode), + zap.Duration(string(semconv.HTTPServerRequestDurationName), time.Since(start)), + ) + if err != nil { + fields = append(fields, zap.Error(err)) + middleware.logger.Error(logMessage, fields...) + } else { + if buf.Len() != 0 { + fields = append(fields, zap.String("response.body", buf.String())) + } + + middleware.logger.Info(logMessage, fields...) + } + }) +} diff --git a/pkg/http/middleware/middleware.go b/pkg/http/middleware/middleware.go new file mode 100644 index 0000000000..6313089aa4 --- /dev/null +++ b/pkg/http/middleware/middleware.go @@ -0,0 +1,20 @@ +package middleware + +import "net/http" + +const ( + pkgname string = "go.signoz.io/pkg/http/middleware" +) + +// Wrapper is an interface implemented by all middlewares +type Wrapper interface { + Wrap(http.Handler) http.Handler +} + +// WrapperFunc is to Wrapper as http.HandlerFunc is to http.Handler +type WrapperFunc func(http.Handler) http.Handler + +// WrapperFunc implements Wrapper +func (m WrapperFunc) Wrap(next http.Handler) http.Handler { + return m(next) +} diff --git a/pkg/http/middleware/response.go b/pkg/http/middleware/response.go new file mode 100644 index 0000000000..deb0f3dd81 --- /dev/null +++ b/pkg/http/middleware/response.go @@ -0,0 +1,122 @@ +package middleware + +import ( + "bufio" + "fmt" + "io" + "net" + "net/http" +) + +const ( + maxResponseBodyInLogs = 4096 // At most 4k bytes from response bodies in our logs. +) + +type badResponseLoggingWriter interface { + http.ResponseWriter + // Get the status code. + StatusCode() int + // Get the error while writing. + WriteError() error +} + +func newBadResponseLoggingWriter(rw http.ResponseWriter, buffer io.Writer) badResponseLoggingWriter { + b := nonFlushingBadResponseLoggingWriter{ + rw: rw, + buffer: buffer, + logBody: false, + bodyBytesLeft: maxResponseBodyInLogs, + statusCode: http.StatusOK, + } + + if f, ok := rw.(http.Flusher); ok { + return &flushingBadResponseLoggingWriter{b, f} + } + + return &b +} + +type nonFlushingBadResponseLoggingWriter struct { + rw http.ResponseWriter + buffer io.Writer + logBody bool + bodyBytesLeft int + statusCode int + writeError error // The error returned when downstream Write() fails. +} + +// Extends nonFlushingBadResponseLoggingWriter that implements http.Flusher +type flushingBadResponseLoggingWriter struct { + nonFlushingBadResponseLoggingWriter + f http.Flusher +} + +// Unwrap method is used by http.ResponseController to get access to original http.ResponseWriter. +func (writer *nonFlushingBadResponseLoggingWriter) Unwrap() http.ResponseWriter { + return writer.rw +} + +// Header returns the header map that will be sent by WriteHeader. +// Implements ResponseWriter. +func (writer *nonFlushingBadResponseLoggingWriter) Header() http.Header { + return writer.rw.Header() +} + +// WriteHeader writes the HTTP response header. +func (writer *nonFlushingBadResponseLoggingWriter) WriteHeader(statusCode int) { + writer.statusCode = statusCode + if statusCode >= 500 || statusCode == 400 { + writer.logBody = true + } + writer.rw.WriteHeader(statusCode) +} + +// Writes HTTP response data. +func (writer *nonFlushingBadResponseLoggingWriter) Write(data []byte) (int, error) { + if writer.statusCode == 0 { + // WriteHeader has (probably) not been called, so we need to call it with StatusOK to fulfill the interface contract. + // https://godoc.org/net/http#ResponseWriter + writer.WriteHeader(http.StatusOK) + } + n, err := writer.rw.Write(data) + if writer.logBody { + writer.captureResponseBody(data) + } + if err != nil { + writer.writeError = err + } + return n, err +} + +// Hijack hijacks the first response writer that is a Hijacker. +func (writer *nonFlushingBadResponseLoggingWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { + hj, ok := writer.rw.(http.Hijacker) + if ok { + return hj.Hijack() + } + return nil, nil, fmt.Errorf("cannot cast underlying response writer to Hijacker") +} + +func (writer *nonFlushingBadResponseLoggingWriter) StatusCode() int { + return writer.statusCode +} + +func (writer *nonFlushingBadResponseLoggingWriter) WriteError() error { + return writer.writeError +} + +func (writer *flushingBadResponseLoggingWriter) Flush() { + writer.f.Flush() +} + +func (writer *nonFlushingBadResponseLoggingWriter) captureResponseBody(data []byte) { + if len(data) > writer.bodyBytesLeft { + _, _ = writer.buffer.Write(data[:writer.bodyBytesLeft]) + _, _ = io.WriteString(writer.buffer, "...") + writer.bodyBytesLeft = 0 + writer.logBody = false + } else { + _, _ = writer.buffer.Write(data) + writer.bodyBytesLeft -= len(data) + } +} diff --git a/pkg/http/middleware/timeout.go b/pkg/http/middleware/timeout.go new file mode 100644 index 0000000000..50e9d82f22 --- /dev/null +++ b/pkg/http/middleware/timeout.go @@ -0,0 +1,78 @@ +package middleware + +import ( + "context" + "net/http" + "strings" + "time" + + "go.uber.org/zap" +) + +const ( + headerName string = "timeout" +) + +type Timeout struct { + logger *zap.Logger + excluded map[string]struct{} + // The default timeout + defaultTimeout time.Duration + // The max allowed timeout + maxTimeout time.Duration +} + +func NewTimeout(logger *zap.Logger, excluded map[string]struct{}, defaultTimeout time.Duration, maxTimeout time.Duration) *Timeout { + if logger == nil { + panic("cannot build timeout, logger is empty") + } + + if excluded == nil { + excluded = make(map[string]struct{}) + } + + if defaultTimeout.Seconds() == 0 { + defaultTimeout = 60 * time.Second + } + + if maxTimeout == 0 { + maxTimeout = 600 * time.Second + } + + return &Timeout{ + logger: logger.Named(pkgname), + excluded: excluded, + defaultTimeout: defaultTimeout, + maxTimeout: maxTimeout, + } +} + +func (middleware *Timeout) Wrap(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + if _, ok := middleware.excluded[req.URL.Path]; !ok { + actual := middleware.defaultTimeout + incoming := req.Header.Get(headerName) + if incoming != "" { + parsed, err := time.ParseDuration(strings.TrimSpace(incoming) + "s") + if err != nil { + middleware.logger.Warn("cannot parse timeout in header, using default timeout", zap.String("timeout", incoming), zap.Error(err), zap.Any("context", req.Context())) + } else { + if parsed > middleware.maxTimeout { + actual = middleware.maxTimeout + } else { + actual = parsed + } + } + } + + ctx, cancel := context.WithTimeout(req.Context(), actual) + defer cancel() + + req = req.WithContext(ctx) + next.ServeHTTP(rw, req) + return + } + + next.ServeHTTP(rw, req) + }) +} diff --git a/pkg/http/middleware/timeout_test.go b/pkg/http/middleware/timeout_test.go new file mode 100644 index 0000000000..2575bfe7d9 --- /dev/null +++ b/pkg/http/middleware/timeout_test.go @@ -0,0 +1,80 @@ +package middleware + +import ( + "net" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/require" + "go.uber.org/zap" +) + +func TestTimeout(t *testing.T) { + t.Parallel() + + writeTimeout := 6 * time.Second + defaultTimeout := 2 * time.Second + maxTimeout := 4 * time.Second + m := NewTimeout(zap.NewNop(), map[string]struct{}{"/excluded": {}}, defaultTimeout, maxTimeout) + + listener, err := net.Listen("tcp", "localhost:0") + require.NoError(t, err) + + server := &http.Server{ + WriteTimeout: writeTimeout, + Handler: m.Wrap(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, ok := r.Context().Deadline() + if ok { + <-r.Context().Done() + require.Error(t, r.Context().Err()) + } + w.WriteHeader(204) + })), + } + + go func() { + require.NoError(t, server.Serve(listener)) + }() + + testCases := []struct { + name string + wait time.Duration + header string + path string + }{ + { + name: "WaitTillNoTimeoutForExcludedPath", + wait: 1 * time.Nanosecond, + header: "4", + path: "excluded", + }, + { + name: "WaitTillHeaderTimeout", + wait: 3 * time.Second, + header: "3", + path: "header-timeout", + }, + { + name: "WaitTillMaxTimeout", + wait: 4 * time.Second, + header: "5", + path: "max-timeout", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + start := time.Now() + req, err := http.NewRequest("GET", "http://"+listener.Addr().String()+"/"+tc.path, nil) + require.NoError(t, err) + req.Header.Add(headerName, tc.header) + + _, err = http.DefaultClient.Do(req) + require.NoError(t, err) + + // confirm that we waited at least till the "wait" time + require.GreaterOrEqual(t, time.Since(start), tc.wait) + }) + } +} diff --git a/pkg/http/render/render.go b/pkg/http/render/render.go new file mode 100644 index 0000000000..405bb76ed1 --- /dev/null +++ b/pkg/http/render/render.go @@ -0,0 +1,83 @@ +package render + +import ( + "net/http" + + jsoniter "github.com/json-iterator/go" + "go.signoz.io/signoz/pkg/errors" +) + +var json = jsoniter.ConfigCompatibleWithStandardLibrary + +type response struct { + Status string `json:"status"` + Data interface{} `json:"data,omitempty"` + Error *responseerror `json:"error,omitempty"` +} + +type responseerror struct { + Code string `json:"code"` + Message string `json:"message"` + Url string `json:"url,omitempty"` + Errors []responseerroradditional `json:"errors,omitempty"` +} + +type responseerroradditional struct { + Message string `json:"message"` +} + +func Success(rw http.ResponseWriter, httpCode int, data interface{}) { + body, err := json.Marshal(&response{Status: StatusSuccess.s, Data: data}) + if err != nil { + Error(rw, err) + return + } + + if httpCode == 0 { + httpCode = http.StatusOK + } + + rw.WriteHeader(httpCode) + _, _ = rw.Write(body) +} + +func Error(rw http.ResponseWriter, cause error) { + // See if this is an instance of the base error or not + t, c, m, _, u, a := errors.Unwrapb(cause) + + // Derive the http code from the error type + httpCode := http.StatusInternalServerError + switch t { + case errors.TypeInvalidInput: + httpCode = http.StatusBadRequest + case errors.TypeNotFound: + httpCode = http.StatusNotFound + case errors.TypeAlreadyExists: + httpCode = http.StatusConflict + case errors.TypeUnauthenticated: + httpCode = http.StatusUnauthorized + } + + rea := make([]responseerroradditional, len(a)) + for k, v := range a { + rea[k] = responseerroradditional{v} + } + + body, err := json.Marshal(&response{ + Status: StatusError.s, + Error: &responseerror{ + Code: c, + Url: u, + Message: m, + Errors: rea, + }, + }) + if err != nil { + // this should never be the case + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + rw.WriteHeader(httpCode) + _, _ = rw.Write(body) +} diff --git a/pkg/http/render/render_test.go b/pkg/http/render/render_test.go new file mode 100644 index 0000000000..79943157f3 --- /dev/null +++ b/pkg/http/render/render_test.go @@ -0,0 +1,116 @@ +package render + +import ( + "context" + "fmt" + "io" + "net" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.signoz.io/signoz/pkg/errors" +) + +func TestSuccess(t *testing.T) { + listener, err := net.Listen("tcp", "localhost:0") + require.NoError(t, err) + + data := map[string]any{ + "int64": int64(9), + "string": "string", + "bool": true, + } + + marshalled, err := json.Marshal(data) + require.NoError(t, err) + + expected := []byte(fmt.Sprintf(`{"status":"success","data":%s}`, string(marshalled))) + + server := &http.Server{ + Handler: http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + Success(rw, http.StatusAccepted, data) + }), + } + + go func() { + _ = server.Serve(listener) + }() + + defer func() { + _ = server.Shutdown(context.Background()) + }() + + req, err := http.NewRequest("GET", "http://"+listener.Addr().String(), nil) + require.NoError(t, err) + + res, err := http.DefaultClient.Do(req) + require.NoError(t, err) + + actual, err := io.ReadAll(res.Body) + require.NoError(t, err) + + assert.Equal(t, http.StatusAccepted, res.StatusCode) + assert.Equal(t, expected, actual) +} + +func TestError(t *testing.T) { + listener, err := net.Listen("tcp", "localhost:0") + require.NoError(t, err) + + testCases := map[string]struct { + name string + statusCode int + err error + expected []byte + }{ + "/already_exists": { + name: "AlreadyExists", + statusCode: http.StatusConflict, + err: errors.New(errors.TypeAlreadyExists, "already_exists", "already exists").WithUrl("https://already_exists"), + expected: []byte(`{"status":"error","error":{"code":"already_exists","message":"already exists","url":"https://already_exists"}}`), + }, + "/unauthenticated": { + name: "Unauthenticated", + statusCode: http.StatusUnauthorized, + err: errors.New(errors.TypeUnauthenticated, "not_allowed", "not allowed").WithUrl("https://unauthenticated").WithAdditional("a1", "a2"), + expected: []byte(`{"status":"error","error":{"code":"not_allowed","message":"not allowed","url":"https://unauthenticated","errors":[{"message":"a1"},{"message":"a2"}]}}`), + }, + } + + server := &http.Server{ + Handler: http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + tc, ok := testCases[req.URL.Path] + if ok { + Error(rw, tc.err) + return + } + }), + } + + go func() { + _ = server.Serve(listener) + }() + + defer func() { + _ = server.Shutdown(context.Background()) + }() + + for path, tc := range testCases { + t.Run("", func(t *testing.T) { + req, err := http.NewRequest("GET", "http://"+listener.Addr().String()+path, nil) + require.NoError(t, err) + + res, err := http.DefaultClient.Do(req) + require.NoError(t, err) + + actual, err := io.ReadAll(res.Body) + require.NoError(t, err) + + assert.Equal(t, tc.statusCode, res.StatusCode) + assert.Equal(t, tc.expected, actual) + }) + } + +} diff --git a/pkg/http/render/status.go b/pkg/http/render/status.go new file mode 100644 index 0000000000..dc4f8720ff --- /dev/null +++ b/pkg/http/render/status.go @@ -0,0 +1,9 @@ +package render + +var ( + StatusSuccess status = status{"success"} + StatusError = status{"error"} +) + +// Defines custom error types +type status struct{ s string } diff --git a/pkg/http/server/config.go b/pkg/http/server/config.go new file mode 100644 index 0000000000..fb8eb5be11 --- /dev/null +++ b/pkg/http/server/config.go @@ -0,0 +1,27 @@ +package server + +import ( + "go.signoz.io/signoz/pkg/confmap" +) + +// Config satisfies the confmap.Config interface +var _ confmap.Config = (*Config)(nil) + +// Config holds the configuration for http. +type Config struct { + //Address specifies the TCP address for the server to listen on, in the form "host:port". + // If empty, ":http" (port 80) is used. The service names are defined in RFC 6335 and assigned by IANA. + // See net.Dial for details of the address format. + Address string `mapstructure:"address"` +} + +func (c *Config) NewWithDefaults() confmap.Config { + return &Config{ + Address: "0.0.0.0:8080", + } + +} + +func (c *Config) Validate() error { + return nil +} diff --git a/pkg/http/server/doc.go b/pkg/http/server/doc.go new file mode 100644 index 0000000000..9bf12b5ae5 --- /dev/null +++ b/pkg/http/server/doc.go @@ -0,0 +1,2 @@ +// package server contains an implementation of the http server. +package server diff --git a/pkg/http/server/server.go b/pkg/http/server/server.go new file mode 100644 index 0000000000..fbeca1c3a9 --- /dev/null +++ b/pkg/http/server/server.go @@ -0,0 +1,79 @@ +package server + +import ( + "context" + "fmt" + "net/http" + "time" + + "go.signoz.io/signoz/pkg/registry" + "go.uber.org/zap" +) + +var _ registry.NamedService = (*Server)(nil) + +type Server struct { + srv *http.Server + logger *zap.Logger + handler http.Handler + cfg Config + name string +} + +func New(logger *zap.Logger, name string, cfg Config, handler http.Handler) (*Server, error) { + if handler == nil { + return nil, fmt.Errorf("cannot build http server, handler is required") + } + + if logger == nil { + return nil, fmt.Errorf("cannot build http server, logger is required") + } + + if name == "" { + return nil, fmt.Errorf("cannot build http server, name is required") + } + + srv := &http.Server{ + Addr: cfg.Address, + Handler: handler, + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + MaxHeaderBytes: 1 << 20, + } + + return &Server{ + srv: srv, + logger: logger.Named("go.signoz.io/pkg/http/server"), + handler: handler, + cfg: cfg, + name: name, + }, nil +} + +func (server *Server) Name() string { + return server.name +} + +func (server *Server) Start(ctx context.Context) error { + server.logger.Info("starting http server", zap.String("address", server.srv.Addr)) + if err := server.srv.ListenAndServe(); err != nil { + if err != http.ErrServerClosed { + server.logger.Error("failed to start server", zap.Error(err), zap.Any("context", ctx)) + return err + } + } + return nil +} + +func (server *Server) Stop(ctx context.Context) error { + ctx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + + if err := server.srv.Shutdown(ctx); err != nil { + server.logger.Error("failed to stop server", zap.Error(err), zap.Any("context", ctx)) + return err + } + + server.logger.Info("server stopped gracefully", zap.Any("context", ctx)) + return nil +} diff --git a/pkg/instrumentation/config.go b/pkg/instrumentation/config.go new file mode 100644 index 0000000000..20eb9b97bb --- /dev/null +++ b/pkg/instrumentation/config.go @@ -0,0 +1,62 @@ +package instrumentation + +import ( + contribsdkconfig "go.opentelemetry.io/contrib/config" + "go.signoz.io/signoz/pkg/confmap" + "go.uber.org/zap/zapcore" +) + +// Config satisfies the confmap.Config interface +var _ confmap.Config = (*Config)(nil) + +// Config holds the configuration for all instrumentation components. +type Config struct { + Logs LogsConfig `mapstructure:"logs"` + Traces TracesConfig `mapstructure:"traces"` + Metrics MetricsConfig `mapstructure:"metrics"` + Resource Resource `mapstructure:"resource"` +} + +// Resource defines the configuration for OpenTelemetry resource attributes. +type Resource struct { + Attributes contribsdkconfig.Attributes `mapstructure:"attributes"` +} + +// LogsConfig holds the configuration for the logging component. +type LogsConfig struct { + Enabled bool `mapstructure:"enabled"` + Level zapcore.Level `mapstructure:"level"` + contribsdkconfig.LoggerProvider `mapstructure:",squash"` +} + +// TracesConfig holds the configuration for the tracing component. +type TracesConfig struct { + Enabled bool `mapstructure:"enabled"` + contribsdkconfig.TracerProvider `mapstructure:",squash"` +} + +// MetricsConfig holds the configuration for the metrics component. +type MetricsConfig struct { + Enabled bool `mapstructure:"enabled"` + contribsdkconfig.MeterProvider `mapstructure:",squash"` +} + +func (c *Config) NewWithDefaults() confmap.Config { + return &Config{ + Logs: LogsConfig{ + Enabled: false, + Level: zapcore.InfoLevel, + }, + Traces: TracesConfig{ + Enabled: false, + }, + Metrics: MetricsConfig{ + Enabled: false, + }, + } + +} + +func (c *Config) Validate() error { + return nil +} diff --git a/pkg/instrumentation/doc.go b/pkg/instrumentation/doc.go new file mode 100644 index 0000000000..ebe97eb30a --- /dev/null +++ b/pkg/instrumentation/doc.go @@ -0,0 +1,5 @@ +// Package instrumentation provides utilities for initializing and managing +// OpenTelemetry resources, logging, tracing, and metering within the application. It +// leverages the OpenTelemetry SDK to facilitate the collection and +// export of telemetry data, to an OTLP (OpenTelemetry Protocol) endpoint. +package instrumentation diff --git a/pkg/instrumentation/instrumentation.go b/pkg/instrumentation/instrumentation.go new file mode 100644 index 0000000000..3f12fc9d63 --- /dev/null +++ b/pkg/instrumentation/instrumentation.go @@ -0,0 +1,95 @@ +package instrumentation + +import ( + "context" + "fmt" + + contribsdkconfig "go.opentelemetry.io/contrib/config" + sdklog "go.opentelemetry.io/otel/log" + sdkmetric "go.opentelemetry.io/otel/metric" + sdkresource "go.opentelemetry.io/otel/sdk/resource" + semconv "go.opentelemetry.io/otel/semconv/v1.26.0" + sdktrace "go.opentelemetry.io/otel/trace" + "go.signoz.io/signoz/pkg/version" + "go.uber.org/zap" +) + +// Instrumentation holds the core components for application instrumentation. +type Instrumentation struct { + LoggerProvider sdklog.LoggerProvider + Logger *zap.Logger + MeterProvider sdkmetric.MeterProvider + TracerProvider sdktrace.TracerProvider +} + +// New creates a new Instrumentation instance with configured providers. +// It sets up logging, tracing, and metrics based on the provided configuration. +func New(ctx context.Context, build version.Build, cfg Config) (*Instrumentation, error) { + // Set default resource attributes if not provided + if cfg.Resource.Attributes == nil { + cfg.Resource.Attributes = map[string]any{ + string(semconv.ServiceNameKey): build.Name, + string(semconv.ServiceVersionKey): build.Version, + } + } + + // Create a new resource with default detectors. + // The upstream contrib repository is not taking detectors into account. + // We are, therefore, using some sensible defaults here. + resource, err := sdkresource.New( + ctx, + sdkresource.WithContainer(), + sdkresource.WithFromEnv(), + sdkresource.WithHost(), + ) + if err != nil { + return nil, err + } + + // Prepare the resource configuration by merging + // resource and attributes. + sch := semconv.SchemaURL + configResource := contribsdkconfig.Resource{ + Attributes: attributes(cfg.Resource.Attributes, resource), + Detectors: nil, + SchemaUrl: &sch, + } + + loggerProvider, err := newLoggerProvider(ctx, cfg, configResource) + if err != nil { + return nil, fmt.Errorf("cannot create logger provider: %w", err) + } + + tracerProvider, err := newTracerProvider(ctx, cfg, configResource) + if err != nil { + return nil, fmt.Errorf("cannot create tracer provider: %w", err) + } + + meterProvider, err := newMeterProvider(ctx, cfg, configResource) + if err != nil { + return nil, fmt.Errorf("cannot create meter provider: %w", err) + } + + return &Instrumentation{ + LoggerProvider: loggerProvider, + TracerProvider: tracerProvider, + MeterProvider: meterProvider, + Logger: newLogger(cfg, loggerProvider), + }, nil +} + +// attributes merges the input attributes with the resource attributes. +func attributes(input map[string]any, resource *sdkresource.Resource) map[string]any { + output := make(map[string]any) + + for k, v := range input { + output[k] = v + } + + kvs := resource.Attributes() + for _, kv := range kvs { + output[string(kv.Key)] = kv.Value + } + + return output +} diff --git a/pkg/instrumentation/logger.go b/pkg/instrumentation/logger.go new file mode 100644 index 0000000000..255323bc1a --- /dev/null +++ b/pkg/instrumentation/logger.go @@ -0,0 +1,45 @@ +package instrumentation + +import ( + "context" + "os" + + "go.opentelemetry.io/contrib/bridges/otelzap" + contribsdkconfig "go.opentelemetry.io/contrib/config" + sdklog "go.opentelemetry.io/otel/log" + nooplog "go.opentelemetry.io/otel/log/noop" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// newLoggerProvider creates a new logger provider based on the configuration. +// If logging is disabled, it returns a no-op logger provider. +func newLoggerProvider(ctx context.Context, cfg Config, cfgResource contribsdkconfig.Resource) (sdklog.LoggerProvider, error) { + if !cfg.Logs.Enabled { + return nooplog.NewLoggerProvider(), nil + } + + sdk, err := contribsdkconfig.NewSDK( + contribsdkconfig.WithContext(ctx), + contribsdkconfig.WithOpenTelemetryConfiguration(contribsdkconfig.OpenTelemetryConfiguration{ + LoggerProvider: &cfg.Logs.LoggerProvider, + Resource: &cfgResource, + }), + ) + if err != nil { + return nil, err + } + + return sdk.LoggerProvider(), nil +} + +// newLogger creates a new Zap logger with the configured level and output. +// It combines a JSON encoder for stdout and an OpenTelemetry bridge. +func newLogger(cfg Config, provider sdklog.LoggerProvider) *zap.Logger { + core := zapcore.NewTee( + zapcore.NewCore(zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), zapcore.AddSync(os.Stdout), cfg.Logs.Level), + otelzap.NewCore("go.signoz.io/pkg/instrumentation", otelzap.WithLoggerProvider(provider)), + ) + + return zap.New(core, zap.AddCaller(), zap.AddStacktrace(zap.ErrorLevel)) +} diff --git a/pkg/instrumentation/meter.go b/pkg/instrumentation/meter.go new file mode 100644 index 0000000000..f1433ee29f --- /dev/null +++ b/pkg/instrumentation/meter.go @@ -0,0 +1,30 @@ +package instrumentation + +import ( + "context" + + contribsdkconfig "go.opentelemetry.io/contrib/config" + sdkmetric "go.opentelemetry.io/otel/metric" + noopmetric "go.opentelemetry.io/otel/metric/noop" +) + +// newMeterProvider creates a new meter provider based on the configuration. +// If metrics are disabled, it returns a no-op meter provider. +func newMeterProvider(ctx context.Context, cfg Config, cfgResource contribsdkconfig.Resource) (sdkmetric.MeterProvider, error) { + if !cfg.Metrics.Enabled { + return noopmetric.NewMeterProvider(), nil + } + + sdk, err := contribsdkconfig.NewSDK( + contribsdkconfig.WithContext(ctx), + contribsdkconfig.WithOpenTelemetryConfiguration(contribsdkconfig.OpenTelemetryConfiguration{ + MeterProvider: &cfg.Metrics.MeterProvider, + Resource: &cfgResource, + }), + ) + if err != nil { + return nil, err + } + + return sdk.MeterProvider(), nil +} diff --git a/pkg/instrumentation/tracer.go b/pkg/instrumentation/tracer.go new file mode 100644 index 0000000000..a2231217dc --- /dev/null +++ b/pkg/instrumentation/tracer.go @@ -0,0 +1,30 @@ +package instrumentation + +import ( + "context" + + contribsdkconfig "go.opentelemetry.io/contrib/config" + sdktrace "go.opentelemetry.io/otel/trace" + nooptrace "go.opentelemetry.io/otel/trace/noop" +) + +// newTracerProvider creates a new tracer provider based on the configuration. +// If tracing is disabled, it returns a no-op tracer provider. +func newTracerProvider(ctx context.Context, cfg Config, cfgResource contribsdkconfig.Resource) (sdktrace.TracerProvider, error) { + if !cfg.Traces.Enabled { + return nooptrace.NewTracerProvider(), nil + } + + sdk, err := contribsdkconfig.NewSDK( + contribsdkconfig.WithContext(ctx), + contribsdkconfig.WithOpenTelemetryConfiguration(contribsdkconfig.OpenTelemetryConfiguration{ + TracerProvider: &cfg.Traces.TracerProvider, + Resource: &cfgResource, + }), + ) + if err != nil { + return nil, err + } + + return sdk.TracerProvider(), nil +} diff --git a/pkg/query-service/app/clickhouseReader/reader.go b/pkg/query-service/app/clickhouseReader/reader.go index b34780c37d..5d8f7ece82 100644 --- a/pkg/query-service/app/clickhouseReader/reader.go +++ b/pkg/query-service/app/clickhouseReader/reader.go @@ -184,6 +184,7 @@ func NewReaderFromClickhouseConnection( wrap := clickhouseConnWrapper{ conn: db, settings: ClickhouseQuerySettings{ + MaxExecutionTime: os.Getenv("ClickHouseMaxExecutionTime"), MaxExecutionTimeLeaf: os.Getenv("ClickHouseMaxExecutionTimeLeaf"), TimeoutBeforeCheckingExecutionSpeed: os.Getenv("ClickHouseTimeoutBeforeCheckingExecutionSpeed"), MaxBytesToRead: os.Getenv("ClickHouseMaxBytesToRead"), @@ -3346,17 +3347,40 @@ func (r *ClickHouseReader) GetDashboardsInfo(ctx context.Context) (*model.Dashbo return &dashboardsInfo, err } totalDashboardsWithPanelAndName := 0 + var dashboardNames []string + count := 0 for _, dashboard := range dashboardsData { if isDashboardWithPanelAndName(dashboard.Data) { totalDashboardsWithPanelAndName = totalDashboardsWithPanelAndName + 1 } - dashboardsInfo = countPanelsInDashboard(dashboard.Data) + dashboardName := extractDashboardName(dashboard.Data) + if dashboardName != "" { + dashboardNames = append(dashboardNames, dashboardName) + } + dashboardInfo := countPanelsInDashboard(dashboard.Data) + dashboardsInfo.LogsBasedPanels += dashboardInfo.LogsBasedPanels + dashboardsInfo.TracesBasedPanels += dashboardInfo.TracesBasedPanels + dashboardsInfo.MetricBasedPanels += dashboardsInfo.MetricBasedPanels + if isDashboardWithTSV2(dashboard.Data) { + count = count + 1 + } } + + dashboardsInfo.DashboardNames = dashboardNames dashboardsInfo.TotalDashboards = len(dashboardsData) dashboardsInfo.TotalDashboardsWithPanelAndName = totalDashboardsWithPanelAndName + dashboardsInfo.QueriesWithTSV2 = count return &dashboardsInfo, nil } +func isDashboardWithTSV2(data map[string]interface{}) bool { + jsonData, err := json.Marshal(data) + if err != nil { + return false + } + return strings.Contains(string(jsonData), "time_series_v2") +} + func isDashboardWithPanelAndName(data map[string]interface{}) bool { isDashboardName := false isDashboardWithPanelAndName := false @@ -3376,6 +3400,19 @@ func isDashboardWithPanelAndName(data map[string]interface{}) bool { return isDashboardWithPanelAndName } + +func extractDashboardName(data map[string]interface{}) string { + + if data != nil && data["title"] != nil { + title, ok := data["title"].(string) + if ok { + return title + } + } + + return "" +} + func countPanelsInDashboard(data map[string]interface{}) model.DashboardsInfo { var logsPanelCount, tracesPanelCount, metricsPanelCount int // totalPanels := 0 @@ -5186,7 +5223,7 @@ func (r *ClickHouseReader) AddRuleStateHistory(ctx context.Context, ruleStateHis } func (r *ClickHouseReader) ReadRuleStateHistoryByRuleID( - ctx context.Context, ruleID string, params *v3.QueryRuleStateHistory) ([]v3.RuleStateHistory, error) { + ctx context.Context, ruleID string, params *v3.QueryRuleStateHistory) (*v3.RuleStateTimeline, error) { var conditions []string @@ -5194,6 +5231,10 @@ func (r *ClickHouseReader) ReadRuleStateHistoryByRuleID( conditions = append(conditions, fmt.Sprintf("unix_milli >= %d AND unix_milli < %d", params.Start, params.End)) + if params.State != "" { + conditions = append(conditions, fmt.Sprintf("state = '%s'", params.State)) + } + if params.Filters != nil && len(params.Filters.Items) != 0 { for _, item := range params.Filters.Items { toFormat := item.Value @@ -5252,7 +5293,19 @@ func (r *ClickHouseReader) ReadRuleStateHistoryByRuleID( return nil, err } - return history, nil + var total uint64 + err = r.db.QueryRow(ctx, fmt.Sprintf("SELECT count(*) FROM %s.%s WHERE %s", + signozHistoryDBName, ruleStateHistoryTableName, whereClause)).Scan(&total) + if err != nil { + return nil, err + } + + timeline := &v3.RuleStateTimeline{ + Items: history, + Total: total, + } + + return timeline, nil } func (r *ClickHouseReader) ReadRuleStateHistoryTopContributorsByRuleID( diff --git a/pkg/query-service/app/clickhouseReader/wrapper.go b/pkg/query-service/app/clickhouseReader/wrapper.go index c575aa7226..fc0a71dc8d 100644 --- a/pkg/query-service/app/clickhouseReader/wrapper.go +++ b/pkg/query-service/app/clickhouseReader/wrapper.go @@ -11,6 +11,7 @@ import ( ) type ClickhouseQuerySettings struct { + MaxExecutionTime string MaxExecutionTimeLeaf string TimeoutBeforeCheckingExecutionSpeed string MaxBytesToRead string @@ -47,6 +48,10 @@ func (c clickhouseConnWrapper) addClickHouseSettings(ctx context.Context, query settings["max_bytes_to_read"] = c.settings.MaxBytesToRead } + if c.settings.MaxExecutionTime != "" { + settings["max_execution_time"] = c.settings.MaxExecutionTime + } + if c.settings.MaxExecutionTimeLeaf != "" { settings["max_execution_time_leaf"] = c.settings.MaxExecutionTimeLeaf } diff --git a/pkg/query-service/app/http_handler.go b/pkg/query-service/app/http_handler.go index 97b5f0de2a..1210cd4f67 100644 --- a/pkg/query-service/app/http_handler.go +++ b/pkg/query-service/app/http_handler.go @@ -9,7 +9,6 @@ import ( "io" "math" "net/http" - "net/url" "regexp" "slices" "strconv" @@ -214,30 +213,15 @@ func NewAPIHandler(opts APIHandlerOpts) (*APIHandler, error) { } aH.Upgrader = &websocket.Upgrader{ - // Same-origin check is the server's responsibility in websocket spec. CheckOrigin: func(r *http.Request) bool { - // Based on the default CheckOrigin implementation in websocket package. - originHeader := r.Header.Get("Origin") - if len(originHeader) < 1 { - return false - } - origin, err := url.Parse(originHeader) - if err != nil { - return false - } - - // Allow cross origin websocket connections on localhost - if strings.HasPrefix(origin.Host, "localhost") { - return true - } - - return origin.Host == r.Host + return true }, } return aH, nil } +// todo(remove): Implemented at render package (go.signoz.io/signoz/pkg/http/render) with the new error structure type structuredResponse struct { Data interface{} `json:"data"` Total int `json:"total"` @@ -246,11 +230,13 @@ type structuredResponse struct { Errors []structuredError `json:"errors"` } +// todo(remove): Implemented at render package (go.signoz.io/signoz/pkg/http/render) with the new error structure type structuredError struct { Code int `json:"code,omitempty"` Msg string `json:"msg"` } +// todo(remove): Implemented at render package (go.signoz.io/signoz/pkg/http/render) with the new error structure type ApiResponse struct { Status status `json:"status"` Data interface{} `json:"data,omitempty"` @@ -258,6 +244,7 @@ type ApiResponse struct { Error string `json:"error,omitempty"` } +// todo(remove): Implemented at render package (go.signoz.io/signoz/pkg/http/render) with the new error structure func RespondError(w http.ResponseWriter, apiErr model.BaseApiError, data interface{}) { json := jsoniter.ConfigCompatibleWithStandardLibrary b, err := json.Marshal(&ApiResponse{ @@ -301,6 +288,7 @@ func RespondError(w http.ResponseWriter, apiErr model.BaseApiError, data interfa } } +// todo(remove): Implemented at render package (go.signoz.io/signoz/pkg/http/render) with the new error structure func writeHttpResponse(w http.ResponseWriter, data interface{}) { json := jsoniter.ConfigCompatibleWithStandardLibrary b, err := json.Marshal(&ApiResponse{ @@ -333,19 +321,25 @@ func (aH *APIHandler) RegisterQueryRangeV3Routes(router *mux.Router, am *AuthMid subRouter.HandleFunc("/filter_suggestions", am.ViewAccess(aH.getQueryBuilderSuggestions)).Methods(http.MethodGet) - // websocket handler for query progress + // TODO(Raj): Remove this handler after /ws based path has been completely rolled out. subRouter.HandleFunc("/query_progress", am.ViewAccess(aH.GetQueryProgressUpdates)).Methods(http.MethodGet) // live logs subRouter.HandleFunc("/logs/livetail", am.ViewAccess(aH.liveTailLogs)).Methods(http.MethodGet) } +func (aH *APIHandler) RegisterWebSocketPaths(router *mux.Router, am *AuthMiddleware) { + subRouter := router.PathPrefix("/ws").Subrouter() + subRouter.HandleFunc("/query_progress", am.ViewAccess(aH.GetQueryProgressUpdates)).Methods(http.MethodGet) +} + func (aH *APIHandler) RegisterQueryRangeV4Routes(router *mux.Router, am *AuthMiddleware) { subRouter := router.PathPrefix("/api/v4").Subrouter() subRouter.HandleFunc("/query_range", am.ViewAccess(aH.QueryRangeV4)).Methods(http.MethodPost) subRouter.HandleFunc("/metric/metric_metadata", am.ViewAccess(aH.getMetricMetadata)).Methods(http.MethodGet) } +// todo(remove): Implemented at render package (go.signoz.io/signoz/pkg/http/render) with the new error structure func (aH *APIHandler) Respond(w http.ResponseWriter, data interface{}) { writeHttpResponse(w, data) } @@ -722,6 +716,13 @@ func (aH *APIHandler) getRuleStats(w http.ResponseWriter, r *http.Request) { RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil) return } + if math.IsNaN(currentAvgResolutionTime) || math.IsInf(currentAvgResolutionTime, 0) { + currentAvgResolutionTime = 0 + } + if math.IsNaN(pastAvgResolutionTime) || math.IsInf(pastAvgResolutionTime, 0) { + pastAvgResolutionTime = 0 + } + stats := v3.Stats{ TotalCurrentTriggers: totalCurrentTriggers, TotalPastTriggers: totalPastTriggers, @@ -794,6 +795,37 @@ func (aH *APIHandler) getRuleStateHistory(w http.ResponseWriter, r *http.Request RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil) return } + + rule, err := aH.ruleManager.GetRule(r.Context(), ruleID) + if err == nil { + for idx := range res.Items { + lbls := make(map[string]string) + err := json.Unmarshal([]byte(res.Items[idx].Labels), &lbls) + if err != nil { + continue + } + filterItems := []v3.FilterItem{} + if rule.AlertType == rules.AlertTypeLogs || rule.AlertType == rules.AlertTypeTraces { + if rule.RuleCondition.CompositeQuery != nil { + if rule.RuleCondition.QueryType() == v3.QueryTypeBuilder { + for _, query := range rule.RuleCondition.CompositeQuery.BuilderQueries { + if query.Filters != nil && len(query.Filters.Items) > 0 { + filterItems = append(filterItems, query.Filters.Items...) + } + } + } + } + } + newFilters := common.PrepareFilters(lbls, filterItems) + ts := time.Unix(res.Items[idx].UnixMilli/1000, 0) + if rule.AlertType == rules.AlertTypeLogs { + res.Items[idx].RelatedLogsLink = common.PrepareLinksToLogs(ts, newFilters) + } else if rule.AlertType == rules.AlertTypeTraces { + res.Items[idx].RelatedTracesLink = common.PrepareLinksToTraces(ts, newFilters) + } + } + } + aH.Respond(w, res) } @@ -811,6 +843,25 @@ func (aH *APIHandler) getRuleStateHistoryTopContributors(w http.ResponseWriter, RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil) return } + + rule, err := aH.ruleManager.GetRule(r.Context(), ruleID) + if err == nil { + for idx := range res { + lbls := make(map[string]string) + err := json.Unmarshal([]byte(res[idx].Labels), &lbls) + if err != nil { + continue + } + ts := time.Unix(params.End/1000, 0) + filters := common.PrepareFilters(lbls, nil) + if rule.AlertType == rules.AlertTypeLogs { + res[idx].RelatedLogsLink = common.PrepareLinksToLogs(ts, filters) + } else if rule.AlertType == rules.AlertTypeTraces { + res[idx].RelatedTracesLink = common.PrepareLinksToTraces(ts, filters) + } + } + } + aH.Respond(w, res) } @@ -2445,10 +2496,113 @@ func (aH *APIHandler) RegisterMessagingQueuesRoutes(router *mux.Router, am *Auth kafkaSubRouter.HandleFunc("/producer-details", am.ViewAccess(aH.getProducerData)).Methods(http.MethodPost) kafkaSubRouter.HandleFunc("/consumer-details", am.ViewAccess(aH.getConsumerData)).Methods(http.MethodPost) + kafkaSubRouter.HandleFunc("/network-latency", am.ViewAccess(aH.getNetworkData)).Methods(http.MethodPost) // for other messaging queues, add SubRouters here } +// not using md5 hashing as the plain string would work +func uniqueIdentifier(clientID, serviceInstanceID, serviceName, separator string) string { + return clientID + separator + serviceInstanceID + separator + serviceName +} + +func (aH *APIHandler) getNetworkData( + w http.ResponseWriter, r *http.Request, +) { + attributeCache := &mq.Clients{ + Hash: make(map[string]struct{}), + } + messagingQueue, apiErr := ParseMessagingQueueBody(r) + + if apiErr != nil { + zap.L().Error(apiErr.Err.Error()) + RespondError(w, apiErr, nil) + return + } + + queryRangeParams, err := mq.BuildQRParamsNetwork(messagingQueue, "throughput", attributeCache) + if err != nil { + zap.L().Error(err.Error()) + RespondError(w, apiErr, nil) + return + } + if err := validateQueryRangeParamsV3(queryRangeParams); err != nil { + zap.L().Error(err.Error()) + RespondError(w, apiErr, nil) + return + } + + var result []*v3.Result + var errQueriesByName map[string]error + + result, errQueriesByName, err = aH.querierV2.QueryRange(r.Context(), queryRangeParams, nil) + if err != nil { + apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err} + RespondError(w, apiErrObj, errQueriesByName) + return + } + + for _, res := range result { + for _, series := range res.Series { + clientID, clientIDOk := series.Labels["client_id"] + serviceInstanceID, serviceInstanceIDOk := series.Labels["service_instance_id"] + serviceName, serviceNameOk := series.Labels["service_name"] + hashKey := uniqueIdentifier(clientID, serviceInstanceID, serviceName, "#") + _, ok := attributeCache.Hash[hashKey] + if clientIDOk && serviceInstanceIDOk && serviceNameOk && !ok { + attributeCache.Hash[hashKey] = struct{}{} + attributeCache.ClientID = append(attributeCache.ClientID, clientID) + attributeCache.ServiceInstanceID = append(attributeCache.ServiceInstanceID, serviceInstanceID) + attributeCache.ServiceName = append(attributeCache.ServiceName, serviceName) + } + } + } + + queryRangeParams, err = mq.BuildQRParamsNetwork(messagingQueue, "fetch-latency", attributeCache) + if err != nil { + zap.L().Error(err.Error()) + RespondError(w, apiErr, nil) + return + } + if err := validateQueryRangeParamsV3(queryRangeParams); err != nil { + zap.L().Error(err.Error()) + RespondError(w, apiErr, nil) + return + } + + resultFetchLatency, errQueriesByNameFetchLatency, err := aH.querierV2.QueryRange(r.Context(), queryRangeParams, nil) + if err != nil { + apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err} + RespondError(w, apiErrObj, errQueriesByNameFetchLatency) + return + } + + latencyColumn := &v3.Result{QueryName: "latency"} + var latencySeries []*v3.Series + for _, res := range resultFetchLatency { + for _, series := range res.Series { + clientID, clientIDOk := series.Labels["client_id"] + serviceInstanceID, serviceInstanceIDOk := series.Labels["service_instance_id"] + serviceName, serviceNameOk := series.Labels["service_name"] + hashKey := uniqueIdentifier(clientID, serviceInstanceID, serviceName, "#") + _, ok := attributeCache.Hash[hashKey] + if clientIDOk && serviceInstanceIDOk && serviceNameOk && ok { + latencySeries = append(latencySeries, series) + } + } + } + + latencyColumn.Series = latencySeries + result = append(result, latencyColumn) + + resultFetchLatency = postprocess.TransformToTableForBuilderQueries(result, queryRangeParams) + + resp := v3.QueryRangeResponse{ + Result: resultFetchLatency, + } + aH.Respond(w, resp) +} + func (aH *APIHandler) getProducerData( w http.ResponseWriter, r *http.Request, ) { @@ -3753,7 +3907,7 @@ func (aH *APIHandler) GetQueryProgressUpdates(w http.ResponseWriter, r *http.Req // Shouldn't happen unless query progress requested after query finished zap.L().Warn( "couldn't subscribe to query progress", - zap.String("queryId", queryId), zap.Any("error", err), + zap.String("queryId", queryId), zap.Any("error", apiErr), ) return } diff --git a/pkg/query-service/app/integrations/messagingQueues/kafka/consumerLag.md b/pkg/query-service/app/integrations/messagingQueues/kafka/consumerLag.md index c34bc7ad64..38b61669ff 100644 --- a/pkg/query-service/app/integrations/messagingQueues/kafka/consumerLag.md +++ b/pkg/query-service/app/integrations/messagingQueues/kafka/consumerLag.md @@ -1,11 +1,6 @@ ## Consumer Lag feature break down -### 1) Consumer Lag Graph - - ---- - -### 2) Consumer Group Details +### 1) Consumer Group Details API endpoint: @@ -13,75 +8,75 @@ API endpoint: POST /api/v1/messaging-queues/kafka/consumer-lag/consumer-details ``` +Request-Body ```json { - "start": 1720685296000000000, - "end": 1721290096000000000, - "variables": { - "partition": "0", - "topic": "topic1", - "consumer_group": "cg1" - } + "start": 1724429217000000000, + "end": 1724431017000000000, + "variables": { + "partition": "0", + "topic": "topic1", + "consumer_group": "cg1" + } } ``` - -response in query range format `series` +Response in query range `table` format ```json { - "status": "success", - "data": { - "resultType": "", - "result": [ + "status": "success", + "data": { + "resultType": "", + "result": [ + { + "table": { + "columns": [ { - "table": { - "columns": [ - { - "name": "service_name", - "queryName": "", - "isValueColumn": false - }, - { - "name": "p99", - "queryName": "", - "isValueColumn": false - }, - { - "name": "error_rate", - "queryName": "", - "isValueColumn": false - }, - { - "name": "throughput", - "queryName": "", - "isValueColumn": false - }, - { - "name": "avg_msg_size", - "queryName": "", - "isValueColumn": false - } - ], - "rows": [ - { - "data": { - "avg_msg_size": "0", - "error_rate": "0", - "p99": "0.2942205100000016", - "service_name": "consumer-svc", - "throughput": "0.00016534391534391533" - } - } - ] - } + "name": "service_name", + "queryName": "", + "isValueColumn": false + }, + { + "name": "p99", + "queryName": "", + "isValueColumn": false + }, + { + "name": "error_rate", + "queryName": "", + "isValueColumn": false + }, + { + "name": "throughput", + "queryName": "", + "isValueColumn": false + }, + { + "name": "avg_msg_size", + "queryName": "", + "isValueColumn": false } - ] - } + ], + "rows": [ + { + "data": { + "avg_msg_size": "15", + "error_rate": "0", + "p99": "0.47993265000000035", + "service_name": "consumer-svc", + "throughput": "39.86888888888889" + } + } + ] + } + } + ] + } } ``` +---- - -### 3) Producer Details +### 2) Producer Details API endpoint: @@ -89,18 +84,19 @@ API endpoint: POST /api/v1/messaging-queues/kafka/consumer-lag/producer-details ``` +Request-Body ```json { - "start": 1720685296000000000, - "end": 1721290096000000000, + "start": 1724429217000000000, + "end": 1724431017000000000, "variables": { - "partition": "0", + "partition": "0", "topic": "topic1" } } ``` -response in query range format `series` +Response in query range `table` format ```json { "status": "success", @@ -116,17 +112,17 @@ response in query range format `series` "isValueColumn": false }, { - "name": "p99_query.p99", + "name": "p99", "queryName": "", "isValueColumn": false }, { - "name": "error_rate", + "name": "error_percentage", "queryName": "", "isValueColumn": false }, { - "name": "rps", + "name": "throughput", "queryName": "", "isValueColumn": false } @@ -134,56 +130,9 @@ response in query range format `series` "rows": [ { "data": { - "error_rate": "0", - "p99_query.p99": "150.08830908000002", - "rps": "0.00016534391534391533", - "service_name": "producer-svc" - } - } - ] - } - } - ] - } -} -``` -response in query range format `table` -```json -{ - "status": "success", - "data": { - "resultType": "", - "result": [ - { - "table": { - "columns": [ - { - "name": "service_name", - "queryName": "", - "isValueColumn": false - }, - { - "name": "p99_query.p99", - "queryName": "", - "isValueColumn": false - }, - { - "name": "error_rate", - "queryName": "", - "isValueColumn": false - }, - { - "name": "rps", - "queryName": "", - "isValueColumn": false - } - ], - "rows": [ - { - "data": { - "error_rate": "0", - "p99_query.p99": "150.08830908000002", - "rps": "0.00016534391534391533", + "error_percentage": "0", + "p99": "5.51359028", + "throughput": "39.86888888888889", "service_name": "producer-svc" } } @@ -195,3 +144,85 @@ response in query range format `table` } ``` +### 3) Network Fetch Latency: + +API endpoint: + +``` +POST /api/v1/messaging-queues/kafka/consumer-lag/network-latency +``` + +Request-Body +```json +{ + "start": 1724673937000000000, + "end": 1724675737000000000, + "variables": { + "consumer_group": "cg1", + "partition": "0" + } +} +``` + +Response in query range `table` format +```json +{ + "status": "success", + "data": { + "resultType": "", + "result": [ + { + "table": { + "columns": [ + { + "name": "service_name", + "queryName": "", + "isValueColumn": false + }, + { + "name": "client_id", + "queryName": "", + "isValueColumn": false + }, + { + "name": "service_instance_id", + "queryName": "", + "isValueColumn": false + }, + { + "name": "latency", + "queryName": "latency", + "isValueColumn": true + }, + { + "name": "throughput", + "queryName": "throughput", + "isValueColumn": true + } + ], + "rows": [ + { + "data": { + "client_id": "consumer-cg1-1", + "latency": 48.99, + "service_instance_id": "b0a851d7-1735-4e3f-8f5f-7c63a8a55a24", + "service_name": "consumer-svc", + "throughput": 14.97 + } + }, + { + "data": { + "client_id": "consumer-cg1-1", + "latency": 25.21, + "service_instance_id": "ccf49550-2e8f-4c7b-be29-b9e0891ef93d", + "service_name": "consumer-svc", + "throughput": 24.91 + } + } + ] + } + } + ] + } +} +``` diff --git a/pkg/query-service/app/integrations/messagingQueues/kafka/model.go b/pkg/query-service/app/integrations/messagingQueues/kafka/model.go index b24734cf48..f587868610 100644 --- a/pkg/query-service/app/integrations/messagingQueues/kafka/model.go +++ b/pkg/query-service/app/integrations/messagingQueues/kafka/model.go @@ -7,3 +7,10 @@ type MessagingQueue struct { End int64 `json:"end"` Variables map[string]string `json:"variables,omitempty"` } + +type Clients struct { + Hash map[string]struct{} + ClientID []string + ServiceInstanceID []string + ServiceName []string +} diff --git a/pkg/query-service/app/integrations/messagingQueues/kafka/sql.go b/pkg/query-service/app/integrations/messagingQueues/kafka/sql.go index e06e35efde..eb06689ef3 100644 --- a/pkg/query-service/app/integrations/messagingQueues/kafka/sql.go +++ b/pkg/query-service/app/integrations/messagingQueues/kafka/sql.go @@ -26,7 +26,6 @@ WITH consumer_query AS ( GROUP BY serviceName ) --- Main query to select all metrics SELECT serviceName AS service_name, p99, @@ -65,7 +64,7 @@ SELECT serviceName AS service_name, p99, COALESCE((error_count * 100.0) / total_count, 0) AS error_percentage, - COALESCE(total_count / %d, 0) AS rps -- Convert nanoseconds to seconds + COALESCE(total_count / %d, 0) AS throughput -- Convert nanoseconds to seconds FROM producer_query ORDER BY @@ -74,3 +73,25 @@ ORDER BY `, start, end, queueType, topic, partition, timeRange) return query } + +func generateNetworkLatencyThroughputSQL(start, end int64, consumerGroup, partitionID, queueType string) string { + timeRange := (end - start) / 1000000000 + query := fmt.Sprintf(` +SELECT + stringTagMap['messaging.client_id'] AS client_id, + stringTagMap['service.instance.id'] AS service_instance_id, + serviceName AS service_name, + count(*) / %d AS throughput +FROM signoz_traces.distributed_signoz_index_v2 +WHERE + timestamp >= '%d' + AND timestamp <= '%d' + AND kind = 5 + AND msgSystem = '%s' + AND stringTagMap['messaging.kafka.consumer.group'] = '%s' + AND stringTagMap['messaging.destination.partition.id'] = '%s' +GROUP BY service_name, client_id, service_instance_id +ORDER BY throughput DESC +`, timeRange, start, end, queueType, consumerGroup, partitionID) + return query +} diff --git a/pkg/query-service/app/integrations/messagingQueues/kafka/translator.go b/pkg/query-service/app/integrations/messagingQueues/kafka/translator.go index 98414ebf0f..7f4266df67 100644 --- a/pkg/query-service/app/integrations/messagingQueues/kafka/translator.go +++ b/pkg/query-service/app/integrations/messagingQueues/kafka/translator.go @@ -2,7 +2,9 @@ package kafka import ( "fmt" + "strings" + "go.signoz.io/signoz/pkg/query-service/common" v3 "go.signoz.io/signoz/pkg/query-service/model/v3" ) @@ -35,6 +37,146 @@ func BuildQueryRangeParams(messagingQueue *MessagingQueue, queryContext string) return queryRangeParams, nil } +func buildClickHouseQueryNetwork(messagingQueue *MessagingQueue, queueType string) (*v3.ClickHouseQuery, error) { + start := messagingQueue.Start + end := messagingQueue.End + consumerGroup, ok := messagingQueue.Variables["consumer_group"] + if !ok { + return nil, fmt.Errorf("consumer_group not found in the request") + } + + partitionID, ok := messagingQueue.Variables["partition"] + if !ok { + return nil, fmt.Errorf("partition not found in the request") + } + + query := generateNetworkLatencyThroughputSQL(start, end, consumerGroup, partitionID, queueType) + + return &v3.ClickHouseQuery{ + Query: query, + }, nil +} + +func formatstring(str []string) string { + joined := strings.Join(str, ", ") + if len(joined) <= 2 { + return "" + } + return joined[1 : len(joined)-1] +} + +func buildBuilderQueriesNetwork(unixMilliStart, unixMilliEnd int64, attributeCache *Clients) (map[string]*v3.BuilderQuery, error) { + bq := make(map[string]*v3.BuilderQuery) + queryName := fmt.Sprintf("latency") + + chq := &v3.BuilderQuery{ + QueryName: queryName, + StepInterval: common.MinAllowedStepInterval(unixMilliStart, unixMilliEnd), + DataSource: v3.DataSourceMetrics, + AggregateAttribute: v3.AttributeKey{ + Key: "kafka_consumer_fetch_latency_avg", + }, + AggregateOperator: v3.AggregateOperatorAvg, + Temporality: v3.Unspecified, + TimeAggregation: v3.TimeAggregationAvg, + SpaceAggregation: v3.SpaceAggregationAvg, + Filters: &v3.FilterSet{ + Operator: "AND", + Items: []v3.FilterItem{ + { + Key: v3.AttributeKey{ + Key: "service_name", + Type: v3.AttributeKeyTypeTag, + DataType: v3.AttributeKeyDataTypeString, + }, + Operator: v3.FilterOperatorIn, + Value: attributeCache.ServiceName, + }, + { + Key: v3.AttributeKey{ + Key: "client_id", + Type: v3.AttributeKeyTypeTag, + DataType: v3.AttributeKeyDataTypeString, + }, + Operator: v3.FilterOperatorIn, + Value: attributeCache.ClientID, + }, + { + Key: v3.AttributeKey{ + Key: "service_instance_id", + Type: v3.AttributeKeyTypeTag, + DataType: v3.AttributeKeyDataTypeString, + }, + Operator: v3.FilterOperatorIn, + Value: attributeCache.ServiceInstanceID, + }, + }, + }, + Expression: queryName, + ReduceTo: v3.ReduceToOperatorAvg, + GroupBy: []v3.AttributeKey{{ + Key: "service_name", + DataType: v3.AttributeKeyDataTypeString, + Type: v3.AttributeKeyTypeTag, + }, + { + Key: "client_id", + DataType: v3.AttributeKeyDataTypeString, + Type: v3.AttributeKeyTypeTag, + }, + { + Key: "service_instance_id", + DataType: v3.AttributeKeyDataTypeString, + Type: v3.AttributeKeyTypeTag, + }, + }, + } + bq[queryName] = chq + return bq, nil +} + +func BuildQRParamsNetwork(messagingQueue *MessagingQueue, queryContext string, attributeCache *Clients) (*v3.QueryRangeParamsV3, error) { + + queueType := kafkaQueue + + unixMilliStart := messagingQueue.Start / 1000000 + unixMilliEnd := messagingQueue.End / 1000000 + + var cq *v3.CompositeQuery + + if queryContext == "throughput" { + chq, err := buildClickHouseQueryNetwork(messagingQueue, queueType) + + if err != nil { + return nil, err + } + + cq, err = buildCompositeQuery(chq, queryContext) + + } else if queryContext == "fetch-latency" { + bhq, err := buildBuilderQueriesNetwork(unixMilliStart, unixMilliEnd, attributeCache) + if err != nil { + return nil, err + } + cq = &v3.CompositeQuery{ + QueryType: v3.QueryTypeBuilder, + BuilderQueries: bhq, + PanelType: v3.PanelTypeTable, + } + } + + queryRangeParams := &v3.QueryRangeParamsV3{ + Start: unixMilliStart, + End: unixMilliEnd, + Step: defaultStepInterval, + CompositeQuery: cq, + Version: "v4", + FormatForWeb: true, + } + + return queryRangeParams, nil +} + func buildClickHouseQuery(messagingQueue *MessagingQueue, queueType string, queryContext string) (*v3.ClickHouseQuery, error) { start := messagingQueue.Start end := messagingQueue.End @@ -48,15 +190,14 @@ func buildClickHouseQuery(messagingQueue *MessagingQueue, queueType string, quer return nil, fmt.Errorf("invalid type for Partition") } - consumerGroup, ok := messagingQueue.Variables["consumer_group"] - if !ok { - return nil, fmt.Errorf("invalid type for consumer group") - } - var query string if queryContext == "producer" { query = generateProducerSQL(start, end, topic, partition, queueType) } else if queryContext == "consumer" { + consumerGroup, ok := messagingQueue.Variables["consumer_group"] + if !ok { + return nil, fmt.Errorf("invalid type for consumer group") + } query = generateConsumerSQL(start, end, topic, partition, consumerGroup, queueType) } diff --git a/pkg/query-service/app/server.go b/pkg/query-service/app/server.go index 3f5352edbd..77caa9170b 100644 --- a/pkg/query-service/app/server.go +++ b/pkg/query-service/app/server.go @@ -303,6 +303,7 @@ func (s *Server) createPublicServer(api *APIHandler) (*http.Server, error) { api.RegisterLogsRoutes(r, am) api.RegisterIntegrationRoutes(r, am) api.RegisterQueryRangeV3Routes(r, am) + api.RegisterWebSocketPaths(r, am) api.RegisterQueryRangeV4Routes(r, am) api.RegisterMessagingQueuesRoutes(r, am) @@ -321,6 +322,7 @@ func (s *Server) createPublicServer(api *APIHandler) (*http.Server, error) { }, nil } +// TODO(remove): Implemented at pkg/http/middleware/logging.go // loggingMiddleware is used for logging public api calls func loggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -391,6 +393,7 @@ func LogCommentEnricher(next http.Handler) http.Handler { }) } +// TODO(remove): Implemented at pkg/http/middleware/logging.go // loggingMiddlewarePrivate is used for logging private api calls // from internal services like alert manager func loggingMiddlewarePrivate(next http.Handler) http.Handler { @@ -403,27 +406,32 @@ func loggingMiddlewarePrivate(next http.Handler) http.Handler { }) } +// TODO(remove): Implemented at pkg/http/middleware/logging.go type loggingResponseWriter struct { http.ResponseWriter statusCode int } +// TODO(remove): Implemented at pkg/http/middleware/logging.go func NewLoggingResponseWriter(w http.ResponseWriter) *loggingResponseWriter { // WriteHeader(int) is not called if our response implicitly returns 200 OK, so // we default to that status code. return &loggingResponseWriter{w, http.StatusOK} } +// TODO(remove): Implemented at pkg/http/middleware/logging.go func (lrw *loggingResponseWriter) WriteHeader(code int) { lrw.statusCode = code lrw.ResponseWriter.WriteHeader(code) } +// TODO(remove): Implemented at pkg/http/middleware/logging.go // Flush implements the http.Flush interface. func (lrw *loggingResponseWriter) Flush() { lrw.ResponseWriter.(http.Flusher).Flush() } +// TODO(remove): Implemented at pkg/http/middleware/logging.go // Support websockets func (lrw *loggingResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { h, ok := lrw.ResponseWriter.(http.Hijacker) @@ -537,6 +545,7 @@ func (s *Server) analyticsMiddleware(next http.Handler) http.Handler { }) } +// TODO(remove): Implemented at pkg/http/middleware/timeout.go func getRouteContextTimeout(overrideTimeout string) time.Duration { var timeout time.Duration var err error @@ -553,6 +562,7 @@ func getRouteContextTimeout(overrideTimeout string) time.Duration { return constants.ContextTimeout } +// TODO(remove): Implemented at pkg/http/middleware/timeout.go func setTimeoutMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() diff --git a/pkg/query-service/app/server_test.go b/pkg/query-service/app/server_test.go index afdc06fa33..49fc6191fb 100644 --- a/pkg/query-service/app/server_test.go +++ b/pkg/query-service/app/server_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/assert" ) +// TODO(remove): Implemented at pkg/http/middleware/timeout_test.go func TestGetRouteContextTimeout(t *testing.T) { var testGetRouteContextTimeoutData = []struct { Name string diff --git a/pkg/query-service/collectorsimulator/inmemoryexporter/config_test.go b/pkg/query-service/collectorsimulator/inmemoryexporter/config_test.go index 29749757dc..c9628451d4 100644 --- a/pkg/query-service/collectorsimulator/inmemoryexporter/config_test.go +++ b/pkg/query-service/collectorsimulator/inmemoryexporter/config_test.go @@ -34,7 +34,7 @@ func TestValidate(t *testing.T) { t.Run(tt.name, func(t *testing.T) { factory := NewFactory() cfg := factory.CreateDefaultConfig() - err := component.UnmarshalConfig(tt.rawConf, cfg) + err := tt.rawConf.Unmarshal(cfg) require.NoError(t, err, "could not UnmarshalConfig") err = component.ValidateConfig(cfg) diff --git a/pkg/query-service/collectorsimulator/inmemoryexporter/exporter_test.go b/pkg/query-service/collectorsimulator/inmemoryexporter/exporter_test.go index 4fe4753d72..a2d60439b4 100644 --- a/pkg/query-service/collectorsimulator/inmemoryexporter/exporter_test.go +++ b/pkg/query-service/collectorsimulator/inmemoryexporter/exporter_test.go @@ -6,7 +6,6 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/require" - "go.opentelemetry.io/collector/component" "go.opentelemetry.io/collector/component/componenttest" "go.opentelemetry.io/collector/confmap" "go.opentelemetry.io/collector/exporter" @@ -57,9 +56,7 @@ func makeTestExporter(exporterId string) (exporter.Logs, error) { factory := NewFactory() cfg := factory.CreateDefaultConfig() - component.UnmarshalConfig(confmap.NewFromStringMap( - map[string]interface{}{"id": exporterId}), cfg, - ) + confmap.NewFromStringMap(map[string]any{"id": exporterId}).Unmarshal(&cfg) return factory.CreateLogsExporter( context.Background(), exporter.CreateSettings{}, cfg, diff --git a/pkg/query-service/collectorsimulator/inmemoryreceiver/config_test.go b/pkg/query-service/collectorsimulator/inmemoryreceiver/config_test.go index a0daf71c45..5a6144a379 100644 --- a/pkg/query-service/collectorsimulator/inmemoryreceiver/config_test.go +++ b/pkg/query-service/collectorsimulator/inmemoryreceiver/config_test.go @@ -34,7 +34,7 @@ func TestValidate(t *testing.T) { t.Run(tt.name, func(t *testing.T) { factory := NewFactory() cfg := factory.CreateDefaultConfig() - err := component.UnmarshalConfig(tt.rawConf, cfg) + err := tt.rawConf.Unmarshal(&cfg) require.NoError(t, err, "could not UnmarshalConfig") err = component.ValidateConfig(cfg) diff --git a/pkg/query-service/collectorsimulator/inmemoryreceiver/receiver_test.go b/pkg/query-service/collectorsimulator/inmemoryreceiver/receiver_test.go index 4fe7169cc7..9205147156 100644 --- a/pkg/query-service/collectorsimulator/inmemoryreceiver/receiver_test.go +++ b/pkg/query-service/collectorsimulator/inmemoryreceiver/receiver_test.go @@ -6,7 +6,6 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/require" - "go.opentelemetry.io/collector/component" "go.opentelemetry.io/collector/component/componenttest" "go.opentelemetry.io/collector/confmap" "go.opentelemetry.io/collector/consumer/consumertest" @@ -58,9 +57,8 @@ func makeTestLogReceiver(receiverId string) (receiver.Logs, error) { factory := NewFactory() cfg := factory.CreateDefaultConfig() - component.UnmarshalConfig(confmap.NewFromStringMap( - map[string]interface{}{"id": receiverId}), cfg, - ) + + confmap.NewFromStringMap(map[string]any{"id": receiverId}).Unmarshal(&cfg) return factory.CreateLogsReceiver( context.Background(), receiver.CreateSettings{}, cfg, consumertest.NewNop(), diff --git a/pkg/query-service/common/query_range.go b/pkg/query-service/common/query_range.go index c352c7d9f2..e0c675c50a 100644 --- a/pkg/query-service/common/query_range.go +++ b/pkg/query-service/common/query_range.go @@ -1,7 +1,10 @@ package common import ( + "encoding/json" + "fmt" "math" + "net/url" "time" "go.signoz.io/signoz/pkg/query-service/constants" @@ -70,3 +73,183 @@ func LCMList(nums []int64) int64 { } return result } + +// TODO(srikanthccv): move the custom function in threshold_rule.go to here +func PrepareLinksToTraces(ts time.Time, filterItems []v3.FilterItem) string { + + start := ts.Add(-time.Minute * 15) + end := ts.Add(time.Minute * 15) + + // Traces list view expects time in nanoseconds + tr := v3.URLShareableTimeRange{ + Start: start.UnixNano(), + End: end.UnixNano(), + PageSize: 100, + } + + options := v3.URLShareableOptions{ + MaxLines: 2, + Format: "list", + SelectColumns: constants.TracesListViewDefaultSelectedColumns, + } + + period, _ := json.Marshal(tr) + urlEncodedTimeRange := url.QueryEscape(string(period)) + + urlData := v3.URLShareableCompositeQuery{ + QueryType: string(v3.QueryTypeBuilder), + Builder: v3.URLShareableBuilderQuery{ + QueryData: []v3.BuilderQuery{ + { + DataSource: v3.DataSourceTraces, + QueryName: "A", + AggregateOperator: v3.AggregateOperatorNoOp, + AggregateAttribute: v3.AttributeKey{}, + Filters: &v3.FilterSet{ + Items: filterItems, + Operator: "AND", + }, + Expression: "A", + Disabled: false, + Having: []v3.Having{}, + StepInterval: 60, + OrderBy: []v3.OrderBy{ + { + ColumnName: "timestamp", + Order: "desc", + }, + }, + }, + }, + QueryFormulas: make([]string, 0), + }, + } + + data, _ := json.Marshal(urlData) + compositeQuery := url.QueryEscape(url.QueryEscape(string(data))) + + optionsData, _ := json.Marshal(options) + urlEncodedOptions := url.QueryEscape(string(optionsData)) + + return fmt.Sprintf("compositeQuery=%s&timeRange=%s&startTime=%d&endTime=%d&options=%s", compositeQuery, urlEncodedTimeRange, tr.Start, tr.End, urlEncodedOptions) +} + +func PrepareLinksToLogs(ts time.Time, filterItems []v3.FilterItem) string { + start := ts.Add(-time.Minute * 15) + end := ts.Add(time.Minute * 15) + + // Logs list view expects time in milliseconds + // Logs list view expects time in milliseconds + tr := v3.URLShareableTimeRange{ + Start: start.UnixMilli(), + End: end.UnixMilli(), + PageSize: 100, + } + + options := v3.URLShareableOptions{ + MaxLines: 2, + Format: "list", + SelectColumns: []v3.AttributeKey{}, + } + + period, _ := json.Marshal(tr) + urlEncodedTimeRange := url.QueryEscape(string(period)) + + urlData := v3.URLShareableCompositeQuery{ + QueryType: string(v3.QueryTypeBuilder), + Builder: v3.URLShareableBuilderQuery{ + QueryData: []v3.BuilderQuery{ + { + DataSource: v3.DataSourceLogs, + QueryName: "A", + AggregateOperator: v3.AggregateOperatorNoOp, + AggregateAttribute: v3.AttributeKey{}, + Filters: &v3.FilterSet{ + Items: filterItems, + Operator: "AND", + }, + Expression: "A", + Disabled: false, + Having: []v3.Having{}, + StepInterval: 60, + OrderBy: []v3.OrderBy{ + { + ColumnName: "timestamp", + Order: "desc", + }, + }, + }, + }, + QueryFormulas: make([]string, 0), + }, + } + + data, _ := json.Marshal(urlData) + compositeQuery := url.QueryEscape(url.QueryEscape(string(data))) + + optionsData, _ := json.Marshal(options) + urlEncodedOptions := url.QueryEscape(string(optionsData)) + + return fmt.Sprintf("compositeQuery=%s&timeRange=%s&startTime=%d&endTime=%d&options=%s", compositeQuery, urlEncodedTimeRange, tr.Start, tr.End, urlEncodedOptions) +} + +// The following function is used to prepare the where clause for the query +// `lbls` contains the key value pairs of the labels from the result of the query +// We iterate over the where clause and replace the labels with the actual values +// There are two cases: +// 1. The label is present in the where clause +// 2. The label is not present in the where clause +// +// Example for case 2: +// Latency by serviceName without any filter +// In this case, for each service with latency > threshold we send a notification +// The expectation will be that clicking on the related traces for service A, will +// take us to the traces page with the filter serviceName=A +// So for all the missing labels in the where clause, we add them as key = value +// +// Example for case 1: +// Severity text IN (WARN, ERROR) +// In this case, the Severity text will appear in the `lbls` if it were part of the group +// by clause, in which case we replace it with the actual value for the notification +// i.e Severity text = WARN +// If the Severity text is not part of the group by clause, then we add it as it is +func PrepareFilters(labels map[string]string, filters []v3.FilterItem) []v3.FilterItem { + var filterItems []v3.FilterItem + + added := make(map[string]struct{}) + + for _, item := range filters { + exists := false + for key, value := range labels { + if item.Key.Key == key { + // if the label is present in the where clause, replace it with key = value + filterItems = append(filterItems, v3.FilterItem{ + Key: item.Key, + Operator: v3.FilterOperatorEqual, + Value: value, + }) + exists = true + added[key] = struct{}{} + break + } + } + + if !exists { + // if the label is not present in the where clause, add it as it is + filterItems = append(filterItems, item) + } + } + + // add the labels which are not present in the where clause + for key, value := range labels { + if _, ok := added[key]; !ok { + filterItems = append(filterItems, v3.FilterItem{ + Key: v3.AttributeKey{Key: key}, + Operator: v3.FilterOperatorEqual, + Value: value, + }) + } + } + + return filterItems +} diff --git a/pkg/query-service/interfaces/interface.go b/pkg/query-service/interfaces/interface.go index 4086947d4f..f275579104 100644 --- a/pkg/query-service/interfaces/interface.go +++ b/pkg/query-service/interfaces/interface.go @@ -110,7 +110,7 @@ type Reader interface { AddRuleStateHistory(ctx context.Context, ruleStateHistory []v3.RuleStateHistory) error GetOverallStateTransitions(ctx context.Context, ruleID string, params *v3.QueryRuleStateHistory) ([]v3.RuleStateTransition, error) - ReadRuleStateHistoryByRuleID(ctx context.Context, ruleID string, params *v3.QueryRuleStateHistory) ([]v3.RuleStateHistory, error) + ReadRuleStateHistoryByRuleID(ctx context.Context, ruleID string, params *v3.QueryRuleStateHistory) (*v3.RuleStateTimeline, error) GetTotalTriggers(ctx context.Context, ruleID string, params *v3.QueryRuleStateHistory) (uint64, error) GetTriggersByInterval(ctx context.Context, ruleID string, params *v3.QueryRuleStateHistory) (*v3.Series, error) GetAvgResolutionTime(ctx context.Context, ruleID string, params *v3.QueryRuleStateHistory) (float64, error) diff --git a/pkg/query-service/migrate/0_45_alerts_to_v4/run.go b/pkg/query-service/migrate/0_45_alerts_to_v4/run.go deleted file mode 100644 index f68f4ca43b..0000000000 --- a/pkg/query-service/migrate/0_45_alerts_to_v4/run.go +++ /dev/null @@ -1,153 +0,0 @@ -package alertstov4 - -import ( - "context" - "encoding/json" - - "github.com/jmoiron/sqlx" - v3 "go.signoz.io/signoz/pkg/query-service/model/v3" - "go.signoz.io/signoz/pkg/query-service/rules" - "go.uber.org/multierr" - "go.uber.org/zap" -) - -var Version = "0.45-alerts-to-v4" - -var mapTimeAggregation = map[v3.AggregateOperator]v3.TimeAggregation{ - v3.AggregateOperatorSum: v3.TimeAggregationSum, - v3.AggregateOperatorMin: v3.TimeAggregationMin, - v3.AggregateOperatorMax: v3.TimeAggregationMax, - v3.AggregateOperatorSumRate: v3.TimeAggregationRate, - v3.AggregateOperatorAvgRate: v3.TimeAggregationRate, - v3.AggregateOperatorMinRate: v3.TimeAggregationRate, - v3.AggregateOperatorMaxRate: v3.TimeAggregationRate, - v3.AggregateOperatorHistQuant50: v3.TimeAggregationUnspecified, - v3.AggregateOperatorHistQuant75: v3.TimeAggregationUnspecified, - v3.AggregateOperatorHistQuant90: v3.TimeAggregationUnspecified, - v3.AggregateOperatorHistQuant95: v3.TimeAggregationUnspecified, - v3.AggregateOperatorHistQuant99: v3.TimeAggregationUnspecified, -} - -var mapSpaceAggregation = map[v3.AggregateOperator]v3.SpaceAggregation{ - v3.AggregateOperatorSum: v3.SpaceAggregationSum, - v3.AggregateOperatorMin: v3.SpaceAggregationMin, - v3.AggregateOperatorMax: v3.SpaceAggregationMax, - v3.AggregateOperatorSumRate: v3.SpaceAggregationSum, - v3.AggregateOperatorAvgRate: v3.SpaceAggregationAvg, - v3.AggregateOperatorMinRate: v3.SpaceAggregationMin, - v3.AggregateOperatorMaxRate: v3.SpaceAggregationMax, - v3.AggregateOperatorHistQuant50: v3.SpaceAggregationPercentile50, - v3.AggregateOperatorHistQuant75: v3.SpaceAggregationPercentile75, - v3.AggregateOperatorHistQuant90: v3.SpaceAggregationPercentile90, - v3.AggregateOperatorHistQuant95: v3.SpaceAggregationPercentile95, - v3.AggregateOperatorHistQuant99: v3.SpaceAggregationPercentile99, -} - -func canMigrateOperator(operator v3.AggregateOperator) bool { - switch operator { - case v3.AggregateOperatorSum, - v3.AggregateOperatorMin, - v3.AggregateOperatorMax, - v3.AggregateOperatorSumRate, - v3.AggregateOperatorAvgRate, - v3.AggregateOperatorMinRate, - v3.AggregateOperatorMaxRate, - v3.AggregateOperatorHistQuant50, - v3.AggregateOperatorHistQuant75, - v3.AggregateOperatorHistQuant90, - v3.AggregateOperatorHistQuant95, - v3.AggregateOperatorHistQuant99: - return true - } - return false -} - -func Migrate(conn *sqlx.DB) error { - ruleDB := rules.NewRuleDB(conn) - storedRules, err := ruleDB.GetStoredRules(context.Background()) - if err != nil { - return err - } - - for _, storedRule := range storedRules { - parsedRule, errs := rules.ParsePostableRule([]byte(storedRule.Data)) - if len(errs) > 0 { - // this should not happen but if it does, we should not stop the migration - zap.L().Error("Error parsing rule", zap.Error(multierr.Combine(errs...)), zap.Int("rule", storedRule.Id)) - continue - } - zap.L().Info("Rule parsed", zap.Int("rule", storedRule.Id)) - updated := false - if parsedRule.RuleCondition != nil && parsedRule.Version == "" { - if parsedRule.RuleCondition.QueryType() == v3.QueryTypeBuilder { - // check if all the queries can be converted to v4 - canMigrate := true - for _, query := range parsedRule.RuleCondition.CompositeQuery.BuilderQueries { - if query.DataSource == v3.DataSourceMetrics && query.Expression == query.QueryName { - if !canMigrateOperator(query.AggregateOperator) { - canMigrate = false - break - } - } - } - - if canMigrate { - parsedRule.Version = "v4" - for _, query := range parsedRule.RuleCondition.CompositeQuery.BuilderQueries { - if query.DataSource == v3.DataSourceMetrics && query.Expression == query.QueryName { - // update aggregate attribute - if query.AggregateOperator == v3.AggregateOperatorSum || - query.AggregateOperator == v3.AggregateOperatorMin || - query.AggregateOperator == v3.AggregateOperatorMax { - query.AggregateAttribute.Type = "Gauge" - } - if query.AggregateOperator == v3.AggregateOperatorSumRate || - query.AggregateOperator == v3.AggregateOperatorAvgRate || - query.AggregateOperator == v3.AggregateOperatorMinRate || - query.AggregateOperator == v3.AggregateOperatorMaxRate { - query.AggregateAttribute.Type = "Sum" - } - - if query.AggregateOperator == v3.AggregateOperatorHistQuant50 || - query.AggregateOperator == v3.AggregateOperatorHistQuant75 || - query.AggregateOperator == v3.AggregateOperatorHistQuant90 || - query.AggregateOperator == v3.AggregateOperatorHistQuant95 || - query.AggregateOperator == v3.AggregateOperatorHistQuant99 { - query.AggregateAttribute.Type = "Histogram" - } - query.AggregateAttribute.DataType = v3.AttributeKeyDataTypeFloat64 - query.AggregateAttribute.IsColumn = true - query.TimeAggregation = mapTimeAggregation[query.AggregateOperator] - query.SpaceAggregation = mapSpaceAggregation[query.AggregateOperator] - query.AggregateOperator = v3.AggregateOperator(query.TimeAggregation) - updated = true - } - } - } - } - } - - if !updated { - zap.L().Info("Rule not updated", zap.Int("rule", storedRule.Id)) - continue - } - - ruleJSON, jsonErr := json.Marshal(parsedRule) - if jsonErr != nil { - zap.L().Error("Error marshalling rule; skipping rule migration", zap.Error(jsonErr), zap.Int("rule", storedRule.Id)) - continue - } - - stmt, prepareError := conn.PrepareContext(context.Background(), `UPDATE rules SET data=$3 WHERE id=$4;`) - if prepareError != nil { - zap.L().Error("Error in preparing statement for UPDATE to rules", zap.Error(prepareError)) - continue - } - defer stmt.Close() - - if _, err := stmt.Exec(ruleJSON, storedRule.Id); err != nil { - zap.L().Error("Error in Executing prepared statement for UPDATE to rules", zap.Error(err)) - } - } - return nil -} diff --git a/pkg/query-service/migrate/0_47_alerts_custom_step/run.go b/pkg/query-service/migrate/0_47_alerts_custom_step/run.go deleted file mode 100644 index b25705aaf2..0000000000 --- a/pkg/query-service/migrate/0_47_alerts_custom_step/run.go +++ /dev/null @@ -1,70 +0,0 @@ -package alertscustomstep - -import ( - "context" - "encoding/json" - "time" - - "github.com/jmoiron/sqlx" - v3 "go.signoz.io/signoz/pkg/query-service/model/v3" - "go.signoz.io/signoz/pkg/query-service/rules" - "go.uber.org/multierr" - "go.uber.org/zap" -) - -var Version = "0.47-alerts-custom-step" - -func Migrate(conn *sqlx.DB) error { - ruleDB := rules.NewRuleDB(conn) - storedRules, err := ruleDB.GetStoredRules(context.Background()) - if err != nil { - return err - } - - for _, storedRule := range storedRules { - parsedRule, errs := rules.ParsePostableRule([]byte(storedRule.Data)) - if len(errs) > 0 { - // this should not happen but if it does, we should not stop the migration - zap.L().Error("Error parsing rule", zap.Error(multierr.Combine(errs...)), zap.Int("rule", storedRule.Id)) - continue - } - zap.L().Info("Rule parsed", zap.Int("rule", storedRule.Id)) - updated := false - if parsedRule.RuleCondition != nil { - if parsedRule.RuleCondition.QueryType() == v3.QueryTypeBuilder { - if parsedRule.EvalWindow <= rules.Duration(6*time.Hour) { - for _, query := range parsedRule.RuleCondition.CompositeQuery.BuilderQueries { - if query.StepInterval > 60 { - updated = true - zap.L().Info("Updating step interval", zap.Int("rule", storedRule.Id), zap.Int64("old", query.StepInterval), zap.Int64("new", 60)) - query.StepInterval = 60 - } - } - } - } - } - - if !updated { - zap.L().Info("Rule not updated", zap.Int("rule", storedRule.Id)) - continue - } - - ruleJSON, jsonErr := json.Marshal(parsedRule) - if jsonErr != nil { - zap.L().Error("Error marshalling rule; skipping rule migration", zap.Error(jsonErr), zap.Int("rule", storedRule.Id)) - continue - } - - stmt, prepareError := conn.PrepareContext(context.Background(), `UPDATE rules SET data=$3 WHERE id=$4;`) - if prepareError != nil { - zap.L().Error("Error in preparing statement for UPDATE to rules", zap.Error(prepareError)) - continue - } - defer stmt.Close() - - if _, err := stmt.Exec(ruleJSON, storedRule.Id); err != nil { - zap.L().Error("Error in Executing prepared statement for UPDATE to rules", zap.Error(err)) - } - } - return nil -} diff --git a/pkg/query-service/migrate/migate.go b/pkg/query-service/migrate/migate.go index bf5f3a7738..60e65d6d72 100644 --- a/pkg/query-service/migrate/migate.go +++ b/pkg/query-service/migrate/migate.go @@ -7,9 +7,6 @@ import ( "github.com/ClickHouse/clickhouse-go/v2/lib/driver" "github.com/jmoiron/sqlx" - alertstov4 "go.signoz.io/signoz/pkg/query-service/migrate/0_45_alerts_to_v4" - alertscustomstep "go.signoz.io/signoz/pkg/query-service/migrate/0_47_alerts_custom_step" - "go.uber.org/zap" ) type DataMigration struct { @@ -56,28 +53,6 @@ func Migrate(dsn string) error { return err } - if m, err := getMigrationVersion(conn, "0.45_alerts_to_v4"); err == nil && m == nil { - if err := alertstov4.Migrate(conn); err != nil { - zap.L().Error("failed to migrate 0.45_alerts_to_v4", zap.Error(err)) - } else { - _, err := conn.Exec("INSERT INTO data_migrations (version, succeeded) VALUES ('0.45_alerts_to_v4', true)") - if err != nil { - return err - } - } - } - - if m, err := getMigrationVersion(conn, "0.47_alerts_custom_step"); err == nil && m == nil { - if err := alertscustomstep.Migrate(conn); err != nil { - zap.L().Error("failed to migrate 0.47_alerts_custom_step", zap.Error(err)) - } else { - _, err := conn.Exec("INSERT INTO data_migrations (version, succeeded) VALUES ('0.47_alerts_custom_step', true)") - if err != nil { - return err - } - } - } - return nil } diff --git a/pkg/query-service/model/response.go b/pkg/query-service/model/response.go index eb979642da..83df872175 100644 --- a/pkg/query-service/model/response.go +++ b/pkg/query-service/model/response.go @@ -634,20 +634,22 @@ type TagsInfo struct { } type AlertsInfo struct { - TotalAlerts int `json:"totalAlerts"` - LogsBasedAlerts int `json:"logsBasedAlerts"` - MetricBasedAlerts int `json:"metricBasedAlerts"` - TracesBasedAlerts int `json:"tracesBasedAlerts"` - SlackChannels int `json:"slackChannels"` - WebHookChannels int `json:"webHookChannels"` - PagerDutyChannels int `json:"pagerDutyChannels"` - OpsGenieChannels int `json:"opsGenieChannels"` - EmailChannels int `json:"emailChannels"` - MSTeamsChannels int `json:"microsoftTeamsChannels"` - MetricsBuilderQueries int `json:"metricsBuilderQueries"` - MetricsClickHouseQueries int `json:"metricsClickHouseQueries"` - MetricsPrometheusQueries int `json:"metricsPrometheusQueries"` - SpanMetricsPrometheusQueries int `json:"spanMetricsPrometheusQueries"` + TotalAlerts int `json:"totalAlerts"` + LogsBasedAlerts int `json:"logsBasedAlerts"` + MetricBasedAlerts int `json:"metricBasedAlerts"` + TracesBasedAlerts int `json:"tracesBasedAlerts"` + SlackChannels int `json:"slackChannels"` + WebHookChannels int `json:"webHookChannels"` + PagerDutyChannels int `json:"pagerDutyChannels"` + OpsGenieChannels int `json:"opsGenieChannels"` + EmailChannels int `json:"emailChannels"` + MSTeamsChannels int `json:"microsoftTeamsChannels"` + MetricsBuilderQueries int `json:"metricsBuilderQueries"` + MetricsClickHouseQueries int `json:"metricsClickHouseQueries"` + MetricsPrometheusQueries int `json:"metricsPrometheusQueries"` + SpanMetricsPrometheusQueries int `json:"spanMetricsPrometheusQueries"` + AlertNames []string `json:"alertNames"` + AlertsWithTSV2 int `json:"alertsWithTSv2"` } type SavedViewsInfo struct { @@ -657,11 +659,13 @@ type SavedViewsInfo struct { } type DashboardsInfo struct { - TotalDashboards int `json:"totalDashboards"` - TotalDashboardsWithPanelAndName int `json:"totalDashboardsWithPanelAndName"` // dashboards with panel and name without sample title - LogsBasedPanels int `json:"logsBasedPanels"` - MetricBasedPanels int `json:"metricBasedPanels"` - TracesBasedPanels int `json:"tracesBasedPanels"` + TotalDashboards int `json:"totalDashboards"` + TotalDashboardsWithPanelAndName int `json:"totalDashboardsWithPanelAndName"` // dashboards with panel and name without sample title + LogsBasedPanels int `json:"logsBasedPanels"` + MetricBasedPanels int `json:"metricBasedPanels"` + TracesBasedPanels int `json:"tracesBasedPanels"` + DashboardNames []string `json:"dashboardNames"` + QueriesWithTSV2 int `json:"queriesWithTSV2"` } type TagTelemetryData struct { diff --git a/pkg/query-service/model/v3/v3.go b/pkg/query-service/model/v3/v3.go index 6f7881a336..b0e786a6d6 100644 --- a/pkg/query-service/model/v3/v3.go +++ b/pkg/query-service/model/v3/v3.go @@ -1181,6 +1181,11 @@ func (l LabelsString) String() string { return string(l) } +type RuleStateTimeline struct { + Items []RuleStateHistory `json:"items"` + Total uint64 `json:"total"` +} + type RuleStateHistory struct { RuleID string `json:"ruleID" ch:"rule_id"` RuleName string `json:"ruleName" ch:"rule_name"` @@ -1194,11 +1199,15 @@ type RuleStateHistory struct { Labels LabelsString `json:"labels" ch:"labels"` Fingerprint uint64 `json:"fingerprint" ch:"fingerprint"` Value float64 `json:"value" ch:"value"` + + RelatedTracesLink string `json:"relatedTracesLink"` + RelatedLogsLink string `json:"relatedLogsLink"` } type QueryRuleStateHistory struct { Start int64 `json:"start"` End int64 `json:"end"` + State string `json:"state"` Filters *FilterSet `json:"filters"` Offset int64 `json:"offset"` Limit int64 `json:"limit"` @@ -1219,9 +1228,11 @@ func (r *QueryRuleStateHistory) Validate() error { } type RuleStateHistoryContributor struct { - Fingerprint uint64 `json:"fingerprint" ch:"fingerprint"` - Labels LabelsString `json:"labels" ch:"labels"` - Count uint64 `json:"count" ch:"count"` + Fingerprint uint64 `json:"fingerprint" ch:"fingerprint"` + Labels LabelsString `json:"labels" ch:"labels"` + Count uint64 `json:"count" ch:"count"` + RelatedTracesLink string `json:"relatedTracesLink"` + RelatedLogsLink string `json:"relatedLogsLink"` } type RuleStateTransition struct { @@ -1255,3 +1266,25 @@ type QueryProgress struct { ElapsedMs uint64 `json:"elapsed_ms"` } + +type URLShareableTimeRange struct { + Start int64 `json:"start"` + End int64 `json:"end"` + PageSize int64 `json:"pageSize"` +} + +type URLShareableBuilderQuery struct { + QueryData []BuilderQuery `json:"queryData"` + QueryFormulas []string `json:"queryFormulas"` +} + +type URLShareableCompositeQuery struct { + QueryType string `json:"queryType"` + Builder URLShareableBuilderQuery `json:"builder"` +} + +type URLShareableOptions struct { + MaxLines int `json:"maxLines"` + Format string `json:"format"` + SelectColumns []AttributeKey `json:"selectColumns"` +} diff --git a/pkg/query-service/postprocess/formula.go b/pkg/query-service/postprocess/formula.go index 6c800b47c4..4d928a6a48 100644 --- a/pkg/query-service/postprocess/formula.go +++ b/pkg/query-service/postprocess/formula.go @@ -146,6 +146,10 @@ func joinAndCalculate( return nil, fmt.Errorf("expected float64, got %T", newValue) } + if math.IsNaN(val) || math.IsInf(val, 0) { + continue + } + resultSeries.Points = append(resultSeries.Points, v3.Point{ Timestamp: timestamp, Value: val, diff --git a/pkg/query-service/postprocess/formula_test.go b/pkg/query-service/postprocess/formula_test.go index d80519b105..22fc9f61db 100644 --- a/pkg/query-service/postprocess/formula_test.go +++ b/pkg/query-service/postprocess/formula_test.go @@ -1,7 +1,6 @@ package postprocess import ( - "math" "reflect" "testing" @@ -204,9 +203,10 @@ func TestFindUniqueLabelSets(t *testing.T) { func TestProcessResults(t *testing.T) { tests := []struct { - name string - results []*v3.Result - want *v3.Result + name string + results []*v3.Result + want *v3.Result + expression string }{ { name: "test1", @@ -288,12 +288,68 @@ func TestProcessResults(t *testing.T) { }, }, }, + expression: "A + B", + }, + { + name: "test2", + results: []*v3.Result{ + { + QueryName: "A", + Series: []*v3.Series{ + { + Labels: map[string]string{}, + Points: []v3.Point{ + { + Timestamp: 1, + Value: 10, + }, + { + Timestamp: 2, + Value: 0, + }, + }, + }, + }, + }, + { + QueryName: "B", + Series: []*v3.Series{ + { + Labels: map[string]string{}, + Points: []v3.Point{ + { + Timestamp: 1, + Value: 0, + }, + { + Timestamp: 3, + Value: 10, + }, + }, + }, + }, + }, + }, + want: &v3.Result{ + Series: []*v3.Series{ + { + Labels: map[string]string{}, + Points: []v3.Point{ + { + Timestamp: 3, + Value: 0, + }, + }, + }, + }, + }, + expression: "A/B", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - expression, err := govaluate.NewEvaluableExpression("A + B") + expression, err := govaluate.NewEvaluableExpression(tt.expression) if err != nil { t.Errorf("Error parsing expression: %v", err) } @@ -835,10 +891,6 @@ func TestFormula(t *testing.T) { Timestamp: 5, Value: 1, }, - { - Timestamp: 7, - Value: math.Inf(1), - }, }, }, { @@ -855,10 +907,6 @@ func TestFormula(t *testing.T) { Timestamp: 2, Value: 0.6923076923076923, }, - { - Timestamp: 3, - Value: math.Inf(1), - }, { Timestamp: 4, Value: 1, @@ -997,62 +1045,6 @@ func TestFormula(t *testing.T) { }, want: &v3.Result{ Series: []*v3.Series{ - { - Labels: map[string]string{ - "host_name": "ip-10-420-69-1", - "state": "running", - }, - Points: []v3.Point{ - { - Timestamp: 1, - Value: math.Inf(0), - }, - { - Timestamp: 2, - Value: math.Inf(0), - }, - { - Timestamp: 4, - Value: math.Inf(0), - }, - { - Timestamp: 5, - Value: math.Inf(0), - }, - { - Timestamp: 7, - Value: math.Inf(0), - }, - }, - }, - { - Labels: map[string]string{ - "host_name": "ip-10-420-69-2", - "state": "idle", - }, - Points: []v3.Point{ - { - Timestamp: 1, - Value: math.Inf(0), - }, - { - Timestamp: 2, - Value: math.Inf(0), - }, - { - Timestamp: 3, - Value: math.Inf(0), - }, - { - Timestamp: 4, - Value: math.Inf(0), - }, - { - Timestamp: 5, - Value: math.Inf(0), - }, - }, - }, { Labels: map[string]string{ "host_name": "ip-10-420-69-1", @@ -1262,39 +1254,6 @@ func TestFormula(t *testing.T) { Timestamp: 5, Value: 1, }, - { - Timestamp: 7, - Value: math.Inf(1), - }, - }, - }, - { - Labels: map[string]string{ - "host_name": "ip-10-420-69-2", - "state": "idle", - "os.type": "linux", - }, - Points: []v3.Point{ - { - Timestamp: 1, - Value: math.Inf(0), - }, - { - Timestamp: 2, - Value: math.Inf(0), - }, - { - Timestamp: 3, - Value: math.Inf(0), - }, - { - Timestamp: 4, - Value: math.Inf(0), - }, - { - Timestamp: 5, - Value: math.Inf(0), - }, }, }, { @@ -1537,10 +1496,6 @@ func TestFormula(t *testing.T) { Timestamp: 5, Value: 51, }, - { - Timestamp: 7, - Value: math.Inf(1), - }, }, }, { @@ -1558,10 +1513,6 @@ func TestFormula(t *testing.T) { Timestamp: 2, Value: 45.6923076923076923, }, - { - Timestamp: 3, - Value: math.Inf(1), - }, { Timestamp: 4, Value: 41, diff --git a/pkg/query-service/rules/alerting.go b/pkg/query-service/rules/alerting.go index e5660e0dfe..7c7fb40ed6 100644 --- a/pkg/query-service/rules/alerting.go +++ b/pkg/query-service/rules/alerting.go @@ -61,12 +61,43 @@ func (s AlertState) String() string { panic(errors.Errorf("unknown alert state: %d", s)) } +func (s AlertState) MarshalJSON() ([]byte, error) { + return json.Marshal(s.String()) +} + +func (s *AlertState) UnmarshalJSON(b []byte) error { + var v interface{} + if err := json.Unmarshal(b, &v); err != nil { + return err + } + switch value := v.(type) { + case string: + switch value { + case "inactive": + *s = StateInactive + case "pending": + *s = StatePending + case "firing": + *s = StateFiring + case "disabled": + *s = StateDisabled + default: + return errors.New("invalid alert state") + } + return nil + default: + return errors.New("invalid alert state") + } +} + type Alert struct { State AlertState Labels labels.BaseLabels Annotations labels.BaseLabels + QueryResultLables labels.BaseLabels + GeneratorURL string // list of preferred receivers, e.g. slack diff --git a/pkg/query-service/rules/api_params.go b/pkg/query-service/rules/api_params.go index af7e9378f6..890d464671 100644 --- a/pkg/query-service/rules/api_params.go +++ b/pkg/query-service/rules/api_params.go @@ -16,6 +16,22 @@ import ( yaml "gopkg.in/yaml.v2" ) +type AlertType string + +const ( + AlertTypeMetric AlertType = "METRIC_BASED_ALERT" + AlertTypeTraces AlertType = "TRACES_BASED_ALERT" + AlertTypeLogs AlertType = "LOGS_BASED_ALERT" + AlertTypeExceptions AlertType = "EXCEPTIONS_BASED_ALERT" +) + +type RuleDataKind string + +const ( + RuleDataKindJson RuleDataKind = "json" + RuleDataKindYaml RuleDataKind = "yaml" +) + // this file contains api request and responses to be // served over http @@ -31,12 +47,12 @@ func newApiErrorBadData(err error) *model.ApiError { // PostableRule is used to create alerting rule from HTTP api type PostableRule struct { - AlertName string `yaml:"alert,omitempty" json:"alert,omitempty"` - AlertType string `yaml:"alertType,omitempty" json:"alertType,omitempty"` - Description string `yaml:"description,omitempty" json:"description,omitempty"` - RuleType RuleType `yaml:"ruleType,omitempty" json:"ruleType,omitempty"` - EvalWindow Duration `yaml:"evalWindow,omitempty" json:"evalWindow,omitempty"` - Frequency Duration `yaml:"frequency,omitempty" json:"frequency,omitempty"` + AlertName string `yaml:"alert,omitempty" json:"alert,omitempty"` + AlertType AlertType `yaml:"alertType,omitempty" json:"alertType,omitempty"` + Description string `yaml:"description,omitempty" json:"description,omitempty"` + RuleType RuleType `yaml:"ruleType,omitempty" json:"ruleType,omitempty"` + EvalWindow Duration `yaml:"evalWindow,omitempty" json:"evalWindow,omitempty"` + Frequency Duration `yaml:"frequency,omitempty" json:"frequency,omitempty"` RuleCondition *RuleCondition `yaml:"condition,omitempty" json:"condition,omitempty"` Labels map[string]string `yaml:"labels,omitempty" json:"labels,omitempty"` @@ -234,33 +250,11 @@ type GettableRules struct { // GettableRule has info for an alerting rules. type GettableRule struct { - Id string `json:"id"` - State string `json:"state"` + Id string `json:"id"` + State AlertState `json:"state"` PostableRule CreatedAt *time.Time `json:"createAt"` CreatedBy *string `json:"createBy"` UpdatedAt *time.Time `json:"updateAt"` UpdatedBy *string `json:"updateBy"` } - -type timeRange struct { - Start int64 `json:"start"` - End int64 `json:"end"` - PageSize int64 `json:"pageSize"` -} - -type builderQuery struct { - QueryData []v3.BuilderQuery `json:"queryData"` - QueryFormulas []string `json:"queryFormulas"` -} - -type urlShareableCompositeQuery struct { - QueryType string `json:"queryType"` - Builder builderQuery `json:"builder"` -} - -type Options struct { - MaxLines int `json:"maxLines"` - Format string `json:"format"` - SelectColumns []v3.AttributeKey `json:"selectColumns"` -} diff --git a/pkg/query-service/rules/db.go b/pkg/query-service/rules/db.go index 64a35e3eb9..d9a9be195c 100644 --- a/pkg/query-service/rules/db.go +++ b/pkg/query-service/rules/db.go @@ -308,6 +308,7 @@ func (r *ruleDB) GetAlertsInfo(ctx context.Context) (*model.AlertsInfo, error) { // fetch alerts from rules db query := "SELECT data FROM rules" var alertsData []string + var alertNames []string err := r.Select(&alertsData, query) if err != nil { zap.L().Error("Error in processing sql query", zap.Error(err)) @@ -315,14 +316,18 @@ func (r *ruleDB) GetAlertsInfo(ctx context.Context) (*model.AlertsInfo, error) { } for _, alert := range alertsData { var rule GettableRule + if strings.Contains(alert, "time_series_v2") { + alertsInfo.AlertsWithTSV2 = alertsInfo.AlertsWithTSV2 + 1 + } err = json.Unmarshal([]byte(alert), &rule) if err != nil { zap.L().Error("invalid rule data", zap.Error(err)) continue } - if rule.AlertType == "LOGS_BASED_ALERT" { + alertNames = append(alertNames, rule.AlertName) + if rule.AlertType == AlertTypeLogs { alertsInfo.LogsBasedAlerts = alertsInfo.LogsBasedAlerts + 1 - } else if rule.AlertType == "METRIC_BASED_ALERT" { + } else if rule.AlertType == AlertTypeMetric { alertsInfo.MetricBasedAlerts = alertsInfo.MetricBasedAlerts + 1 if rule.RuleCondition != nil && rule.RuleCondition.CompositeQuery != nil { if rule.RuleCondition.CompositeQuery.QueryType == v3.QueryTypeBuilder { @@ -338,11 +343,11 @@ func (r *ruleDB) GetAlertsInfo(ctx context.Context) (*model.AlertsInfo, error) { } } } - } else if rule.AlertType == "TRACES_BASED_ALERT" { + } else if rule.AlertType == AlertTypeTraces { alertsInfo.TracesBasedAlerts = alertsInfo.TracesBasedAlerts + 1 } alertsInfo.TotalAlerts = alertsInfo.TotalAlerts + 1 } - + alertsInfo.AlertNames = alertNames return &alertsInfo, nil } diff --git a/pkg/query-service/rules/manager.go b/pkg/query-service/rules/manager.go index 40764f9fb0..f738fefcc7 100644 --- a/pkg/query-service/rules/manager.go +++ b/pkg/query-service/rules/manager.go @@ -20,11 +20,9 @@ import ( "github.com/jmoiron/sqlx" - // opentracing "github.com/opentracing/opentracing-go" am "go.signoz.io/signoz/pkg/query-service/integrations/alertManager" "go.signoz.io/signoz/pkg/query-service/interfaces" "go.signoz.io/signoz/pkg/query-service/model" - v3 "go.signoz.io/signoz/pkg/query-service/model/v3" "go.signoz.io/signoz/pkg/query-service/telemetry" "go.signoz.io/signoz/pkg/query-service/utils/labels" ) @@ -240,20 +238,6 @@ func (m *Manager) EditRule(ctx context.Context, ruleStr string, id string) error parsedRule, errs := ParsePostableRule([]byte(ruleStr)) - currentRule, err := m.GetRule(ctx, id) - if err != nil { - zap.L().Error("failed to get the rule from rule db", zap.String("id", id), zap.Error(err)) - return err - } - - if !checkIfTraceOrLogQB(¤tRule.PostableRule) { - // check if the new rule uses any feature that is not enabled - err = m.checkFeatureUsage(parsedRule) - if err != nil { - return err - } - } - if len(errs) > 0 { zap.L().Error("failed to parse rules", zap.Errors("errors", errs)) // just one rule is being parsed so expect just one error @@ -272,20 +256,6 @@ func (m *Manager) EditRule(ctx context.Context, ruleStr string, id string) error } } - // update feature usage if the current rule is not a trace or log query builder - if !checkIfTraceOrLogQB(¤tRule.PostableRule) { - err = m.updateFeatureUsage(parsedRule, 1) - if err != nil { - zap.L().Error("error updating feature usage", zap.Error(err)) - } - // update feature usage if the new rule is not a trace or log query builder and the current rule is - } else if !checkIfTraceOrLogQB(parsedRule) { - err = m.updateFeatureUsage(¤tRule.PostableRule, -1) - if err != nil { - zap.L().Error("error updating feature usage", zap.Error(err)) - } - } - return nil } @@ -335,13 +305,6 @@ func (m *Manager) DeleteRule(ctx context.Context, id string) error { return fmt.Errorf("delete rule received an rule id in invalid format, must be a number") } - // update feature usage - rule, err := m.GetRule(ctx, id) - if err != nil { - zap.L().Error("failed to get the rule from rule db", zap.String("id", id), zap.Error(err)) - return err - } - taskName := prepareTaskName(int64(idInt)) if !m.opts.DisableRules { m.deleteTask(taskName) @@ -352,11 +315,6 @@ func (m *Manager) DeleteRule(ctx context.Context, id string) error { return err } - err = m.updateFeatureUsage(&rule.PostableRule, -1) - if err != nil { - zap.L().Error("error updating feature usage", zap.Error(err)) - } - return nil } @@ -381,12 +339,6 @@ func (m *Manager) deleteTask(taskName string) { func (m *Manager) CreateRule(ctx context.Context, ruleStr string) (*GettableRule, error) { parsedRule, errs := ParsePostableRule([]byte(ruleStr)) - // check if the rule uses any feature that is not enabled - err := m.checkFeatureUsage(parsedRule) - if err != nil { - return nil, err - } - if len(errs) > 0 { zap.L().Error("failed to parse rules", zap.Errors("errors", errs)) // just one rule is being parsed so expect just one error @@ -409,11 +361,6 @@ func (m *Manager) CreateRule(ctx context.Context, ruleStr string) (*GettableRule return nil, err } - // update feature usage - err = m.updateFeatureUsage(parsedRule, 1) - if err != nil { - zap.L().Error("error updating feature usage", zap.Error(err)) - } gettableRule := &GettableRule{ Id: fmt.Sprintf("%d", lastInsertId), PostableRule: *parsedRule, @@ -421,59 +368,6 @@ func (m *Manager) CreateRule(ctx context.Context, ruleStr string) (*GettableRule return gettableRule, nil } -func (m *Manager) updateFeatureUsage(parsedRule *PostableRule, usage int64) error { - isTraceOrLogQB := checkIfTraceOrLogQB(parsedRule) - if isTraceOrLogQB { - feature, err := m.featureFlags.GetFeatureFlag(model.QueryBuilderAlerts) - if err != nil { - return err - } - feature.Usage += usage - if feature.Usage == feature.UsageLimit && feature.UsageLimit != -1 { - feature.Active = false - } - if feature.Usage < feature.UsageLimit || feature.UsageLimit == -1 { - feature.Active = true - } - err = m.featureFlags.UpdateFeatureFlag(feature) - if err != nil { - return err - } - } - return nil -} - -func (m *Manager) checkFeatureUsage(parsedRule *PostableRule) error { - isTraceOrLogQB := checkIfTraceOrLogQB(parsedRule) - if isTraceOrLogQB { - err := m.featureFlags.CheckFeature(model.QueryBuilderAlerts) - if err != nil { - switch err.(type) { - case model.ErrFeatureUnavailable: - zap.L().Error("feature unavailable", zap.String("featureKey", model.QueryBuilderAlerts), zap.Error(err)) - return model.BadRequest(err) - default: - zap.L().Error("feature check failed", zap.String("featureKey", model.QueryBuilderAlerts), zap.Error(err)) - return model.BadRequest(err) - } - } - } - return nil -} - -func checkIfTraceOrLogQB(parsedRule *PostableRule) bool { - if parsedRule != nil { - if parsedRule.RuleCondition.QueryType() == v3.QueryTypeBuilder { - for _, query := range parsedRule.RuleCondition.CompositeQuery.BuilderQueries { - if query.DataSource == v3.DataSourceTraces || query.DataSource == v3.DataSourceLogs { - return true - } - } - } - } - return false -} - func (m *Manager) addTask(rule *PostableRule, taskName string) error { m.mtx.Lock() defer m.mtx.Unlock() @@ -569,7 +463,7 @@ func (m *Manager) prepareTask(acquireLock bool, r *PostableRule, taskName string m.rules[ruleId] = pr } else { - return nil, fmt.Errorf(fmt.Sprintf("unsupported rule type. Supported types: %s, %s", RuleTypeProm, RuleTypeThreshold)) + return nil, fmt.Errorf("unsupported rule type. Supported types: %s, %s", RuleTypeProm, RuleTypeThreshold) } return task, nil @@ -710,10 +604,10 @@ func (m *Manager) ListRuleStates(ctx context.Context) (*GettableRules, error) { // fetch state of rule from memory if rm, ok := m.rules[ruleResponse.Id]; !ok { - ruleResponse.State = StateDisabled.String() + ruleResponse.State = StateDisabled ruleResponse.Disabled = true } else { - ruleResponse.State = rm.State().String() + ruleResponse.State = rm.State() } ruleResponse.CreatedAt = s.CreatedAt ruleResponse.CreatedBy = s.CreatedBy @@ -737,10 +631,10 @@ func (m *Manager) GetRule(ctx context.Context, id string) (*GettableRule, error) r.Id = fmt.Sprintf("%d", s.Id) // fetch state of rule from memory if rm, ok := m.rules[r.Id]; !ok { - r.State = StateDisabled.String() + r.State = StateDisabled r.Disabled = true } else { - r.State = rm.State().String() + r.State = rm.State() } r.CreatedAt = s.CreatedAt r.CreatedBy = s.CreatedBy @@ -846,10 +740,10 @@ func (m *Manager) PatchRule(ctx context.Context, ruleStr string, ruleId string) // fetch state of rule from memory if rm, ok := m.rules[ruleId]; !ok { - response.State = StateDisabled.String() + response.State = StateDisabled response.Disabled = true } else { - response.State = rm.State().String() + response.State = rm.State() } return &response, nil diff --git a/pkg/query-service/rules/prom_rule.go b/pkg/query-service/rules/prom_rule.go index 235a5cf825..06f9ae311d 100644 --- a/pkg/query-service/rules/prom_rule.go +++ b/pkg/query-service/rules/prom_rule.go @@ -411,6 +411,7 @@ func (r *PromRule) Eval(ctx context.Context, ts time.Time, queriers *Queriers) ( } lb := plabels.NewBuilder(alertSmpl.Metric).Del(plabels.MetricName) + resultLabels := plabels.NewBuilder(alertSmpl.Metric).Del(plabels.MetricName).Labels() for _, l := range r.labels { lb.Set(l.Name, expand(l.Value)) @@ -439,13 +440,14 @@ func (r *PromRule) Eval(ctx context.Context, ts time.Time, queriers *Queriers) ( } alerts[h] = &Alert{ - Labels: lbs, - Annotations: annotations, - ActiveAt: ts, - State: StatePending, - Value: alertSmpl.F, - GeneratorURL: r.GeneratorURL(), - Receivers: r.preferredChannels, + Labels: lbs, + QueryResultLables: resultLabels, + Annotations: annotations, + ActiveAt: ts, + State: StatePending, + Value: alertSmpl.F, + GeneratorURL: r.GeneratorURL(), + Receivers: r.preferredChannels, } } @@ -489,7 +491,7 @@ func (r *PromRule) Eval(ctx context.Context, ts time.Time, queriers *Queriers) ( StateChanged: true, UnixMilli: ts.UnixMilli(), Labels: v3.LabelsString(labelsJSON), - Fingerprint: a.Labels.Hash(), + Fingerprint: a.QueryResultLables.Hash(), }) } continue @@ -509,7 +511,7 @@ func (r *PromRule) Eval(ctx context.Context, ts time.Time, queriers *Queriers) ( StateChanged: true, UnixMilli: ts.UnixMilli(), Labels: v3.LabelsString(labelsJSON), - Fingerprint: a.Labels.Hash(), + Fingerprint: a.QueryResultLables.Hash(), Value: a.Value, }) } diff --git a/pkg/query-service/rules/threshold_rule.go b/pkg/query-service/rules/threshold_rule.go index 81185f9dca..9bdecbc63d 100644 --- a/pkg/query-service/rules/threshold_rule.go +++ b/pkg/query-service/rules/threshold_rule.go @@ -91,8 +91,7 @@ type ThresholdRule struct { lastTimestampWithDatapoints time.Time // Type of the rule - // One of ["LOGS_BASED_ALERT", "TRACES_BASED_ALERT", "METRIC_BASED_ALERT", "EXCEPTIONS_BASED_ALERT"] - typ string + typ AlertType // querier is used for alerts created before the introduction of new metrics query builder querier interfaces.Querier @@ -625,13 +624,13 @@ func (r *ThresholdRule) prepareLinksToLogs(ts time.Time, lbls labels.Labels) str q := r.prepareQueryRange(ts) // Logs list view expects time in milliseconds - tr := timeRange{ + tr := v3.URLShareableTimeRange{ Start: q.Start, End: q.End, PageSize: 100, } - options := Options{ + options := v3.URLShareableOptions{ MaxLines: 2, Format: "list", SelectColumns: []v3.AttributeKey{}, @@ -641,9 +640,9 @@ func (r *ThresholdRule) prepareLinksToLogs(ts time.Time, lbls labels.Labels) str urlEncodedTimeRange := url.QueryEscape(string(period)) filterItems := r.fetchFilters(selectedQuery, lbls) - urlData := urlShareableCompositeQuery{ + urlData := v3.URLShareableCompositeQuery{ QueryType: string(v3.QueryTypeBuilder), - Builder: builderQuery{ + Builder: v3.URLShareableBuilderQuery{ QueryData: []v3.BuilderQuery{ { DataSource: v3.DataSourceLogs, @@ -671,7 +670,7 @@ func (r *ThresholdRule) prepareLinksToLogs(ts time.Time, lbls labels.Labels) str } data, _ := json.Marshal(urlData) - compositeQuery := url.QueryEscape(string(data)) + compositeQuery := url.QueryEscape(url.QueryEscape(string(data))) optionsData, _ := json.Marshal(options) urlEncodedOptions := url.QueryEscape(string(optionsData)) @@ -689,13 +688,13 @@ func (r *ThresholdRule) prepareLinksToTraces(ts time.Time, lbls labels.Labels) s q := r.prepareQueryRange(ts) // Traces list view expects time in nanoseconds - tr := timeRange{ + tr := v3.URLShareableTimeRange{ Start: q.Start * time.Second.Microseconds(), End: q.End * time.Second.Microseconds(), PageSize: 100, } - options := Options{ + options := v3.URLShareableOptions{ MaxLines: 2, Format: "list", SelectColumns: constants.TracesListViewDefaultSelectedColumns, @@ -705,9 +704,9 @@ func (r *ThresholdRule) prepareLinksToTraces(ts time.Time, lbls labels.Labels) s urlEncodedTimeRange := url.QueryEscape(string(period)) filterItems := r.fetchFilters(selectedQuery, lbls) - urlData := urlShareableCompositeQuery{ + urlData := v3.URLShareableCompositeQuery{ QueryType: string(v3.QueryTypeBuilder), - Builder: builderQuery{ + Builder: v3.URLShareableBuilderQuery{ QueryData: []v3.BuilderQuery{ { DataSource: v3.DataSourceTraces, @@ -735,7 +734,7 @@ func (r *ThresholdRule) prepareLinksToTraces(ts time.Time, lbls labels.Labels) s } data, _ := json.Marshal(urlData) - compositeQuery := url.QueryEscape(string(data)) + compositeQuery := url.QueryEscape(url.QueryEscape(string(data))) optionsData, _ := json.Marshal(options) urlEncodedOptions := url.QueryEscape(string(optionsData)) @@ -954,6 +953,7 @@ func (r *ThresholdRule) Eval(ctx context.Context, ts time.Time, queriers *Querie } lb := labels.NewBuilder(smpl.Metric).Del(labels.MetricNameLabel).Del(labels.TemporalityLabel) + resultLabels := labels.NewBuilder(smpl.MetricOrig).Del(labels.MetricNameLabel).Del(labels.TemporalityLabel).Labels() for _, l := range r.labels { lb.Set(l.Name, expand(l.Value)) @@ -974,12 +974,12 @@ func (r *ThresholdRule) Eval(ctx context.Context, ts time.Time, queriers *Querie // Links with timestamps should go in annotations since labels // is used alert grouping, and we want to group alerts with the same // label set, but different timestamps, together. - if r.typ == "TRACES_BASED_ALERT" { + if r.typ == AlertTypeTraces { link := r.prepareLinksToTraces(ts, smpl.MetricOrig) if link != "" && r.hostFromSource() != "" { annotations = append(annotations, labels.Label{Name: "related_traces", Value: fmt.Sprintf("%s/traces-explorer?%s", r.hostFromSource(), link)}) } - } else if r.typ == "LOGS_BASED_ALERT" { + } else if r.typ == AlertTypeLogs { link := r.prepareLinksToLogs(ts, smpl.MetricOrig) if link != "" && r.hostFromSource() != "" { annotations = append(annotations, labels.Label{Name: "related_logs", Value: fmt.Sprintf("%s/logs/logs-explorer?%s", r.hostFromSource(), link)}) @@ -1001,14 +1001,15 @@ func (r *ThresholdRule) Eval(ctx context.Context, ts time.Time, queriers *Querie } alerts[h] = &Alert{ - Labels: lbs, - Annotations: annotations, - ActiveAt: ts, - State: StatePending, - Value: smpl.V, - GeneratorURL: r.GeneratorURL(), - Receivers: r.preferredChannels, - Missing: smpl.IsMissing, + Labels: lbs, + QueryResultLables: resultLabels, + Annotations: annotations, + ActiveAt: ts, + State: StatePending, + Value: smpl.V, + GeneratorURL: r.GeneratorURL(), + Receivers: r.preferredChannels, + Missing: smpl.IsMissing, } } @@ -1034,7 +1035,7 @@ func (r *ThresholdRule) Eval(ctx context.Context, ts time.Time, queriers *Querie // Check if any pending alerts should be removed or fire now. Write out alert timeseries. for fp, a := range r.active { - labelsJSON, err := json.Marshal(a.Labels) + labelsJSON, err := json.Marshal(a.QueryResultLables) if err != nil { zap.L().Error("error marshaling labels", zap.Error(err), zap.Any("labels", a.Labels)) } @@ -1054,7 +1055,7 @@ func (r *ThresholdRule) Eval(ctx context.Context, ts time.Time, queriers *Querie StateChanged: true, UnixMilli: ts.UnixMilli(), Labels: v3.LabelsString(labelsJSON), - Fingerprint: a.Labels.Hash(), + Fingerprint: a.QueryResultLables.Hash(), }) } continue @@ -1074,7 +1075,7 @@ func (r *ThresholdRule) Eval(ctx context.Context, ts time.Time, queriers *Querie StateChanged: true, UnixMilli: ts.UnixMilli(), Labels: v3.LabelsString(labelsJSON), - Fingerprint: a.Labels.Hash(), + Fingerprint: a.QueryResultLables.Hash(), Value: a.Value, }) } diff --git a/pkg/query-service/rules/threshold_rule_test.go b/pkg/query-service/rules/threshold_rule_test.go index 55a831efcd..05bd613900 100644 --- a/pkg/query-service/rules/threshold_rule_test.go +++ b/pkg/query-service/rules/threshold_rule_test.go @@ -674,7 +674,7 @@ func TestNormalizeLabelName(t *testing.T) { func TestPrepareLinksToLogs(t *testing.T) { postableRule := PostableRule{ AlertName: "Tricky Condition Tests", - AlertType: "LOGS_BASED_ALERT", + AlertType: AlertTypeLogs, RuleType: RuleTypeThreshold, EvalWindow: Duration(5 * time.Minute), Frequency: Duration(1 * time.Minute), diff --git a/pkg/query-service/telemetry/telemetry.go b/pkg/query-service/telemetry/telemetry.go index 2c9dceb910..c916135f4e 100644 --- a/pkg/query-service/telemetry/telemetry.go +++ b/pkg/query-service/telemetry/telemetry.go @@ -314,10 +314,14 @@ func createTelemetry() { dashboardsAlertsData := map[string]interface{}{ "totalDashboards": dashboardsInfo.TotalDashboards, "totalDashboardsWithPanelAndName": dashboardsInfo.TotalDashboardsWithPanelAndName, + "dashboardNames": dashboardsInfo.DashboardNames, + "alertNames": alertsInfo.AlertNames, "logsBasedPanels": dashboardsInfo.LogsBasedPanels, "metricBasedPanels": dashboardsInfo.MetricBasedPanels, "tracesBasedPanels": dashboardsInfo.TracesBasedPanels, + "dashboardsWithTSV2": dashboardsInfo.QueriesWithTSV2, "totalAlerts": alertsInfo.TotalAlerts, + "alertsWithTSV2": alertsInfo.AlertsWithTSV2, "logsBasedAlerts": alertsInfo.LogsBasedAlerts, "metricBasedAlerts": alertsInfo.MetricBasedAlerts, "tracesBasedAlerts": alertsInfo.TracesBasedAlerts, diff --git a/pkg/query-service/tests/test-deploy/clickhouse-config.xml b/pkg/query-service/tests/test-deploy/clickhouse-config.xml index dd2b1bdf5b..4e8dc00b30 100644 --- a/pkg/query-service/tests/test-deploy/clickhouse-config.xml +++ b/pkg/query-service/tests/test-deploy/clickhouse-config.xml @@ -649,12 +649,12 @@ See https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/replication/#creating-replicated-tables --> - + diff --git a/pkg/query-service/tests/test-deploy/docker-compose.yaml b/pkg/query-service/tests/test-deploy/docker-compose.yaml index 4ca72424f3..4c75c8ac43 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.102.4} + image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.7} 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.102.4 + image: signoz/signoz-otel-collector:0.102.7 container_name: signoz-otel-collector command: [ diff --git a/pkg/query-service/utils/format.go b/pkg/query-service/utils/format.go index bd7db15e6b..4de081940d 100644 --- a/pkg/query-service/utils/format.go +++ b/pkg/query-service/utils/format.go @@ -190,6 +190,19 @@ func ClickHouseFormattedValue(v interface{}) string { zap.L().Error("invalid type for formatted value", zap.Any("type", reflect.TypeOf(x[0]))) return "[]" } + case []string: + if len(x) == 0 { + return "[]" + } + str := "[" + for idx, sVal := range x { + str += fmt.Sprintf("'%s'", QuoteEscapedString(sVal)) + if idx != len(x)-1 { + str += "," + } + } + str += "]" + return str default: zap.L().Error("invalid type for formatted value", zap.Any("type", reflect.TypeOf(x))) return "" diff --git a/pkg/registry/doc.go b/pkg/registry/doc.go new file mode 100644 index 0000000000..ff2debbefe --- /dev/null +++ b/pkg/registry/doc.go @@ -0,0 +1,3 @@ +// package registry contains a simple implementation of https://github.com/google/guava/wiki/ServiceExplained +// Here the the "ServiceManager" is called the "Registry" +package registry diff --git a/pkg/registry/registry.go b/pkg/registry/registry.go new file mode 100644 index 0000000000..850a4389a9 --- /dev/null +++ b/pkg/registry/registry.go @@ -0,0 +1,84 @@ +package registry + +import ( + "context" + "errors" + "fmt" + "os" + "os/signal" + "syscall" + + "go.uber.org/zap" +) + +type Registry struct { + services []NamedService + logger *zap.Logger + startCh chan error + stopCh chan error +} + +// New creates a new registry of services. It needs at least one service in the input. +func New(logger *zap.Logger, services ...NamedService) (*Registry, error) { + if logger == nil { + return nil, fmt.Errorf("cannot build registry, logger is required") + } + + if len(services) == 0 { + return nil, fmt.Errorf("cannot build registry, at least one service is required") + } + + return &Registry{ + logger: logger.Named("go.signoz.io/pkg/registry"), + services: services, + startCh: make(chan error, 1), + stopCh: make(chan error, len(services)), + }, nil +} + +func (r *Registry) Start(ctx context.Context) error { + for _, s := range r.services { + go func(s Service) { + err := s.Start(ctx) + r.startCh <- err + }(s) + } + + return nil +} + +func (r *Registry) Wait(ctx context.Context) error { + interrupt := make(chan os.Signal, 1) + signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM) + + select { + case <-ctx.Done(): + r.logger.Info("caught context error, exiting", zap.Any("context", ctx)) + case s := <-interrupt: + r.logger.Info("caught interrupt signal, exiting", zap.Any("context", ctx), zap.Any("signal", s)) + case err := <-r.startCh: + r.logger.Info("caught service error, exiting", zap.Any("context", ctx), zap.Error(err)) + return err + } + + return nil +} + +func (r *Registry) Stop(ctx context.Context) error { + for _, s := range r.services { + go func(s Service) { + err := s.Stop(ctx) + r.stopCh <- err + }(s) + } + + errs := make([]error, len(r.services)) + for i := 0; i < len(r.services); i++ { + err := <-r.stopCh + if err != nil { + errs = append(errs, err) + } + } + + return errors.Join(errs...) +} diff --git a/pkg/registry/registry_test.go b/pkg/registry/registry_test.go new file mode 100644 index 0000000000..12ae1d8862 --- /dev/null +++ b/pkg/registry/registry_test.go @@ -0,0 +1,56 @@ +package registry + +import ( + "context" + "sync" + "testing" + + "github.com/stretchr/testify/require" + "go.uber.org/zap" +) + +func TestRegistryWith2HttpServers(t *testing.T) { + http1, err := newHttpService("http1") + require.NoError(t, err) + + http2, err := newHttpService("http2") + require.NoError(t, err) + + registry, err := New(zap.NewNop(), http1, http2) + require.NoError(t, err) + + ctx, cancel := context.WithCancel(context.Background()) + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + require.NoError(t, registry.Start(ctx)) + require.NoError(t, registry.Wait(ctx)) + require.NoError(t, registry.Stop(ctx)) + }() + cancel() + + wg.Wait() +} + +func TestRegistryWith2HttpServersWithoutWait(t *testing.T) { + http1, err := newHttpService("http1") + require.NoError(t, err) + + http2, err := newHttpService("http2") + require.NoError(t, err) + + registry, err := New(zap.NewNop(), http1, http2) + require.NoError(t, err) + + ctx := context.Background() + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + require.NoError(t, registry.Start(ctx)) + require.NoError(t, registry.Stop(ctx)) + }() + + wg.Wait() +} diff --git a/pkg/registry/service.go b/pkg/registry/service.go new file mode 100644 index 0000000000..38df4f0a4f --- /dev/null +++ b/pkg/registry/service.go @@ -0,0 +1,16 @@ +package registry + +import "context" + +type Service interface { + // Starts a service. The service should return an error if it cannot be started. + Start(context.Context) error + // Stops a service. + Stop(context.Context) error +} + +type NamedService interface { + // Identifier of a service. It should be unique across all services. + Name() string + Service +} diff --git a/pkg/registry/service_test.go b/pkg/registry/service_test.go new file mode 100644 index 0000000000..dc0621e962 --- /dev/null +++ b/pkg/registry/service_test.go @@ -0,0 +1,49 @@ +package registry + +import ( + "context" + "net" + "net/http" +) + +var _ NamedService = (*httpService)(nil) + +type httpService struct { + Listener net.Listener + Server *http.Server + name string +} + +func newHttpService(name string) (*httpService, error) { + return &httpService{ + name: name, + Server: &http.Server{}, + }, nil +} + +func (service *httpService) Name() string { + return service.name +} + +func (service *httpService) Start(ctx context.Context) error { + listener, err := net.Listen("tcp", "localhost:0") + if err != nil { + return err + } + service.Listener = listener + + if err := service.Server.Serve(service.Listener); err != nil { + if err != http.ErrServerClosed { + return err + } + } + return nil +} + +func (service *httpService) Stop(ctx context.Context) error { + if err := service.Server.Shutdown(ctx); err != nil { + return err + } + + return nil +} diff --git a/pkg/version/doc.go b/pkg/version/doc.go new file mode 100644 index 0000000000..9b6483a085 --- /dev/null +++ b/pkg/version/doc.go @@ -0,0 +1,4 @@ +// Package version is used to track the build information of the application. +// This is typically set via ldflags at build time. +// Eg: -ldflags="-X 'pkg/version.Build.Version=v1.0.0'" +package version diff --git a/pkg/version/version.go b/pkg/version/version.go new file mode 100644 index 0000000000..9cbd8c4436 --- /dev/null +++ b/pkg/version/version.go @@ -0,0 +1,9 @@ +package version + +// Build contains information about the build environment. +type Build struct { + // The name of the current build. + Name string + // The version of the current build. + Version string +} diff --git a/pkg/web/config.go b/pkg/web/config.go new file mode 100644 index 0000000000..a0e0c531de --- /dev/null +++ b/pkg/web/config.go @@ -0,0 +1,29 @@ +package web + +import ( + "go.signoz.io/signoz/pkg/confmap" +) + +// Config satisfies the confmap.Config interface +var _ confmap.Config = (*Config)(nil) + +// Config holds the configuration for web. +type Config struct { + // The prefix to serve the files from + Prefix string `mapstructure:"prefix"` + // The directory containing the static build files. The root of this directory should + // have an index.html file. + Directory string `mapstructure:"directory"` +} + +func (c *Config) NewWithDefaults() confmap.Config { + return &Config{ + Prefix: "/", + Directory: "/etc/signoz/web", + } + +} + +func (c *Config) Validate() error { + return nil +} diff --git a/pkg/web/testdata/index.html b/pkg/web/testdata/index.html new file mode 100644 index 0000000000..49c8c8383a --- /dev/null +++ b/pkg/web/testdata/index.html @@ -0,0 +1 @@ +

Welcome to test data!!!

\ No newline at end of file diff --git a/pkg/web/web.go b/pkg/web/web.go new file mode 100644 index 0000000000..2e29b000f9 --- /dev/null +++ b/pkg/web/web.go @@ -0,0 +1,94 @@ +package web + +import ( + "fmt" + "net/http" + "os" + "path/filepath" + "time" + + "github.com/gorilla/mux" + "go.signoz.io/signoz/pkg/http/middleware" + "go.uber.org/zap" +) + +var _ http.Handler = (*Web)(nil) + +const ( + indexFileName string = "index.html" +) + +type Web struct { + logger *zap.Logger + cfg Config +} + +func New(logger *zap.Logger, cfg Config) (*Web, error) { + if logger == nil { + return nil, fmt.Errorf("cannot build web, logger is required") + } + + fi, err := os.Stat(cfg.Directory) + if err != nil { + return nil, fmt.Errorf("cannot access web directory: %w", err) + } + + ok := fi.IsDir() + if !ok { + return nil, fmt.Errorf("web directory is not a directory") + } + + fi, err = os.Stat(filepath.Join(cfg.Directory, indexFileName)) + if err != nil { + return nil, fmt.Errorf("cannot access %q in web directory: %w", indexFileName, err) + } + + if os.IsNotExist(err) || fi.IsDir() { + return nil, fmt.Errorf("%q does not exist", indexFileName) + } + + return &Web{ + logger: logger.Named("go.signoz.io/pkg/web"), + cfg: cfg, + }, nil +} + +func (web *Web) AddToRouter(router *mux.Router) error { + cache := middleware.NewCache(7 * 24 * time.Hour) + err := router.PathPrefix(web.cfg.Prefix). + Handler( + http.StripPrefix( + web.cfg.Prefix, + cache.Wrap(http.HandlerFunc(web.ServeHTTP)), + ), + ).GetError() + if err != nil { + return fmt.Errorf("unable to add web to router: %w", err) + } + + return nil +} + +func (web *Web) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + // Join internally call path.Clean to prevent directory traversal + path := filepath.Join(web.cfg.Directory, req.URL.Path) + + // check whether a file exists or is a directory at the given path + fi, err := os.Stat(path) + if os.IsNotExist(err) || fi.IsDir() { + // file does not exist or path is a directory, serve index.html + http.ServeFile(rw, req, filepath.Join(web.cfg.Directory, indexFileName)) + return + } + + if err != nil { + // if we got an error (that wasn't that the file doesn't exist) stating the + // file, return a 500 internal server error and stop + // TODO: Put down a crash html page here + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + // otherwise, use http.FileServer to serve the static file + http.FileServer(http.Dir(web.cfg.Directory)).ServeHTTP(rw, req) +} diff --git a/pkg/web/web_test.go b/pkg/web/web_test.go new file mode 100644 index 0000000000..d5111cf747 --- /dev/null +++ b/pkg/web/web_test.go @@ -0,0 +1,159 @@ +package web + +import ( + "io" + "net" + "net/http" + "os" + "path/filepath" + "testing" + + "github.com/gorilla/mux" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/zap" +) + +func TestServeHttpWithoutPrefix(t *testing.T) { + t.Parallel() + fi, err := os.Open(filepath.Join("testdata", indexFileName)) + require.NoError(t, err) + + expected, err := io.ReadAll(fi) + require.NoError(t, err) + + web, err := New(zap.NewNop(), Config{Prefix: "/", Directory: filepath.Join("testdata")}) + require.NoError(t, err) + + router := mux.NewRouter() + err = web.AddToRouter(router) + require.NoError(t, err) + + listener, err := net.Listen("tcp", "localhost:0") + require.NoError(t, err) + + server := &http.Server{ + Handler: router, + } + + go func() { + _ = server.Serve(listener) + }() + defer func() { + _ = server.Close() + }() + + testCases := []struct { + name string + path string + }{ + { + name: "Root", + path: "/", + }, + { + name: "Index", + path: "/" + indexFileName, + }, + { + name: "DoesNotExist", + path: "/does-not-exist", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + res, err := http.DefaultClient.Get("http://" + listener.Addr().String() + tc.path) + require.NoError(t, err) + + defer func() { + _ = res.Body.Close() + }() + + actual, err := io.ReadAll(res.Body) + require.NoError(t, err) + + assert.Equal(t, expected, actual) + }) + } + +} + +func TestServeHttpWithPrefix(t *testing.T) { + t.Parallel() + fi, err := os.Open(filepath.Join("testdata", indexFileName)) + require.NoError(t, err) + + expected, err := io.ReadAll(fi) + require.NoError(t, err) + + web, err := New(zap.NewNop(), Config{Prefix: "/web", Directory: filepath.Join("testdata")}) + require.NoError(t, err) + + router := mux.NewRouter() + err = web.AddToRouter(router) + require.NoError(t, err) + + listener, err := net.Listen("tcp", "localhost:0") + require.NoError(t, err) + + server := &http.Server{ + Handler: router, + } + + go func() { + _ = server.Serve(listener) + }() + defer func() { + _ = server.Close() + }() + + testCases := []struct { + name string + path string + found bool + }{ + { + name: "Root", + path: "/web", + found: true, + }, + { + name: "Index", + path: "/web/" + indexFileName, + found: true, + }, + { + name: "FileDoesNotExist", + path: "/web/does-not-exist", + found: true, + }, + { + name: "DoesNotExist", + path: "/does-not-exist", + found: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + res, err := http.DefaultClient.Get("http://" + listener.Addr().String() + tc.path) + require.NoError(t, err) + + defer func() { + _ = res.Body.Close() + }() + + if tc.found { + actual, err := io.ReadAll(res.Body) + require.NoError(t, err) + + assert.Equal(t, expected, actual) + } else { + assert.Equal(t, http.StatusNotFound, res.StatusCode) + } + + }) + } + +}