diff --git a/.github/workflows/staging-deployment.yaml b/.github/workflows/staging-deployment.yaml index c655d69df2..6de51f4733 100644 --- a/.github/workflows/staging-deployment.yaml +++ b/.github/workflows/staging-deployment.yaml @@ -11,21 +11,23 @@ jobs: environment: staging steps: - name: Executing remote ssh commands using ssh key - uses: appleboy/ssh-action@v0.1.6 + uses: appleboy/ssh-action@v0.1.8 env: GITHUB_BRANCH: develop GITHUB_SHA: ${{ github.sha }} with: host: ${{ secrets.HOST_DNS }} username: ${{ secrets.USERNAME }} - key: ${{ secrets.EC2_SSH_KEY }} + key: ${{ secrets.SSH_KEY }} envs: GITHUB_BRANCH,GITHUB_SHA command_timeout: 60m script: | echo "GITHUB_BRANCH: ${GITHUB_BRANCH}" echo "GITHUB_SHA: ${GITHUB_SHA}" export DOCKER_TAG="${GITHUB_SHA:0:7}" # needed for child process to access it + export OTELCOL_TAG="main" docker system prune --force + docker pull signoz/signoz-otel-collector:main cd ~/signoz git status git add . diff --git a/.github/workflows/testing-deployment.yaml b/.github/workflows/testing-deployment.yaml index d122291d42..d65a4e8bbc 100644 --- a/.github/workflows/testing-deployment.yaml +++ b/.github/workflows/testing-deployment.yaml @@ -11,14 +11,14 @@ jobs: if: ${{ github.event.label.name == 'testing-deploy' }} steps: - name: Executing remote ssh commands using ssh key - uses: appleboy/ssh-action@v0.1.6 + uses: appleboy/ssh-action@v0.1.8 env: GITHUB_BRANCH: ${{ github.head_ref || github.ref_name }} GITHUB_SHA: ${{ github.sha }} with: host: ${{ secrets.HOST_DNS }} username: ${{ secrets.USERNAME }} - key: ${{ secrets.EC2_SSH_KEY }} + key: ${{ secrets.SSH_KEY }} envs: GITHUB_BRANCH,GITHUB_SHA command_timeout: 60m script: | diff --git a/README.md b/README.md index 324b8e2d69..70779f3de5 100644 --- a/README.md +++ b/README.md @@ -35,14 +35,37 @@ SigNoz helps developers monitor applications and troubleshoot problems in their 👉 Filter and query logs, build dashboards and alerts based on attributes in logs -![screenzy-1670570187181](https://user-images.githubusercontent.com/504541/206646629-829fdafe-70e2-4503-a9c4-1301b7918586.png) -
-![screenzy-1670570193901](https://user-images.githubusercontent.com/504541/206646676-a676fdeb-331c-4847-aea9-d1cabf7c47e1.png) -
-![screenzy-1670570199026](https://user-images.githubusercontent.com/504541/206646754-28c5534f-0377-428c-9c6e-5c7c0d9dd22d.png) -
-![screenzy-1670569888865](https://user-images.githubusercontent.com/504541/206645819-1e865a56-71b4-4fde-80cc-fbdb137a4da5.png) +👉 Record exceptions automatically in Python, Java, Ruby, and Javascript +👉 Easy to set alerts with DIY query builder + + +### Application Metrics + +![application_metrics](https://user-images.githubusercontent.com/83692067/226637410-900dbc5e-6705-4b11-a10c-bd0faeb2a92f.png) + + +### Distributed Tracing +distributed_tracing_2 2 + +distributed_tracing_1 + +### Logs Management + +logs_management + +### Infrastructure Monitoring + +infrastructure_monitoring + +### Exceptions Monitoring + +![exceptions_light](https://user-images.githubusercontent.com/83692067/226637967-4188d024-3ac9-4799-be95-f5ea9c45436f.png) + + +### Alerts + +alerts_management

@@ -65,6 +88,10 @@ Come say Hi to us on [Slack](https://signoz.io/slack) 👋 - See exact request trace to figure out issues in downstream services, slow DB queries, call to 3rd party services like payment gateways, etc - Filter traces by service name, operation, latency, error, tags/annotations. - Run aggregates on trace data (events/spans) to get business relevant metrics. e.g. You can get error rate and 99th percentile latency of `customer_type: gold` or `deployment_version: v2` or `external_call: paypal` +- Native support for OpenTelemetry Logs, advanced log query builder, and automatic log collection from k8s cluster +- Lightening quick log analytics ([Logs Perf. Benchmark](https://signoz.io/blog/logs-performance-benchmark/)) +- End-to-End visibility into infrastructure performance, ingest metrics from all kinds of host environments +- Easy to set alerts with DIY query builder

diff --git a/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml b/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml index a0c2ba8105..ffa2a10f71 100644 --- a/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml +++ b/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml @@ -137,7 +137,7 @@ services: condition: on-failure query-service: - image: signoz/query-service:0.17.0 + image: signoz/query-service:0.18.0 command: ["-config=/root/config/prometheus.yml"] # ports: # - "6060:6060" # pprof port @@ -166,7 +166,7 @@ services: <<: *clickhouse-depend frontend: - image: signoz/frontend:0.17.0 + image: signoz/frontend:0.18.0 deploy: restart_policy: condition: on-failure @@ -179,7 +179,7 @@ services: - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf otel-collector: - image: signoz/signoz-otel-collector:0.66.6 + image: signoz/signoz-otel-collector:0.66.7 command: ["--config=/etc/otel-collector-config.yaml"] user: root # required for reading docker container logs volumes: @@ -208,7 +208,7 @@ services: <<: *clickhouse-depend otel-collector-metrics: - image: signoz/signoz-otel-collector:0.66.6 + image: signoz/signoz-otel-collector:0.66.7 command: ["--config=/etc/otel-collector-metrics-config.yaml"] volumes: - ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml diff --git a/deploy/docker/clickhouse-setup/docker-compose-core.yaml b/deploy/docker/clickhouse-setup/docker-compose-core.yaml index e7c9360879..80bd4a9890 100644 --- a/deploy/docker/clickhouse-setup/docker-compose-core.yaml +++ b/deploy/docker/clickhouse-setup/docker-compose-core.yaml @@ -41,7 +41,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: otel-collector - image: signoz/signoz-otel-collector:0.66.6 + image: signoz/signoz-otel-collector:0.66.7 command: ["--config=/etc/otel-collector-config.yaml"] # user: root # required for reading docker container logs volumes: @@ -67,7 +67,7 @@ services: otel-collector-metrics: container_name: otel-collector-metrics - image: signoz/signoz-otel-collector:0.66.6 + image: signoz/signoz-otel-collector:0.66.7 command: ["--config=/etc/otel-collector-metrics-config.yaml"] volumes: - ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml diff --git a/deploy/docker/clickhouse-setup/docker-compose.yaml b/deploy/docker/clickhouse-setup/docker-compose.yaml index 4906ba4c10..a4d23311e7 100644 --- a/deploy/docker/clickhouse-setup/docker-compose.yaml +++ b/deploy/docker/clickhouse-setup/docker-compose.yaml @@ -153,7 +153,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.17.0} + image: signoz/query-service:${DOCKER_TAG:-0.18.0} container_name: query-service command: ["-config=/root/config/prometheus.yml"] # ports: @@ -181,7 +181,7 @@ services: <<: *clickhouse-depend frontend: - image: signoz/frontend:${DOCKER_TAG:-0.17.0} + image: signoz/frontend:${DOCKER_TAG:-0.18.0} container_name: frontend restart: on-failure depends_on: @@ -193,7 +193,7 @@ services: - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf otel-collector: - image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.66.6} + image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.66.7} command: ["--config=/etc/otel-collector-config.yaml"] user: root # required for reading docker container logs volumes: @@ -219,7 +219,7 @@ services: <<: *clickhouse-depend otel-collector-metrics: - image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.66.6} + image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.66.7} command: ["--config=/etc/otel-collector-metrics-config.yaml"] volumes: - ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml diff --git a/ee/query-service/app/server.go b/ee/query-service/app/server.go index af4a90f885..72091cc1e3 100644 --- a/ee/query-service/app/server.go +++ b/ee/query-service/app/server.go @@ -25,16 +25,18 @@ import ( licensepkg "go.signoz.io/signoz/ee/query-service/license" "go.signoz.io/signoz/ee/query-service/usage" + "go.signoz.io/signoz/pkg/query-service/agentConf" baseapp "go.signoz.io/signoz/pkg/query-service/app" "go.signoz.io/signoz/pkg/query-service/app/dashboards" - "go.signoz.io/signoz/pkg/query-service/app/explorer" + baseexplorer "go.signoz.io/signoz/pkg/query-service/app/explorer" + "go.signoz.io/signoz/pkg/query-service/app/opamp" + opAmpModel "go.signoz.io/signoz/pkg/query-service/app/opamp/model" baseauth "go.signoz.io/signoz/pkg/query-service/auth" - "go.signoz.io/signoz/pkg/query-service/constants" baseconst "go.signoz.io/signoz/pkg/query-service/constants" "go.signoz.io/signoz/pkg/query-service/healthcheck" basealm "go.signoz.io/signoz/pkg/query-service/integrations/alertManager" baseint "go.signoz.io/signoz/pkg/query-service/interfaces" - "go.signoz.io/signoz/pkg/query-service/model" + basemodel "go.signoz.io/signoz/pkg/query-service/model" pqle "go.signoz.io/signoz/pkg/query-service/pqlEngine" rules "go.signoz.io/signoz/pkg/query-service/rules" "go.signoz.io/signoz/pkg/query-service/telemetry" @@ -42,6 +44,8 @@ import ( "go.uber.org/zap" ) +const AppDbEngine = "sqlite" + type ServerOptions struct { PromConfigPath string HTTPHostPort string @@ -85,8 +89,9 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) { return nil, err } + baseexplorer.InitWithDSN(baseconst.RELATIONAL_DATASOURCE_PATH) + localDB, err := dashboards.InitDB(baseconst.RELATIONAL_DATASOURCE_PATH) - explorer.InitWithDSN(constants.RELATIONAL_DATASOURCE_PATH) if err != nil { return nil, err @@ -127,6 +132,17 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) { return nil, err } + // initiate opamp + _, err = opAmpModel.InitDB(baseconst.RELATIONAL_DATASOURCE_PATH) + if err != nil { + return nil, err + } + + // initiate agent config handler + if err := agentConf.Initiate(localDB, AppDbEngine); err != nil { + return nil, err + } + // start the usagemanager usageManager, err := usage.New("sqlite", localDB, lm.GetRepo(), reader.GetConn()) if err != nil { @@ -208,7 +224,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e r := mux.NewRouter() - getUserFromRequest := func(r *http.Request) (*model.UserPayload, error) { + getUserFromRequest := func(r *http.Request) (*basemodel.UserPayload, error) { patToken := r.Header.Get("SIGNOZ-API-KEY") if len(patToken) > 0 { zap.S().Debugf("Received a non-zero length PAT token") @@ -299,7 +315,7 @@ func extractDashboardMetaData(path string, r *http.Request) (map[string]interfac pathToExtractBodyFrom := "/api/v2/metrics/query_range" data := map[string]interface{}{} - var postData *model.QueryRangeParamsV2 + var postData *basemodel.QueryRangeParamsV2 if path == pathToExtractBodyFrom && (r.Method == "POST") { if r.Body != nil { @@ -472,7 +488,7 @@ func (s *Server) Start() error { if port, err := utils.GetPort(s.privateConn.Addr()); err == nil { privatePort = port } - fmt.Println("starting private http") + go func() { zap.S().Info("Starting Private HTTP server", zap.Int("port", privatePort), zap.String("addr", s.serverOptions.PrivateHostPort)) @@ -488,6 +504,37 @@ func (s *Server) Start() error { }() + go func() { + zap.S().Info("Starting OpAmp Websocket server", zap.String("addr", baseconst.OpAmpWsEndpoint)) + err := opamp.InitalizeServer(baseconst.OpAmpWsEndpoint, &opAmpModel.AllAgents) + if err != nil { + zap.S().Info("opamp ws server failed to start", err) + s.unavailableChannel <- healthcheck.Unavailable + } + }() + + return nil +} + +func (s *Server) Stop() error { + if s.httpServer != nil { + if err := s.httpServer.Shutdown(context.Background()); err != nil { + return err + } + } + + if s.privateHTTP != nil { + if err := s.privateHTTP.Shutdown(context.Background()); err != nil { + return err + } + } + + opamp.StopServer() + + if s.ruleManager != nil { + s.ruleManager.Stop() + } + return nil } diff --git a/frontend/src/AppRoutes/index.tsx b/frontend/src/AppRoutes/index.tsx index a5641c35da..edfe843882 100644 --- a/frontend/src/AppRoutes/index.tsx +++ b/frontend/src/AppRoutes/index.tsx @@ -4,7 +4,9 @@ import Spinner from 'components/Spinner'; import AppLayout from 'container/AppLayout'; import { useThemeConfig } from 'hooks/useDarkMode'; import { NotificationProvider } from 'hooks/useNotifications'; +import { ResourceProvider } from 'hooks/useResourceAttribute'; import history from 'lib/history'; +import { QueryBuilderProvider } from 'providers/QueryBuilder'; import React, { Suspense } from 'react'; import { Route, Router, Switch } from 'react-router-dom'; @@ -16,28 +18,32 @@ function App(): JSX.Element { return ( - - + + - - }> - - {routes.map(({ path, component, exact }) => ( - - ))} + + + + }> + + {routes.map(({ path, component, exact }) => ( + + ))} - - - - + + + + + + - - + + ); } diff --git a/frontend/src/api/errors/getAll.ts b/frontend/src/api/errors/getAll.ts index 7014e52a56..8d6793ee87 100644 --- a/frontend/src/api/errors/getAll.ts +++ b/frontend/src/api/errors/getAll.ts @@ -1,7 +1,6 @@ import axios from 'api'; import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; import { AxiosError } from 'axios'; -import createQueryParams from 'lib/createQueryParams'; import { ErrorResponse, SuccessResponse } from 'types/api'; import { PayloadProps, Props } from 'types/api/errors/getAll'; @@ -9,11 +8,17 @@ const getAll = async ( props: Props, ): Promise | ErrorResponse> => { try { - const response = await axios.get( - `/listErrors?${createQueryParams({ - ...props, - })}`, - ); + const response = await axios.post(`/listErrors`, { + start: `${props.start}`, + end: `${props.end}`, + order: props.order, + orderParam: props.orderParam, + limit: props.limit, + offset: props.offset, + exceptionType: props.exceptionType, + serviceName: props.serviceName, + tags: props.tags, + }); return { statusCode: 200, diff --git a/frontend/src/api/errors/getErrorCounts.ts b/frontend/src/api/errors/getErrorCounts.ts index 4992a6d391..977eeb226f 100644 --- a/frontend/src/api/errors/getErrorCounts.ts +++ b/frontend/src/api/errors/getErrorCounts.ts @@ -1,7 +1,6 @@ import axios from 'api'; import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; import { AxiosError } from 'axios'; -import createQueryParams from 'lib/createQueryParams'; import { ErrorResponse, SuccessResponse } from 'types/api'; import { PayloadProps, Props } from 'types/api/errors/getErrorCounts'; @@ -9,11 +8,13 @@ const getErrorCounts = async ( props: Props, ): Promise | ErrorResponse> => { try { - const response = await axios.get( - `/countErrors?${createQueryParams({ - ...props, - })}`, - ); + const response = await axios.post(`/countErrors`, { + start: `${props.start}`, + end: `${props.end}`, + exceptionType: props.exceptionType, + serviceName: props.serviceName, + tags: props.tags, + }); return { statusCode: 200, diff --git a/frontend/src/api/trace/getSpans.ts b/frontend/src/api/trace/getSpans.ts index 8b56caa46d..261b2652c6 100644 --- a/frontend/src/api/trace/getSpans.ts +++ b/frontend/src/api/trace/getSpans.ts @@ -10,7 +10,7 @@ const getSpans = async ( ): Promise | ErrorResponse> => { try { const updatedSelectedTags = props.selectedTags.map((e) => ({ - Key: e.Key, + Key: `${e.Key}.(string)`, Operator: e.Operator, StringValues: e.StringValues, NumberValues: e.NumberValues, diff --git a/frontend/src/api/trace/getSpansAggregate.ts b/frontend/src/api/trace/getSpansAggregate.ts index cfa1f7e31f..7f245605fc 100644 --- a/frontend/src/api/trace/getSpansAggregate.ts +++ b/frontend/src/api/trace/getSpansAggregate.ts @@ -28,7 +28,7 @@ const getSpanAggregate = async ( }); const updatedSelectedTags = props.selectedTags.map((e) => ({ - Key: e.Key, + Key: `${e.Key}.(string)`, Operator: e.Operator, StringValues: e.StringValues, NumberValues: e.NumberValues, diff --git a/frontend/src/components/Editor/index.tsx b/frontend/src/components/Editor/index.tsx index a58df3778e..e24fab3c80 100644 --- a/frontend/src/components/Editor/index.tsx +++ b/frontend/src/components/Editor/index.tsx @@ -1,6 +1,6 @@ import MEditor, { EditorProps } from '@monaco-editor/react'; import { useIsDarkMode } from 'hooks/useDarkMode'; -import React from 'react'; +import React, { useMemo } from 'react'; function Editor({ value, @@ -11,16 +11,24 @@ function Editor({ options, }: MEditorProps): JSX.Element { const isDarkMode = useIsDarkMode(); + + const onChangeHandler = (newValue?: string): void => { + if (typeof newValue === 'string' && onChange) onChange(newValue); + }; + + const editorOptions = useMemo( + () => ({ fontSize: 16, automaticLayout: true, readOnly, ...options }), + [options, readOnly], + ); + return ( { - if (typeof newValue === 'string') onChange(newValue); - }} + onChange={onChangeHandler} /> ); } @@ -28,7 +36,7 @@ function Editor({ interface MEditorProps { value: string; language?: string; - onChange: (value: string) => void; + onChange?: (value: string) => void; readOnly?: boolean; height?: string; options?: EditorProps['options']; @@ -39,6 +47,7 @@ Editor.defaultProps = { readOnly: false, height: '40vh', options: {}, + onChange: (): void => {}, }; export default Editor; diff --git a/frontend/src/components/RouteTab/index.tsx b/frontend/src/components/RouteTab/index.tsx index 1192f2f8e2..0059f5b84c 100644 --- a/frontend/src/components/RouteTab/index.tsx +++ b/frontend/src/components/RouteTab/index.tsx @@ -2,8 +2,6 @@ import { Tabs, TabsProps } from 'antd'; import history from 'lib/history'; import React from 'react'; -const { TabPane } = Tabs; - function RouteTab({ routes, activeKey, @@ -22,29 +20,23 @@ function RouteTab({ } }; + const items = routes.map(({ Component, name, route }) => ({ + label: name, + key: name, + tabKey: route, + children: , + })); + return ( - {routes.map( - ({ Component, name, route }): JSX.Element => ( - - - - ), - )} - + /> ); } diff --git a/frontend/src/components/TextToolTip/index.tsx b/frontend/src/components/TextToolTip/index.tsx index 7e6c04c6f1..64a9cd053a 100644 --- a/frontend/src/components/TextToolTip/index.tsx +++ b/frontend/src/components/TextToolTip/index.tsx @@ -1,23 +1,40 @@ -/* eslint-disable react/no-unstable-nested-components */ +import { grey } from '@ant-design/colors'; import { QuestionCircleFilled } from '@ant-design/icons'; import { Tooltip } from 'antd'; -import React from 'react'; +import { themeColors } from 'constants/theme'; +import { useIsDarkMode } from 'hooks/useDarkMode'; +import React, { useMemo } from 'react'; + +import { style } from './styles'; function TextToolTip({ text, url }: TextToolTipProps): JSX.Element { + const isDarkMode = useIsDarkMode(); + + const overlay = useMemo( + () => ( +
+ {`${text} `} + {url && ( + + here + + )} +
+ ), + [text, url], + ); + + const iconStyle = useMemo( + () => ({ + ...style, + color: isDarkMode ? themeColors.whiteCream : grey[0], + }), + [isDarkMode], + ); + return ( - ( -
- {`${text} `} - {url && ( - - here - - )} -
- )} - > - + + ); } diff --git a/frontend/src/components/TextToolTip/styles.ts b/frontend/src/components/TextToolTip/styles.ts new file mode 100644 index 0000000000..bb2532182d --- /dev/null +++ b/frontend/src/components/TextToolTip/styles.ts @@ -0,0 +1 @@ +export const style = { fontSize: '1.3125rem' }; diff --git a/frontend/src/components/TimePreferenceDropDown/index.tsx b/frontend/src/components/TimePreferenceDropDown/index.tsx index 8774edbdc9..3ce9795f15 100644 --- a/frontend/src/components/TimePreferenceDropDown/index.tsx +++ b/frontend/src/components/TimePreferenceDropDown/index.tsx @@ -1,9 +1,9 @@ -import { Button, Dropdown, Menu } from 'antd'; +import { Button, Dropdown } from 'antd'; import TimeItems, { timePreferance, timePreferenceType, } from 'container/NewWidget/RightContainer/timeItems'; -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { menuItems } from './config'; import { TextContainer } from './styles'; @@ -22,11 +22,17 @@ function TimePreference({ [setSelectedTime], ); + const menu = useMemo( + () => ({ + items: menuItems, + onClick: timeMenuItemOnChangeHandler, + }), + [timeMenuItemOnChangeHandler], + ); + return ( - } - > + diff --git a/frontend/src/container/AllError/index.tsx b/frontend/src/container/AllError/index.tsx index 64d83e70ec..c3b0580f44 100644 --- a/frontend/src/container/AllError/index.tsx +++ b/frontend/src/container/AllError/index.tsx @@ -18,6 +18,8 @@ import { ResizeTable } from 'components/ResizeTable'; import ROUTES from 'constants/routes'; import dayjs from 'dayjs'; import { useNotifications } from 'hooks/useNotifications'; +import useResourceAttribute from 'hooks/useResourceAttribute'; +import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils'; import useUrlQuery from 'hooks/useUrlQuery'; import createQueryParams from 'lib/createQueryParams'; import history from 'lib/history'; @@ -93,9 +95,11 @@ function AllErrors(): JSX.Element { ], ); + const { queries } = useResourceAttribute(); + const [{ isLoading, data }, errorCountResponse] = useQueries([ { - queryKey: ['getAllErrors', updatedPath, maxTime, minTime], + queryKey: ['getAllErrors', updatedPath, maxTime, minTime, queries], queryFn: (): Promise | ErrorResponse> => getAll({ end: maxTime, @@ -106,6 +110,7 @@ function AllErrors(): JSX.Element { orderParam: getUpdatedParams, exceptionType: getUpdatedExceptionType, serviceName: getUpdatedServiceName, + tags: convertRawQueriesToTraceSelectedTags(queries), }), enabled: !loading, }, @@ -116,6 +121,7 @@ function AllErrors(): JSX.Element { minTime, getUpdatedExceptionType, getUpdatedServiceName, + queries, ], queryFn: (): Promise> => getErrorCounts({ @@ -123,6 +129,7 @@ function AllErrors(): JSX.Element { start: minTime, exceptionType: getUpdatedExceptionType, serviceName: getUpdatedServiceName, + tags: convertRawQueriesToTraceSelectedTags(queries), }), enabled: !loading, }, diff --git a/frontend/src/container/ConfigDropdown/Config/styles.ts b/frontend/src/container/ConfigDropdown/Config/styles.ts deleted file mode 100644 index 4807ea77c2..0000000000 --- a/frontend/src/container/ConfigDropdown/Config/styles.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Menu } from 'antd'; -import styled from 'styled-components'; - -export const MenuDropdown = styled(Menu)` - &&& { - .ant-dropdown, - .ant-dropdown-menu, - .ant-dropdown-menu-item { - padding: 0px; - } - .ant-menu-item { - height: 1.75rem; - display: flex; - align-items: center; - } - } -`; diff --git a/frontend/src/container/ConfigDropdown/index.tsx b/frontend/src/container/ConfigDropdown/index.tsx index 8390e09167..1ddd676948 100644 --- a/frontend/src/container/ConfigDropdown/index.tsx +++ b/frontend/src/container/ConfigDropdown/index.tsx @@ -13,7 +13,6 @@ import { ConfigProps } from 'types/api/dynamicConfigs/getDynamicConfigs'; import AppReducer from 'types/reducer/app'; import HelpToolTip from './Config'; -import { MenuDropdown } from './Config/styles'; function DynamicConfigDropdown({ frontendId, @@ -34,13 +33,15 @@ function DynamicConfigDropdown({ setIsHelpDropDownOpen(!isHelpDropDownOpen); }; - const menuItems = useMemo( - () => [ - { - key: '1', - label: , - }, - ], + const menu = useMemo( + () => ({ + items: [ + { + key: '1', + label: , + }, + ], + }), [config], ); @@ -53,10 +54,10 @@ function DynamicConfigDropdown({ return ( } - visible={isHelpDropDownOpen} + menu={menu} + open={isHelpDropDownOpen} > { switch (typ) { case AlertTypes.TRACES_BASED_ALERT: @@ -303,14 +320,8 @@ function QuerySection({ )} } - > - - - + items={tabs} + /> ); case AlertTypes.METRICS_BASED_ALERT: default: @@ -330,11 +341,8 @@ function QuerySection({ )} } - > - - - - + items={items} + /> ); } }; diff --git a/frontend/src/container/Header/SignedInAs/index.tsx b/frontend/src/container/Header/SignedIn/index.tsx similarity index 69% rename from frontend/src/container/Header/SignedInAs/index.tsx rename to frontend/src/container/Header/SignedIn/index.tsx index a9fab7507a..b1804ea93a 100644 --- a/frontend/src/container/Header/SignedInAs/index.tsx +++ b/frontend/src/container/Header/SignedIn/index.tsx @@ -1,16 +1,21 @@ import { Avatar, Typography } from 'antd'; import ROUTES from 'constants/routes'; import history from 'lib/history'; -import React from 'react'; +import React, { useCallback } from 'react'; import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; import AppReducer from 'types/reducer/app'; import { AvatarContainer, ManageAccountLink, Wrapper } from '../styles'; -function SignedInAS(): JSX.Element { +function SignedIn({ onToggle }: SignedInProps): JSX.Element { const { user } = useSelector((state) => state.app); + const onManageAccountClick = useCallback(() => { + onToggle(); + history.push(ROUTES.MY_SETTINGS); + }, [onToggle]); + if (!user) { return
; } @@ -30,11 +35,7 @@ function SignedInAS(): JSX.Element { {email}
- { - history.push(ROUTES.MY_SETTINGS); - }} - > + Manage Account @@ -42,4 +43,8 @@ function SignedInAS(): JSX.Element { ); } -export default SignedInAS; +interface SignedInProps { + onToggle: VoidFunction; +} + +export default SignedIn; diff --git a/frontend/src/container/Header/index.tsx b/frontend/src/container/Header/index.tsx index 9f04454d33..a34287e665 100644 --- a/frontend/src/container/Header/index.tsx +++ b/frontend/src/container/Header/index.tsx @@ -3,12 +3,19 @@ import { CaretUpFilled, LogoutOutlined, } from '@ant-design/icons'; -import { Divider, Dropdown, Menu, Space, Typography } from 'antd'; +import type { MenuProps } from 'antd'; +import { Divider, Dropdown, Space, Typography } from 'antd'; import { Logout } from 'api/utils'; import ROUTES from 'constants/routes'; import Config from 'container/ConfigDropdown'; import { useIsDarkMode, useThemeMode } from 'hooks/useDarkMode'; -import React, { Dispatch, SetStateAction, useCallback, useState } from 'react'; +import React, { + Dispatch, + SetStateAction, + useCallback, + useMemo, + useState, +} from 'react'; import { useSelector } from 'react-redux'; import { NavLink } from 'react-router-dom'; import { AppState } from 'store/reducers'; @@ -16,7 +23,7 @@ import AppReducer from 'types/reducer/app'; import CurrentOrganization from './CurrentOrganization'; import ManageLicense from './ManageLicense'; -import SignedInAS from './SignedInAs'; +import SignedIn from './SignedIn'; import { AvatarWrapper, Container, @@ -43,32 +50,45 @@ function HeaderContainer(): JSX.Element { [], ); - const menu = ( - - - - - - - - - - -
{ - if (e.key === 'Enter' || e.key === 'Space') { - Logout(); - } - }} - role="button" - onClick={Logout} - > - Logout -
-
-
-
+ const onLogoutKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === 'Enter' || e.key === 'Space') { + Logout(); + } + }, + [], + ); + + const menu: MenuProps = useMemo( + () => ({ + items: [ + { + key: 'main-menu', + label: ( +
+ + + + + + + + +
+ Logout +
+
+
+ ), + }, + ], + }), + [onToggleHandler, onLogoutKeyDown], ); return ( @@ -98,10 +118,10 @@ function HeaderContainer(): JSX.Element { /> {user?.name[0]} diff --git a/frontend/src/container/Licenses/index.tsx b/frontend/src/container/Licenses/index.tsx index b326a5b0e7..d7dc4ab22b 100644 --- a/frontend/src/container/Licenses/index.tsx +++ b/frontend/src/container/Licenses/index.tsx @@ -8,8 +8,6 @@ import { useQuery } from 'react-query'; import ApplyLicenseForm from './ApplyLicenseForm'; import ListLicenses from './ListLicenses'; -const { TabPane } = Tabs; - function Licenses(): JSX.Element { const { t } = useTranslation(['licenses']); const { data, isError, isLoading, refetch } = useQuery({ @@ -28,17 +26,21 @@ function Licenses(): JSX.Element { const allValidLicense = data?.payload?.filter((license) => license.isCurrent) || []; - return ( - - - - - + const tabs = [ + { + label: t('tab_current_license'), + key: 'licenses', + children: , + }, + { + label: t('tab_license_history'), + key: 'history', + children: , + }, + ]; - - - - + return ( + ); } diff --git a/frontend/src/container/ListOfDashboard/index.tsx b/frontend/src/container/ListOfDashboard/index.tsx index b513df0c6a..ca5d68d9bb 100644 --- a/frontend/src/container/ListOfDashboard/index.tsx +++ b/frontend/src/container/ListOfDashboard/index.tsx @@ -1,5 +1,12 @@ import { PlusOutlined } from '@ant-design/icons'; -import { Card, Dropdown, Menu, Row, TableColumnProps, Typography } from 'antd'; +import { + Card, + Dropdown, + MenuProps, + Row, + TableColumnProps, + Typography, +} from 'antd'; import { ItemType } from 'antd/es/menu/hooks/useItems'; import createDashboard from 'api/dashboard/create'; import { AxiosError } from 'axios'; @@ -47,10 +54,12 @@ function ListOfAllDashboard(): JSX.Element { ); const { t } = useTranslation('dashboard'); + const [ isImportJSONModalVisible, setIsImportJSONModalVisible, ] = useState(false); + const [uploadedGrafana, setUploadedGrafana] = useState(false); const [filteredDashboards, setFilteredDashboards] = useState(); @@ -58,6 +67,7 @@ function ListOfAllDashboard(): JSX.Element { useEffect(() => { setFilteredDashboards(dashboards); }, [dashboards]); + const [newDashboardState, setNewDashboardState] = useState({ loading: false, error: false, @@ -215,7 +225,12 @@ function ListOfAllDashboard(): JSX.Element { return menuItems; }, [createNewDashboard, loading, onNewDashboardHandler, t]); - const menuItems = getMenuItems(); + const menu: MenuProps = useMemo( + () => ({ + items: getMenuItems(), + }), + [getMenuItems], + ); const GetHeader = useMemo( () => ( @@ -230,7 +245,7 @@ function ListOfAllDashboard(): JSX.Element { }} /> {newDashboard && ( - }> + } type="primary" @@ -249,7 +264,7 @@ function ListOfAllDashboard(): JSX.Element { newDashboard, newDashboardState.error, newDashboardState.loading, - menuItems, + menu, ], ); diff --git a/frontend/src/container/LogDetailedView/TableView.tsx b/frontend/src/container/LogDetailedView/TableView.tsx index 92b15d1b57..eea61f2a32 100644 --- a/frontend/src/container/LogDetailedView/TableView.tsx +++ b/frontend/src/container/LogDetailedView/TableView.tsx @@ -1,14 +1,18 @@ import { blue, orange } from '@ant-design/colors'; import { Input } from 'antd'; +import { ColumnsType } from 'antd/es/table'; +import Editor from 'components/Editor'; import AddToQueryHOC from 'components/Logs/AddToQueryHOC'; import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC'; import { ResizeTable } from 'components/ResizeTable'; import flatten from 'flat'; import { fieldSearchFilter } from 'lib/logs/fieldSearch'; +import { isEmpty } from 'lodash-es'; import React, { useMemo, useState } from 'react'; import { ILog } from 'types/api/logs/log'; import ActionItem from './ActionItem'; +import { recursiveParseJSON } from './utils'; // Fields which should be restricted from adding it to query const RESTRICTED_FIELDS = ['timestamp']; @@ -41,10 +45,10 @@ function TableView({ logData }: TableViewProps): JSX.Element | null { return null; } - const columns = [ + const columns: ColumnsType = [ { title: 'Action', - width: 100, + width: 15, render: (fieldData: Record): JSX.Element | null => { const fieldKey = fieldData.field.split('.').slice(-1); if (!RESTRICTED_FIELDS.includes(fieldKey[0])) { @@ -57,7 +61,8 @@ function TableView({ logData }: TableViewProps): JSX.Element | null { title: 'Field', dataIndex: 'field', key: 'field', - width: 100, + width: 30, + ellipsis: true, render: (field: string): JSX.Element => { const fieldKey = field.split('.').slice(-1); const renderedField = {field}; @@ -78,16 +83,36 @@ function TableView({ logData }: TableViewProps): JSX.Element | null { key: 'value', width: 80, ellipsis: false, - render: (field: never): JSX.Element => ( - - {field} - - ), + render: (field, record): JSX.Element => { + if (record.field === 'body') { + const parsedBody = recursiveParseJSON(field); + if (!isEmpty(parsedBody)) { + return ( + + ); + } + } + + return ( + + {field} + + ); + }, }, ]; return ( -
+ <> setFieldSearchInput(e.target.value)} /> -
+ ); } +interface DataType { + key: string; + field: string; + value: string; +} + export default TableView; diff --git a/frontend/src/container/LogDetailedView/index.tsx b/frontend/src/container/LogDetailedView/index.tsx index 930dfbf0d8..98ab0909a4 100644 --- a/frontend/src/container/LogDetailedView/index.tsx +++ b/frontend/src/container/LogDetailedView/index.tsx @@ -24,6 +24,19 @@ function LogDetailedView(): JSX.Element { }); }; + const items = [ + { + label: 'Table', + key: '1', + children: detailedLog && , + }, + { + label: 'JSON', + key: '2', + children: detailedLog && , + }, + ]; + return ( - - - {detailedLog && } - - - {detailedLog && } - - + ); } diff --git a/frontend/src/container/LogDetailedView/util.test.ts b/frontend/src/container/LogDetailedView/util.test.ts new file mode 100644 index 0000000000..5d6459ea47 --- /dev/null +++ b/frontend/src/container/LogDetailedView/util.test.ts @@ -0,0 +1,47 @@ +import { recursiveParseJSON } from './utils'; + +describe('recursiveParseJSON', () => { + it('should return an empty object if the input is not valid JSON', () => { + const result = recursiveParseJSON('not valid JSON'); + expect(result).toEqual({}); + }); + + it('should return the parsed JSON object for valid JSON input', () => { + const jsonString = '{"name": "John", "age": 30}'; + const result = recursiveParseJSON(jsonString); + expect(result).toEqual({ name: 'John', age: 30 }); + }); + + it('should recursively parse nested JSON objects', () => { + const jsonString = + '{"name": "John", "age": 30, "address": {"street": "123 Main St", "city": "Anytown", "state": "CA"}}'; + const result = recursiveParseJSON(jsonString); + expect(result).toEqual({ + name: 'John', + age: 30, + address: { + street: '123 Main St', + city: 'Anytown', + state: 'CA', + }, + }); + }); + + it('should recursively parse nested JSON arrays', () => { + const jsonString = '[1, 2, [3, 4], {"foo": "bar"}]'; + const result = recursiveParseJSON(jsonString); + expect(result).toEqual([1, 2, [3, 4], { foo: 'bar' }]); + }); + + it('should recursively parse deeply nested JSON objects', () => { + const jsonString = '{"foo": {"bar": {"baz": {"qux": {"value": 42}}}}}'; + const result = recursiveParseJSON(jsonString); + expect(result).toEqual({ foo: { bar: { baz: { qux: { value: 42 } } } } }); + }); + + it('should handle JSON input that contains escaped characters', () => { + const jsonString = '{"name": "John\\", \\"Doe", "age": 30}'; + const result = recursiveParseJSON(jsonString); + expect(result).toEqual({ name: 'John", "Doe', age: 30 }); + }); +}); diff --git a/frontend/src/container/LogDetailedView/utils.ts b/frontend/src/container/LogDetailedView/utils.ts new file mode 100644 index 0000000000..00a89c96d1 --- /dev/null +++ b/frontend/src/container/LogDetailedView/utils.ts @@ -0,0 +1,11 @@ +export const recursiveParseJSON = (obj: string): Record => { + try { + const value = JSON.parse(obj); + if (typeof value === 'string') { + return recursiveParseJSON(value); + } + return value; + } catch (e) { + return {}; + } +}; diff --git a/frontend/src/container/LogsSearchFilter/SearchFields/QueryBuilder/QueryBuilder.tsx b/frontend/src/container/LogsSearchFilter/SearchFields/QueryBuilder/QueryBuilder.tsx index 4db70b7db2..9aff0b6dd7 100644 --- a/frontend/src/container/LogsSearchFilter/SearchFields/QueryBuilder/QueryBuilder.tsx +++ b/frontend/src/container/LogsSearchFilter/SearchFields/QueryBuilder/QueryBuilder.tsx @@ -27,8 +27,9 @@ function QueryConditionField({ return ( - Loading...{' '} - - ) : ( - - No resource attributes available to filter. Please refer docs to send - attributes. - - ) - } - /> - )} - - {(queries.length || staging.length || selectedValues.length) && !disabled ? ( - } - > - - } - key={EQueryType.QUERY_BUILDER.toString()} - > - { - handleLocalQueryUpdate({ updatedQuery }); - }} - metricsBuilderQueries={ - localQueryChanges[WIDGET_QUERY_BUILDER_QUERY_KEY_NAME] - } - selectedGraph={selectedGraph} - /> - - - } - key={EQueryType.CLICKHOUSE.toString()} - > - { - handleLocalQueryUpdate({ updatedQuery }); - }} - clickHouseQueries={localQueryChanges[WIDGET_CLICKHOUSE_QUERY_KEY_NAME]} - /> - - - } - key={EQueryType.PROM.toString()} - > - { - handleLocalQueryUpdate({ updatedQuery }); - }} - promQLQueries={localQueryChanges[WIDGET_PROMQL_QUERY_KEY_NAME]} - /> - -
+ items={items} + /> {/* {localQueryChanges.map((e, index) => ( // null; +} diff --git a/frontend/src/container/QueryBuilder/components/ListMarker/ListMarker.interfaces.ts b/frontend/src/container/QueryBuilder/components/ListMarker/ListMarker.interfaces.ts new file mode 100644 index 0000000000..5ef7ac5a9a --- /dev/null +++ b/frontend/src/container/QueryBuilder/components/ListMarker/ListMarker.interfaces.ts @@ -0,0 +1,8 @@ +export type ListMarkerProps = { + isDisabled: boolean; + labelName: string; + index: number; + className?: string; + isAvailableToDisable: boolean; + toggleDisabled: (index: number) => void; +}; diff --git a/frontend/src/container/QueryBuilder/components/ListMarker/ListMarker.styled.ts b/frontend/src/container/QueryBuilder/components/ListMarker/ListMarker.styled.ts new file mode 100644 index 0000000000..f876af973b --- /dev/null +++ b/frontend/src/container/QueryBuilder/components/ListMarker/ListMarker.styled.ts @@ -0,0 +1,9 @@ +import { Button } from 'antd'; +import styled from 'styled-components'; + +export const StyledButton = styled(Button)` + min-width: 2rem; + height: 2.25rem; + padding: 0.125rem; + border-radius: 0.375rem; +`; diff --git a/frontend/src/container/QueryBuilder/components/ListMarker/ListMarker.tsx b/frontend/src/container/QueryBuilder/components/ListMarker/ListMarker.tsx new file mode 100644 index 0000000000..a1f0437864 --- /dev/null +++ b/frontend/src/container/QueryBuilder/components/ListMarker/ListMarker.tsx @@ -0,0 +1,36 @@ +import { EyeFilled, EyeInvisibleFilled } from '@ant-design/icons'; +import { ButtonProps } from 'antd'; +import React from 'react'; + +// ** Types +import { ListMarkerProps } from './ListMarker.interfaces'; +// ** Styles +import { StyledButton } from './ListMarker.styled'; + +export function ListMarker({ + isDisabled, + labelName, + index, + isAvailableToDisable, + className, + toggleDisabled, +}: ListMarkerProps): JSX.Element { + const buttonProps: Partial = isAvailableToDisable + ? { + type: isDisabled ? 'default' : 'primary', + icon: isDisabled ? : , + onClick: (): void => toggleDisabled(index), + } + : { type: 'primary' }; + + return ( + + {labelName} + + ); +} diff --git a/frontend/src/container/QueryBuilder/components/ListMarker/index.ts b/frontend/src/container/QueryBuilder/components/ListMarker/index.ts new file mode 100644 index 0000000000..089011726b --- /dev/null +++ b/frontend/src/container/QueryBuilder/components/ListMarker/index.ts @@ -0,0 +1 @@ +export { ListMarker } from './ListMarker'; diff --git a/frontend/src/container/QueryBuilder/components/QueryLabel/QueryLabel.interfaces.ts b/frontend/src/container/QueryBuilder/components/QueryLabel/QueryLabel.interfaces.ts new file mode 100644 index 0000000000..48b8775b1f --- /dev/null +++ b/frontend/src/container/QueryBuilder/components/QueryLabel/QueryLabel.interfaces.ts @@ -0,0 +1,17 @@ +import { SelectProps } from 'antd'; +import { DataSource } from 'types/common/queryBuilder'; + +type StaticLabel = { variant: 'static'; dataSource: DataSource }; + +export type DropdownLabel = { + variant: 'dropdown'; + onChange: (value: DataSource) => void; +} & Omit; + +export type QueryLabelProps = StaticLabel | DropdownLabel; + +export function isLabelDropdown( + label: QueryLabelProps, +): label is DropdownLabel { + return label.variant === 'dropdown'; +} diff --git a/frontend/src/container/QueryBuilder/components/QueryLabel/QueryLabel.styled.ts b/frontend/src/container/QueryBuilder/components/QueryLabel/QueryLabel.styled.ts new file mode 100644 index 0000000000..56b4030035 --- /dev/null +++ b/frontend/src/container/QueryBuilder/components/QueryLabel/QueryLabel.styled.ts @@ -0,0 +1,19 @@ +import { Select } from 'antd'; +import styled, { css } from 'styled-components'; + +// ** Types +import { DropdownLabel } from './QueryLabel.interfaces'; + +const LabelStyle = css` + width: fit-content; + min-width: 5.75rem; +`; + +export const StyledSingleLabel = styled(Select)` + pointer-events: none; + ${LabelStyle} +`; + +export const StyledDropdownLabel = styled(Select)` + ${LabelStyle} +`; diff --git a/frontend/src/container/QueryBuilder/components/QueryLabel/QueryLabel.tsx b/frontend/src/container/QueryBuilder/components/QueryLabel/QueryLabel.tsx new file mode 100644 index 0000000000..1bc6fad1a4 --- /dev/null +++ b/frontend/src/container/QueryBuilder/components/QueryLabel/QueryLabel.tsx @@ -0,0 +1,49 @@ +import { Select } from 'antd'; +import React from 'react'; +import { DataSource } from 'types/common/queryBuilder'; +import { SelectOption } from 'types/common/select'; + +// ** Types +import { isLabelDropdown, QueryLabelProps } from './QueryLabel.interfaces'; +// ** Styles +import { StyledSingleLabel } from './QueryLabel.styled'; + +const { Option } = Select; + +const dataSourceMap = [DataSource.LOGS, DataSource.METRICS, DataSource.TRACES]; + +export function QueryLabel(props: QueryLabelProps): JSX.Element { + const isDropdown = isLabelDropdown(props); + + if (!isDropdown) { + const { dataSource } = props; + + return ( + + + + ); + } + + const { onChange } = props; + + const dataSourceOptions: SelectOption< + DataSource, + string + >[] = dataSourceMap.map((source) => ({ + label: source.charAt(0).toUpperCase() + source.slice(1), + value: source, + })); + + return ( + + Loading... + + ) : ( + + No resource attributes available to filter. Please refer docs to send + attributes. + + ) + } + /> + + {queries.length || staging.length || selectedQuery.length ? ( +