diff --git a/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml b/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml index d1c1568f61..88b022d34a 100644 --- a/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml +++ b/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml @@ -40,7 +40,7 @@ services: condition: on-failure query-service: - image: signoz/query-service:0.11.0 + image: signoz/query-service:0.11.1 command: ["-config=/root/config/prometheus.yml"] # ports: # - "6060:6060" # pprof port @@ -70,7 +70,7 @@ services: - clickhouse frontend: - image: signoz/frontend:0.11.0 + image: signoz/frontend:0.11.1 deploy: restart_policy: condition: on-failure @@ -83,7 +83,7 @@ services: - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf otel-collector: - image: signoz-otel-collector:0.55.0 + image: signoz-otel-collector:0.55.1 command: ["--config=/etc/otel-collector-config.yaml"] user: root # required for reading docker container logs volumes: @@ -111,7 +111,7 @@ services: - clickhouse otel-collector-metrics: - image: signoz-otel-collector:0.55.0 + image: signoz-otel-collector:0.55.1 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-swarm/clickhouse-setup/prometheus.yml b/deploy/docker-swarm/clickhouse-setup/prometheus.yml index 16e65ff18c..6a796ea1d0 100644 --- a/deploy/docker-swarm/clickhouse-setup/prometheus.yml +++ b/deploy/docker-swarm/clickhouse-setup/prometheus.yml @@ -19,8 +19,7 @@ rule_files: # A scrape configuration containing exactly one endpoint to scrape: # Here it's Prometheus itself. -scrape_configs: - +scrape_configs: [] remote_read: - url: tcp://clickhouse:9000/?database=signoz_metrics diff --git a/deploy/docker-swarm/common/nginx-config.conf b/deploy/docker-swarm/common/nginx-config.conf index 3153dff62f..d822e68c40 100644 --- a/deploy/docker-swarm/common/nginx-config.conf +++ b/deploy/docker-swarm/common/nginx-config.conf @@ -3,13 +3,17 @@ server { server_name _; gzip on; - gzip_static on; + gzip_static on; gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript; gzip_proxied any; gzip_vary on; gzip_comp_level 6; gzip_buffers 16 8k; - gzip_http_version 1.1; + gzip_http_version 1.1; + + # to handle uri issue 414 from nginx + client_max_body_size 24M; + large_client_header_buffers 8 16k; location / { if ( $uri = '/index.html' ) { diff --git a/deploy/docker/clickhouse-setup/docker-compose-core.yaml b/deploy/docker/clickhouse-setup/docker-compose-core.yaml index ead470cefb..da338d1dd7 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.55.0 + image: signoz/signoz-otel-collector:0.55.1 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.55.0 + image: signoz/signoz-otel-collector:0.55.1 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-prod.yaml b/deploy/docker/clickhouse-setup/docker-compose-prod.yaml index d86510e849..f2a4d73269 100644 --- a/deploy/docker/clickhouse-setup/docker-compose-prod.yaml +++ b/deploy/docker/clickhouse-setup/docker-compose-prod.yaml @@ -2,7 +2,7 @@ version: "2.4" services: query-service: - image: signoz/query-service:0.11.0 + image: signoz/query-service:0.11.1 container_name: query-service command: ["-config=/root/config/prometheus.yml"] # ports: @@ -31,7 +31,7 @@ services: condition: service_healthy frontend: - image: signoz/frontend:0.11.0 + image: signoz/frontend:0.11.1 container_name: frontend restart: on-failure depends_on: diff --git a/deploy/docker/clickhouse-setup/docker-compose.yaml b/deploy/docker/clickhouse-setup/docker-compose.yaml index e085f1dba3..ae330e363f 100644 --- a/deploy/docker/clickhouse-setup/docker-compose.yaml +++ b/deploy/docker/clickhouse-setup/docker-compose.yaml @@ -39,7 +39,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:0.11.0 + image: signoz/query-service:0.11.1 container_name: query-service command: ["-config=/root/config/prometheus.yml"] # ports: @@ -68,7 +68,7 @@ services: condition: service_healthy frontend: - image: signoz/frontend:0.11.0 + image: signoz/frontend:0.11.1 container_name: frontend restart: on-failure depends_on: @@ -80,7 +80,7 @@ services: - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf otel-collector: - image: signoz/signoz-otel-collector:0.55.0 + image: signoz/signoz-otel-collector:0.55.1 command: ["--config=/etc/otel-collector-config.yaml"] user: root # required for reading docker container logs volumes: @@ -106,7 +106,7 @@ services: condition: service_healthy otel-collector-metrics: - image: signoz/signoz-otel-collector:0.55.0 + image: signoz/signoz-otel-collector:0.55.1 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/prometheus.yml b/deploy/docker/clickhouse-setup/prometheus.yml index 16e65ff18c..6a796ea1d0 100644 --- a/deploy/docker/clickhouse-setup/prometheus.yml +++ b/deploy/docker/clickhouse-setup/prometheus.yml @@ -19,8 +19,7 @@ rule_files: # A scrape configuration containing exactly one endpoint to scrape: # Here it's Prometheus itself. -scrape_configs: - +scrape_configs: [] remote_read: - url: tcp://clickhouse:9000/?database=signoz_metrics diff --git a/deploy/docker/common/nginx-config.conf b/deploy/docker/common/nginx-config.conf index 99615f1f60..d822e68c40 100644 --- a/deploy/docker/common/nginx-config.conf +++ b/deploy/docker/common/nginx-config.conf @@ -3,7 +3,7 @@ server { server_name _; gzip on; - gzip_static on; + gzip_static on; gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript; gzip_proxied any; gzip_vary on; @@ -13,7 +13,6 @@ server { # to handle uri issue 414 from nginx client_max_body_size 24M; - large_client_header_buffers 8 16k; location / { diff --git a/frontend/Dockerfile b/frontend/Dockerfile index d7fff6c0bb..71188e71c9 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -13,7 +13,7 @@ WORKDIR /frontend COPY package.json ./ # Install the dependencies and make the folder -RUN CI=1 yarn install +RUN CI=1 yarn install COPY . . diff --git a/frontend/conf/default.conf b/frontend/conf/default.conf index dc0475eaf4..37b77463d4 100644 --- a/frontend/conf/default.conf +++ b/frontend/conf/default.conf @@ -1,15 +1,19 @@ server { listen 3301; server_name _; - + gzip on; - gzip_static on; + gzip_static on; gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript; gzip_proxied any; gzip_vary on; gzip_comp_level 6; gzip_buffers 16 8k; - gzip_http_version 1.1; + gzip_http_version 1.1; + + # to handle uri issue 414 from nginx + client_max_body_size 24M; + large_client_header_buffers 8 16k; location / { root /usr/share/nginx/html; diff --git a/frontend/public/Logos/elixir.png b/frontend/public/Logos/elixir.png new file mode 100644 index 0000000000..909a736e0f Binary files /dev/null and b/frontend/public/Logos/elixir.png differ diff --git a/frontend/public/Logos/go.png b/frontend/public/Logos/go.png new file mode 100644 index 0000000000..70f7bd9728 Binary files /dev/null and b/frontend/public/Logos/go.png differ diff --git a/frontend/public/Logos/java.png b/frontend/public/Logos/java.png new file mode 100644 index 0000000000..9e48da3616 Binary files /dev/null and b/frontend/public/Logos/java.png differ diff --git a/frontend/public/Logos/javascript.png b/frontend/public/Logos/javascript.png new file mode 100644 index 0000000000..f58be12e19 Binary files /dev/null and b/frontend/public/Logos/javascript.png differ diff --git a/frontend/public/Logos/ms-net-framework.png b/frontend/public/Logos/ms-net-framework.png new file mode 100644 index 0000000000..5b0baac0de Binary files /dev/null and b/frontend/public/Logos/ms-net-framework.png differ diff --git a/frontend/public/Logos/php.png b/frontend/public/Logos/php.png new file mode 100644 index 0000000000..9abe0a96f6 Binary files /dev/null and b/frontend/public/Logos/php.png differ diff --git a/frontend/public/Logos/python.png b/frontend/public/Logos/python.png new file mode 100644 index 0000000000..664f75d425 Binary files /dev/null and b/frontend/public/Logos/python.png differ diff --git a/frontend/public/Logos/rails.png b/frontend/public/Logos/rails.png new file mode 100644 index 0000000000..0a44785da6 Binary files /dev/null and b/frontend/public/Logos/rails.png differ diff --git a/frontend/public/Logos/rust.png b/frontend/public/Logos/rust.png new file mode 100644 index 0000000000..2637dc9314 Binary files /dev/null and b/frontend/public/Logos/rust.png differ diff --git a/frontend/src/AppRoutes/pageComponents.ts b/frontend/src/AppRoutes/pageComponents.ts index 46cf8ec34a..c83dfcd991 100644 --- a/frontend/src/AppRoutes/pageComponents.ts +++ b/frontend/src/AppRoutes/pageComponents.ts @@ -35,11 +35,8 @@ export const SettingsPage = Loadable( () => import(/* webpackChunkName: "SettingsPage" */ 'pages/Settings'), ); -export const InstrumentationPage = Loadable( - () => - import( - /* webpackChunkName: "InstrumentationPage" */ 'pages/AddInstrumentation' - ), +export const GettingStarted = Loadable( + () => import(/* webpackChunkName: "GettingStarted" */ 'pages/GettingStarted'), ); export const DashboardPage = Loadable( diff --git a/frontend/src/AppRoutes/routes.ts b/frontend/src/AppRoutes/routes.ts index 876ece4af1..9bf52b39de 100644 --- a/frontend/src/AppRoutes/routes.ts +++ b/frontend/src/AppRoutes/routes.ts @@ -11,7 +11,7 @@ import { EditAlertChannelsAlerts, EditRulesPage, ErrorDetails, - InstrumentationPage, + GettingStarted, ListAllALertsPage, Login, Logs, @@ -85,7 +85,7 @@ const routes: AppRoutes[] = [ { path: ROUTES.INSTRUMENTATION, exact: true, - component: InstrumentationPage, + component: GettingStarted, isPrivate: true, key: 'INSTRUMENTATION', }, diff --git a/frontend/src/api/dashboard/variables/query.ts b/frontend/src/api/dashboard/variables/query.ts new file mode 100644 index 0000000000..61693ba4c0 --- /dev/null +++ b/frontend/src/api/dashboard/variables/query.ts @@ -0,0 +1,26 @@ +import axios from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { PayloadProps, Props } from 'types/api/dashboard/variables/query'; + +const query = async ( + props: Props, +): Promise | ErrorResponse> => { + try { + const response = await axios.get( + `/variables/query?query=${encodeURIComponent(props.query)}`, + ); + + return { + statusCode: 200, + error: null, + message: response.data.status, + payload: response.data.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default query; diff --git a/frontend/src/constants/routes.ts b/frontend/src/constants/routes.ts index c9fc9fe90a..6fe138d33b 100644 --- a/frontend/src/constants/routes.ts +++ b/frontend/src/constants/routes.ts @@ -6,7 +6,7 @@ const ROUTES = { TRACE: '/trace', TRACE_DETAIL: '/trace/:id', SETTINGS: '/settings', - INSTRUMENTATION: '/add-instrumentation', + INSTRUMENTATION: '/get-started', USAGE_EXPLORER: '/usage-explorer', APPLICATION: '/application', ALL_DASHBOARD: '/dashboard', diff --git a/frontend/src/container/AppLayout/styles.ts b/frontend/src/container/AppLayout/styles.ts index 71547d1592..5db7517c66 100644 --- a/frontend/src/container/AppLayout/styles.ts +++ b/frontend/src/container/AppLayout/styles.ts @@ -3,7 +3,7 @@ import styled from 'styled-components'; export const Layout = styled(LayoutComponent)` &&& { - min-height: 91vh; + min-height: 92vh; display: flex; position: relative; } diff --git a/frontend/src/container/GridGraphLayout/Graph/FullView/index.metricsBuilder.tsx b/frontend/src/container/GridGraphLayout/Graph/FullView/index.metricsBuilder.tsx index 8fe3b259c1..113049a295 100644 --- a/frontend/src/container/GridGraphLayout/Graph/FullView/index.metricsBuilder.tsx +++ b/frontend/src/container/GridGraphLayout/Graph/FullView/index.metricsBuilder.tsx @@ -7,6 +7,7 @@ import { timeItems, timePreferance, } from 'container/NewWidget/RightContainer/timeItems'; +import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables'; import getChartData from 'lib/getChartData'; import React, { useCallback, useState } from 'react'; import { useQuery } from 'react-query'; @@ -52,6 +53,7 @@ function FullView({ graphType: widget.panelTypes, query: widget.query, globalSelectedInterval: globalSelectedTime, + variables: getDashboardVariables(), }), ); diff --git a/frontend/src/container/GridGraphLayout/Graph/index.tsx b/frontend/src/container/GridGraphLayout/Graph/index.tsx index f39d64cb76..4d654a0222 100644 --- a/frontend/src/container/GridGraphLayout/Graph/index.tsx +++ b/frontend/src/container/GridGraphLayout/Graph/index.tsx @@ -3,6 +3,7 @@ import { AxiosError } from 'axios'; import { ChartData } from 'chart.js'; import Spinner from 'components/Spinner'; import GridGraphComponent from 'container/GridGraphComponent'; +import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables'; import getChartData from 'lib/getChartData'; import isEmpty from 'lodash-es/isEmpty'; import React, { memo, useCallback, useEffect, useState } from 'react'; @@ -104,11 +105,18 @@ function GridCardGraph({ useEffect(() => { (async (): Promise => { try { + setState((state) => ({ + ...state, + error: false, + errorMessage: '', + loading: true, + })); const response = await GetMetricQueryRange({ selectedTime: widget.timePreferance, graphType: widget.panelTypes, query: widget.query, globalSelectedInterval, + variables: getDashboardVariables(), }); const isError = response.error; @@ -144,6 +152,11 @@ function GridCardGraph({ errorMessage: (error as AxiosError).toString(), loading: false, })); + } finally { + setState((state) => ({ + ...state, + loading: false, + })); } })(); }, [widget, maxTime, minTime, globalSelectedInterval]); diff --git a/frontend/src/container/GridGraphLayout/index.tsx b/frontend/src/container/GridGraphLayout/index.tsx index d80b12f7a1..234b10ceb7 100644 --- a/frontend/src/container/GridGraphLayout/index.tsx +++ b/frontend/src/container/GridGraphLayout/index.tsx @@ -121,6 +121,7 @@ function GridGraph(props: Props): JSX.Element { name: data.name, tags: data.tags, widgets: data.widgets, + variables: data.variables, layout, }, uuid: selectedDashboard.uuid, @@ -157,6 +158,7 @@ function GridGraph(props: Props): JSX.Element { data.name, data.tags, data.title, + data.variables, data.widgets, dispatch, saveLayoutPermission, diff --git a/frontend/src/container/GridGraphLayout/utils.ts b/frontend/src/container/GridGraphLayout/utils.ts index 0d7320f6f5..d06c69c757 100644 --- a/frontend/src/container/GridGraphLayout/utils.ts +++ b/frontend/src/container/GridGraphLayout/utils.ts @@ -27,6 +27,7 @@ export const UpdateDashboard = async ({ description: data.description, name: data.name, tags: data.tags, + variables: data.variables, widgets: [ ...(data.widgets || []), { diff --git a/frontend/src/container/ListOfDashboard/SearchFilter/__tests__/utils.test.ts b/frontend/src/container/ListOfDashboard/SearchFilter/__tests__/utils.test.ts index db9825b677..369a0dffa3 100644 --- a/frontend/src/container/ListOfDashboard/SearchFilter/__tests__/utils.test.ts +++ b/frontend/src/container/ListOfDashboard/SearchFilter/__tests__/utils.test.ts @@ -12,6 +12,7 @@ describe('executeSearchQueries', () => { updated_at: '', data: { title: 'first dashboard', + variables: {}, }, }; const secondDashboard: Dashboard = { @@ -21,6 +22,7 @@ describe('executeSearchQueries', () => { updated_at: '', data: { title: 'second dashboard', + variables: {}, }, }; const thirdDashboard: Dashboard = { @@ -30,6 +32,7 @@ describe('executeSearchQueries', () => { updated_at: '', data: { title: 'third dashboard (with special characters +?\\)', + variables: {}, }, }; const dashboards = [firstDashboard, secondDashboard, thirdDashboard]; diff --git a/frontend/src/container/NewDashboard/DescriptionOfDashboard/AddTags/index.tsx b/frontend/src/container/NewDashboard/DashboardSettings/General/AddTags/index.tsx similarity index 100% rename from frontend/src/container/NewDashboard/DescriptionOfDashboard/AddTags/index.tsx rename to frontend/src/container/NewDashboard/DashboardSettings/General/AddTags/index.tsx diff --git a/frontend/src/container/NewDashboard/DescriptionOfDashboard/AddTags/styles.ts b/frontend/src/container/NewDashboard/DashboardSettings/General/AddTags/styles.ts similarity index 100% rename from frontend/src/container/NewDashboard/DescriptionOfDashboard/AddTags/styles.ts rename to frontend/src/container/NewDashboard/DashboardSettings/General/AddTags/styles.ts diff --git a/frontend/src/container/NewDashboard/DescriptionOfDashboard/Description/index.tsx b/frontend/src/container/NewDashboard/DashboardSettings/General/Description/index.tsx similarity index 100% rename from frontend/src/container/NewDashboard/DescriptionOfDashboard/Description/index.tsx rename to frontend/src/container/NewDashboard/DashboardSettings/General/Description/index.tsx diff --git a/frontend/src/container/NewDashboard/DescriptionOfDashboard/Description/styles.ts b/frontend/src/container/NewDashboard/DashboardSettings/General/Description/styles.ts similarity index 100% rename from frontend/src/container/NewDashboard/DescriptionOfDashboard/Description/styles.ts rename to frontend/src/container/NewDashboard/DashboardSettings/General/Description/styles.ts diff --git a/frontend/src/container/NewDashboard/DashboardSettings/General/index.tsx b/frontend/src/container/NewDashboard/DashboardSettings/General/index.tsx new file mode 100644 index 0000000000..4a8fce57a2 --- /dev/null +++ b/frontend/src/container/NewDashboard/DashboardSettings/General/index.tsx @@ -0,0 +1,112 @@ +import { SaveOutlined } from '@ant-design/icons'; +import { Col, Divider, Input, Space, Typography } from 'antd'; +import AddTags from 'container/NewDashboard/DashboardSettings/General/AddTags'; +import React, { useCallback, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { connect, useSelector } from 'react-redux'; +import { bindActionCreators, Dispatch } from 'redux'; +import { ThunkDispatch } from 'redux-thunk'; +import { + UpdateDashboardTitleDescriptionTags, + UpdateDashboardTitleDescriptionTagsProps, +} from 'store/actions'; +import { AppState } from 'store/reducers'; +import AppActions from 'types/actions'; +import DashboardReducer from 'types/reducer/dashboards'; + +import { Button } from './styles'; + +function GeneralDashboardSettings({ + updateDashboardTitleDescriptionTags, +}: DescriptionOfDashboardProps): JSX.Element { + const { dashboards } = useSelector( + (state) => state.dashboards, + ); + + const [selectedDashboard] = dashboards; + const selectedData = selectedDashboard.data; + const { title } = selectedData; + const { tags } = selectedData; + const { description } = selectedData; + + const [updatedTitle, setUpdatedTitle] = useState(title); + const [updatedTags, setUpdatedTags] = useState(tags || []); + const [updatedDescription, setUpdatedDescription] = useState( + description || '', + ); + + const { t } = useTranslation('common'); + + const onSaveHandler = useCallback(() => { + const dashboard = selectedDashboard; + // @TODO need to update this function to take title,description,tags only + updateDashboardTitleDescriptionTags({ + dashboard: { + ...dashboard, + data: { + ...dashboard.data, + description: updatedDescription, + tags: updatedTags, + title: updatedTitle, + }, + }, + }); + }, [ + updatedTitle, + updatedTags, + updatedDescription, + selectedDashboard, + updateDashboardTitleDescriptionTags, + ]); + + return ( + + +
+ Name + setUpdatedTitle(e.target.value)} + /> +
+ +
+ Description + setUpdatedDescription(e.target.value)} + /> +
+
+ Tags + +
+
+ + +
+
+ + ); +} + +interface DispatchProps { + updateDashboardTitleDescriptionTags: ( + props: UpdateDashboardTitleDescriptionTagsProps, + ) => (dispatch: Dispatch) => void; +} + +const mapDispatchToProps = ( + dispatch: ThunkDispatch, +): DispatchProps => ({ + updateDashboardTitleDescriptionTags: bindActionCreators( + UpdateDashboardTitleDescriptionTags, + dispatch, + ), +}); + +type DescriptionOfDashboardProps = DispatchProps; + +export default connect(null, mapDispatchToProps)(GeneralDashboardSettings); diff --git a/frontend/src/container/NewDashboard/DashboardSettings/General/styles.ts b/frontend/src/container/NewDashboard/DashboardSettings/General/styles.ts new file mode 100644 index 0000000000..43bcccef51 --- /dev/null +++ b/frontend/src/container/NewDashboard/DashboardSettings/General/styles.ts @@ -0,0 +1,20 @@ +import { Button as ButtonComponent, Drawer } from 'antd'; +import styled from 'styled-components'; + +export const Container = styled.div` + margin-top: 0.5rem; +`; + +export const Button = styled(ButtonComponent)` + &&& { + display: flex; + align-items: center; + } +`; + +export const DrawerContainer = styled(Drawer)` + .ant-drawer-header { + padding: 0; + border: none; + } +`; diff --git a/frontend/src/container/NewDashboard/DashboardSettings/Variables/VariableItem/VariableItem.tsx b/frontend/src/container/NewDashboard/DashboardSettings/Variables/VariableItem/VariableItem.tsx new file mode 100644 index 0000000000..f8a08f2677 --- /dev/null +++ b/frontend/src/container/NewDashboard/DashboardSettings/Variables/VariableItem/VariableItem.tsx @@ -0,0 +1,354 @@ +/* eslint-disable sonarjs/cognitive-complexity */ +import { orange } from '@ant-design/colors'; +import { + Button, + Col, + Divider, + Input, + Select, + Switch, + Tag, + Typography, +} from 'antd'; +import query from 'api/dashboard/variables/query'; +import Editor from 'components/Editor'; +import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParser'; +import sortValues from 'lib/dashbaordVariables/sortVariableValues'; +import { map } from 'lodash-es'; +import React, { useEffect, useState } from 'react'; +import { + IDashboardVariable, + TSortVariableValuesType, + TVariableQueryType, + VariableQueryTypeArr, + VariableSortTypeArr, +} from 'types/api/dashboard/getAll'; +import { v4 } from 'uuid'; + +import { TVariableViewMode } from '../types'; +import { LabelContainer, VariableItemRow } from './styles'; + +const { Option } = Select; + +interface VariableItemProps { + variableData: IDashboardVariable; + onCancel: () => void; + onSave: (name: string, arg0: IDashboardVariable, arg1: string) => void; + validateName: (arg0: string) => boolean; + variableViewMode: TVariableViewMode; +} +function VariableItem({ + variableData, + onCancel, + onSave, + validateName, + variableViewMode, +}: VariableItemProps): JSX.Element { + const [variableName, setVariableName] = useState( + variableData.name || '', + ); + const [variableDescription, setVariableDescription] = useState( + variableData.description || '', + ); + const [queryType, setQueryType] = useState( + variableData.type || 'QUERY', + ); + const [variableQueryValue, setVariableQueryValue] = useState( + variableData.queryValue || '', + ); + const [variableCustomValue, setVariableCustomValue] = useState( + variableData.customValue || '', + ); + const [variableTextboxValue, setVariableTextboxValue] = useState( + variableData.textboxValue || '', + ); + const [ + variableSortType, + setVariableSortType, + ] = useState( + variableData.sort || VariableSortTypeArr[0], + ); + const [variableMultiSelect, setVariableMultiSelect] = useState( + variableData.multiSelect || false, + ); + const [variableShowALLOption, setVariableShowALLOption] = useState( + variableData.showALLOption || false, + ); + const [previewValues, setPreviewValues] = useState([]); + + // Internal states + const [previewLoading, setPreviewLoading] = useState(false); + // Error messages + const [errorName, setErrorName] = useState(false); + const [errorPreview, setErrorPreview] = useState(null); + + useEffect(() => { + setPreviewValues([]); + if (queryType === 'CUSTOM') { + setPreviewValues( + sortValues( + commaValuesParser(variableCustomValue), + variableSortType, + ) as never, + ); + } + }, [ + queryType, + variableCustomValue, + variableData.customValue, + variableData.type, + variableSortType, + ]); + + const handleSave = (): void => { + const newVariableData: IDashboardVariable = { + name: variableName, + description: variableDescription, + type: queryType, + queryValue: variableQueryValue, + customValue: variableCustomValue, + textboxValue: variableTextboxValue, + multiSelect: variableMultiSelect, + showALLOption: variableShowALLOption, + sort: variableSortType, + ...(queryType === 'TEXTBOX' && { + selectedValue: (variableData.selectedValue || + variableTextboxValue) as never, + }), + modificationUUID: v4(), + }; + onSave( + variableName, + newVariableData, + (variableViewMode === 'EDIT' && variableName !== variableData.name + ? variableData.name + : '') as string, + ); + onCancel(); + }; + + // Fetches the preview values for the SQL variable query + const handleQueryResult = async (): Promise => { + setPreviewLoading(true); + setErrorPreview(null); + try { + const variableQueryResponse = await query({ + query: variableQueryValue, + }); + setPreviewLoading(false); + if (variableQueryResponse.error) { + setErrorPreview(variableQueryResponse.error); + return; + } + if (variableQueryResponse.payload?.variableValues) + setPreviewValues( + sortValues( + variableQueryResponse.payload?.variableValues || [], + variableSortType, + ) as never, + ); + } catch (e) { + console.error(e); + } + }; + return ( + + {/* Add Variable */} + + + Name + +
+ { + setVariableName(e.target.value); + setErrorName( + !validateName(e.target.value) && e.target.value !== variableData.name, + ); + }} + /> +
+ + {errorName ? 'Variable name already exists' : ''} + +
+
+
+ + + Description + + + setVariableDescription(e.target.value)} + /> + + + + Type + + + + + + Options + + {queryType === 'QUERY' && ( + + + Query + + +
+ setVariableQueryValue(e)} + height="300px" + /> + +
+
+ )} + {queryType === 'CUSTOM' && ( + + + Values separated by comma + + { + setVariableCustomValue(e.target.value); + setPreviewValues( + sortValues( + commaValuesParser(e.target.value), + variableSortType, + ) as never, + ); + }} + /> + + )} + {queryType === 'TEXTBOX' && ( + + + Default Value + + { + setVariableTextboxValue(e.target.value); + }} + placeholder="Default value if any" + style={{ width: 400 }} + /> + + )} + {(queryType === 'QUERY' || queryType === 'CUSTOM') && ( + <> + + + Preview of Values + +
+ {errorPreview ? ( + {errorPreview} + ) : ( + map(previewValues, (value, idx) => ( + {value.toString()} + )) + )} +
+
+ + + Sort + + + + + + + Enable multiple values to be checked + + { + setVariableMultiSelect(e); + if (!e) { + setVariableShowALLOption(false); + } + }} + /> + + {variableMultiSelect && ( + + + Include an option for ALL values + + setVariableShowALLOption(e)} + /> + + )} + + )} + + + + + + + ); +} + +export default VariableItem; diff --git a/frontend/src/container/NewDashboard/DashboardSettings/Variables/VariableItem/styles.ts b/frontend/src/container/NewDashboard/DashboardSettings/Variables/VariableItem/styles.ts new file mode 100644 index 0000000000..2d603ede18 --- /dev/null +++ b/frontend/src/container/NewDashboard/DashboardSettings/Variables/VariableItem/styles.ts @@ -0,0 +1,11 @@ +import { Row } from 'antd'; +import styled from 'styled-components'; + +export const VariableItemRow = styled(Row)` + gap: 1rem; + margin-bottom: 1rem; +`; + +export const LabelContainer = styled.div` + width: 200px; +`; diff --git a/frontend/src/container/NewDashboard/DashboardSettings/Variables/index.tsx b/frontend/src/container/NewDashboard/DashboardSettings/Variables/index.tsx new file mode 100644 index 0000000000..02f2d3e9ae --- /dev/null +++ b/frontend/src/container/NewDashboard/DashboardSettings/Variables/index.tsx @@ -0,0 +1,194 @@ +import { blue, red } from '@ant-design/colors'; +import { PlusOutlined } from '@ant-design/icons'; +import { Button, Modal, Row, Space, Table, Tag } from 'antd'; +import React, { useRef, useState } from 'react'; +import { connect, useSelector } from 'react-redux'; +import { bindActionCreators, Dispatch } from 'redux'; +import { ThunkDispatch } from 'redux-thunk'; +import { UpdateDashboardVariables } from 'store/actions/dashboard/updatedDashboardVariables'; +import { AppState } from 'store/reducers'; +import AppActions from 'types/actions'; +import { IDashboardVariable } from 'types/api/dashboard/getAll'; +import DashboardReducer from 'types/reducer/dashboards'; + +import { TVariableViewMode } from './types'; +import VariableItem from './VariableItem/VariableItem'; + +function VariablesSetting({ + updateDashboardVariables, +}: DispatchProps): JSX.Element { + const variableToDelete = useRef(null); + const [deleteVariableModal, setDeleteVariableModal] = useState(false); + + const { dashboards } = useSelector( + (state) => state.dashboards, + ); + + const [selectedDashboard] = dashboards; + + const { + data: { variables = {} }, + } = selectedDashboard; + + const variablesTableData = Object.keys(variables).map((variableName) => ({ + key: variableName, + name: variableName, + ...variables[variableName], + })); + + const [ + variableViewMode, + setVariableViewMode, + ] = useState(null); + + const [ + variableEditData, + setVariableEditData, + ] = useState(null); + + const onDoneVariableViewMode = (): void => { + setVariableViewMode(null); + setVariableEditData(null); + }; + + const onVariableViewModeEnter = ( + viewType: TVariableViewMode, + varData: IDashboardVariable, + ): void => { + setVariableEditData(varData); + setVariableViewMode(viewType); + }; + + const onVariableSaveHandler = ( + name: string, + variableData: IDashboardVariable, + oldName: string, + ): void => { + if (!variableData.name) { + return; + } + + const newVariables = { ...variables }; + newVariables[name] = variableData; + + if (oldName) { + delete newVariables[oldName]; + } + + updateDashboardVariables(newVariables); + onDoneVariableViewMode(); + }; + + const onVariableDeleteHandler = (variableName: string): void => { + variableToDelete.current = variableName; + setDeleteVariableModal(true); + }; + + const handleDeleteConfirm = (): void => { + const newVariables = { ...variables }; + if (variableToDelete?.current) delete newVariables[variableToDelete?.current]; + updateDashboardVariables(newVariables); + variableToDelete.current = null; + setDeleteVariableModal(false); + }; + const handleDeleteCancel = (): void => { + variableToDelete.current = null; + setDeleteVariableModal(false); + }; + + const validateVariableName = (name: string): boolean => { + return !variables[name]; + }; + + const columns = [ + { + title: 'Variable', + dataIndex: 'name', + key: 'name', + }, + { + title: 'Definition', + dataIndex: 'description', + key: 'description', + }, + { + title: 'Actions', + key: 'action', + render: (_: IDashboardVariable): JSX.Element => ( + + + + + ), + }, + ]; + + return ( + <> + {variableViewMode ? ( + + ) : ( + <> + + + + + + )} + + Are you sure you want to delete variable{' '} + {variableToDelete.current}? + + + ); +} + +interface DispatchProps { + updateDashboardVariables: ( + props: Record, + ) => (dispatch: Dispatch) => void; +} + +const mapDispatchToProps = ( + dispatch: ThunkDispatch, +): DispatchProps => ({ + updateDashboardVariables: bindActionCreators( + UpdateDashboardVariables, + dispatch, + ), +}); + +export default connect(null, mapDispatchToProps)(VariablesSetting); diff --git a/frontend/src/container/NewDashboard/DashboardSettings/Variables/types.ts b/frontend/src/container/NewDashboard/DashboardSettings/Variables/types.ts new file mode 100644 index 0000000000..877372f653 --- /dev/null +++ b/frontend/src/container/NewDashboard/DashboardSettings/Variables/types.ts @@ -0,0 +1 @@ +export type TVariableViewMode = 'EDIT' | 'ADD'; diff --git a/frontend/src/container/NewDashboard/DashboardSettings/index.tsx b/frontend/src/container/NewDashboard/DashboardSettings/index.tsx new file mode 100644 index 0000000000..d8cfa57ff6 --- /dev/null +++ b/frontend/src/container/NewDashboard/DashboardSettings/index.tsx @@ -0,0 +1,22 @@ +import { Tabs } from 'antd'; +import React from 'react'; + +import GeneralDashboardSettings from './General'; +import VariablesSetting from './Variables'; + +const { TabPane } = Tabs; + +function DashboardSettingsContent(): JSX.Element { + return ( + + + + + + + + + ); +} + +export default DashboardSettingsContent; diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.tsx b/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.tsx new file mode 100644 index 0000000000..b40d113be4 --- /dev/null +++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.tsx @@ -0,0 +1,137 @@ +import { orange } from '@ant-design/colors'; +import { WarningOutlined } from '@ant-design/icons'; +import { Input, Popover, Select, Typography } from 'antd'; +import query from 'api/dashboard/variables/query'; +import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParser'; +import sortValues from 'lib/dashbaordVariables/sortVariableValues'; +import { map } from 'lodash-es'; +import React, { useCallback, useEffect, useState } from 'react'; +import { IDashboardVariable } from 'types/api/dashboard/getAll'; + +import { VariableContainer, VariableName } from './styles'; + +const { Option } = Select; + +const ALL_SELECT_VALUE = '__ALL__'; + +interface VariableItemProps { + variableData: IDashboardVariable; + onValueUpdate: (name: string | undefined, arg1: string | string[]) => void; + onAllSelectedUpdate: (name: string | undefined, arg1: boolean) => void; +} +function VariableItem({ + variableData, + onValueUpdate, + onAllSelectedUpdate, +}: VariableItemProps): JSX.Element { + const [optionsData, setOptionsData] = useState([]); + const [isLoading, setIsLoading] = useState(false); + + const [errorMessage, setErrorMessage] = useState(null); + const getOptions = useCallback(async (): Promise => { + if (variableData.type === 'QUERY') { + try { + setErrorMessage(null); + setIsLoading(true); + + const response = await query({ + query: variableData.queryValue || '', + }); + + setIsLoading(false); + if (response.error) { + setErrorMessage(response.error); + return; + } + if (response.payload?.variableValues) + setOptionsData( + sortValues(response.payload?.variableValues, variableData.sort) as never, + ); + } catch (e) { + console.error(e); + } + } else if (variableData.type === 'CUSTOM') { + setOptionsData( + sortValues( + commaValuesParser(variableData.customValue || ''), + variableData.sort, + ) as never, + ); + } + }, [ + variableData.customValue, + variableData.queryValue, + variableData.sort, + variableData.type, + ]); + + useEffect(() => { + getOptions(); + }, [getOptions]); + + const handleChange = (value: string | string[]): void => { + if ( + value === ALL_SELECT_VALUE || + (Array.isArray(value) && value.includes(ALL_SELECT_VALUE)) + ) { + onValueUpdate(variableData.name, optionsData); + onAllSelectedUpdate(variableData.name, true); + } else { + onValueUpdate(variableData.name, value); + onAllSelectedUpdate(variableData.name, false); + } + }; + return ( + + ${variableData.name} + {variableData.type === 'TEXTBOX' ? ( + { + handleChange(e.target.value || ''); + }} + style={{ + width: 50 + ((variableData.selectedValue?.length || 0) * 7 || 50), + }} + /> + ) : ( + + )} + {errorMessage && ( + + {errorMessage}}> + + + + )} + + ); +} + +export default VariableItem; diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/index.tsx b/frontend/src/container/NewDashboard/DashboardVariablesSelection/index.tsx new file mode 100644 index 0000000000..c3aad577bf --- /dev/null +++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/index.tsx @@ -0,0 +1,72 @@ +import { Row } from 'antd'; +import { map, sortBy } from 'lodash-es'; +import React from 'react'; +import { connect, useSelector } from 'react-redux'; +import { bindActionCreators, Dispatch } from 'redux'; +import { ThunkDispatch } from 'redux-thunk'; +import { UpdateDashboardVariables } from 'store/actions/dashboard/updatedDashboardVariables'; +import { AppState } from 'store/reducers'; +import AppActions from 'types/actions'; +import { IDashboardVariable } from 'types/api/dashboard/getAll'; +import DashboardReducer from 'types/reducer/dashboards'; + +import VariableItem from './VariableItem'; + +function DashboardVariableSelection({ + updateDashboardVariables, +}: DispatchProps): JSX.Element { + const { dashboards } = useSelector( + (state) => state.dashboards, + ); + const [selectedDashboard] = dashboards; + const { + data: { variables = {} }, + } = selectedDashboard; + + const onValueUpdate = ( + name: string, + value: IDashboardVariable['selectedValue'], + ): void => { + const updatedVariablesData = { ...variables }; + updatedVariablesData[name].selectedValue = value; + updateDashboardVariables(updatedVariablesData); + }; + const onAllSelectedUpdate = ( + name: string, + value: IDashboardVariable['allSelected'], + ): void => { + const updatedVariablesData = { ...variables }; + updatedVariablesData[name].allSelected = value; + updateDashboardVariables(updatedVariablesData); + }; + + return ( + + {map(sortBy(Object.keys(variables)), (variableName) => ( + + ))} + + ); +} + +interface DispatchProps { + updateDashboardVariables: ( + props: Parameters[0], + ) => (dispatch: Dispatch) => void; +} + +const mapDispatchToProps = ( + dispatch: ThunkDispatch, +): DispatchProps => ({ + updateDashboardVariables: bindActionCreators( + UpdateDashboardVariables, + dispatch, + ), +}); + +export default connect(null, mapDispatchToProps)(DashboardVariableSelection); diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/styles.ts b/frontend/src/container/NewDashboard/DashboardVariablesSelection/styles.ts new file mode 100644 index 0000000000..9b4b1d3322 --- /dev/null +++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/styles.ts @@ -0,0 +1,19 @@ +import { grey } from '@ant-design/colors'; +import { Typography } from 'antd'; +import styled from 'styled-components'; + +export const VariableContainer = styled.div` + border: 1px solid ${grey[1]}66; + border-radius: 2px; + padding: 0; + padding-left: 0.5rem; + display: flex; + align-items: center; + margin-bottom: 0.3rem; +`; + +export const VariableName = styled(Typography)` + font-size: 0.8rem; + font-style: italic; + color: ${grey[0]}; +`; diff --git a/frontend/src/container/NewDashboard/DescriptionOfDashboard/SettingsDrawer.tsx b/frontend/src/container/NewDashboard/DescriptionOfDashboard/SettingsDrawer.tsx new file mode 100644 index 0000000000..b8f6617204 --- /dev/null +++ b/frontend/src/container/NewDashboard/DescriptionOfDashboard/SettingsDrawer.tsx @@ -0,0 +1,37 @@ +import { SettingOutlined } from '@ant-design/icons'; +import { Button } from 'antd'; +import React, { useState } from 'react'; + +import DashboardSettingsContent from '../DashboardSettings'; +import { DrawerContainer } from './styles'; + +function SettingsDrawer(): JSX.Element { + const [visible, setVisible] = useState(false); // TODO Make it False + + const showDrawer = (): void => { + setVisible(true); + }; + + const onClose = (): void => { + setVisible(false); + }; + + return ( + <> + + + + + + ); +} + +export default SettingsDrawer; diff --git a/frontend/src/container/NewDashboard/DescriptionOfDashboard/index.tsx b/frontend/src/container/NewDashboard/DescriptionOfDashboard/index.tsx index bcaa553e62..c1a98cce8f 100644 --- a/frontend/src/container/NewDashboard/DescriptionOfDashboard/index.tsx +++ b/frontend/src/container/NewDashboard/DescriptionOfDashboard/index.tsx @@ -1,135 +1,69 @@ -import { - EditOutlined, - SaveOutlined, - ShareAltOutlined, -} from '@ant-design/icons'; -import { Card, Col, Row, Space, Tag, Typography } from 'antd'; -import AddTags from 'container/NewDashboard/DescriptionOfDashboard/AddTags'; -import NameOfTheDashboard from 'container/NewDashboard/DescriptionOfDashboard/NameOfTheDashboard'; +import { ShareAltOutlined } from '@ant-design/icons'; +import { Button, Card, Col, Row, Space, Tag, Typography } from 'antd'; import useComponentPermission from 'hooks/useComponentPermission'; -import React, { useCallback, useState } from 'react'; +import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { connect, useSelector } from 'react-redux'; -import { bindActionCreators, Dispatch } from 'redux'; -import { ThunkDispatch } from 'redux-thunk'; -import { - ToggleEditMode, - UpdateDashboardTitleDescriptionTags, - UpdateDashboardTitleDescriptionTagsProps, -} from 'store/actions'; +import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; -import AppActions from 'types/actions'; import AppReducer from 'types/reducer/app'; import DashboardReducer from 'types/reducer/dashboards'; -import Description from './Description'; +import DashboardVariableSelection from '../DashboardVariablesSelection'; +import SettingsDrawer from './SettingsDrawer'; import ShareModal from './ShareModal'; -import { Button, Container } from './styles'; -function DescriptionOfDashboard({ - updateDashboardTitleDescriptionTags, - toggleEditMode, -}: DescriptionOfDashboardProps): JSX.Element { - const { dashboards, isEditMode } = useSelector( +function DescriptionOfDashboard(): JSX.Element { + const { dashboards } = useSelector( (state) => state.dashboards, ); const [selectedDashboard] = dashboards; const selectedData = selectedDashboard.data; - const { title } = selectedData; - const { tags } = selectedData; - const { description } = selectedData; + const { title, tags, description } = selectedData; - const [updatedTitle, setUpdatedTitle] = useState(title); - const [updatedTags, setUpdatedTags] = useState(tags || []); - const [updatedDescription, setUpdatedDescription] = useState( - description || '', - ); const [isJSONModalVisible, isIsJSONModalVisible] = useState(false); const { t } = useTranslation('common'); const { role } = useSelector((state) => state.app); const [editDashboard] = useComponentPermission(['edit_dashboard'], role); - const onClickEditHandler = useCallback(() => { - if (isEditMode) { - const dashboard = selectedDashboard; - // @TODO need to update this function to take title,description,tags only - updateDashboardTitleDescriptionTags({ - dashboard: { - ...dashboard, - data: { - ...dashboard.data, - description: updatedDescription, - tags: updatedTags, - title: updatedTitle, - }, - }, - }); - } else { - toggleEditMode(); - } - }, [ - isEditMode, - updatedTitle, - updatedTags, - updatedDescription, - selectedDashboard, - toggleEditMode, - updateDashboardTitleDescriptionTags, - ]); - const onToggleHandler = (): void => { isIsJSONModalVisible((state) => !state); }; return ( - - {!isEditMode ? ( - - {title} - - {tags?.map((e) => ( - {e} - ))} - - - {description} - - - ) : ( - - - - - - )} - - - + + + + {title} + + {description} +
+ {tags?.map((e) => ( + {e} + ))} +
+ + + - - {editDashboard && ( - - )} @@ -137,23 +71,4 @@ function DescriptionOfDashboard({ ); } -interface DispatchProps { - updateDashboardTitleDescriptionTags: ( - props: UpdateDashboardTitleDescriptionTagsProps, - ) => (dispatch: Dispatch) => void; - toggleEditMode: () => void; -} - -const mapDispatchToProps = ( - dispatch: ThunkDispatch, -): DispatchProps => ({ - updateDashboardTitleDescriptionTags: bindActionCreators( - UpdateDashboardTitleDescriptionTags, - dispatch, - ), - toggleEditMode: bindActionCreators(ToggleEditMode, dispatch), -}); - -type DescriptionOfDashboardProps = DispatchProps; - -export default connect(null, mapDispatchToProps)(DescriptionOfDashboard); +export default DescriptionOfDashboard; diff --git a/frontend/src/container/NewDashboard/DescriptionOfDashboard/styles.ts b/frontend/src/container/NewDashboard/DescriptionOfDashboard/styles.ts index 74794e163c..43bcccef51 100644 --- a/frontend/src/container/NewDashboard/DescriptionOfDashboard/styles.ts +++ b/frontend/src/container/NewDashboard/DescriptionOfDashboard/styles.ts @@ -1,4 +1,4 @@ -import { Button as ButtonComponent } from 'antd'; +import { Button as ButtonComponent, Drawer } from 'antd'; import styled from 'styled-components'; export const Container = styled.div` @@ -11,3 +11,10 @@ export const Button = styled(ButtonComponent)` align-items: center; } `; + +export const DrawerContainer = styled(Drawer)` + .ant-drawer-header { + padding: 0; + border: none; + } +`; diff --git a/frontend/src/container/NewWidget/index.tsx b/frontend/src/container/NewWidget/index.tsx index 82627f8d22..eb38d54ae4 100644 --- a/frontend/src/container/NewWidget/index.tsx +++ b/frontend/src/container/NewWidget/index.tsx @@ -1,6 +1,7 @@ import { Button, Modal, Typography } from 'antd'; import ROUTES from 'constants/routes'; import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; +import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables'; import history from 'lib/history'; import { DashboardWidgetPageParams } from 'pages/DashboardWidget'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; @@ -143,6 +144,7 @@ function NewWidget({ widgetId: selectedWidget?.id || '', graphType: selectedGraph, globalSelectedInterval, + variables: getDashboardVariables(), }); } }, [ diff --git a/frontend/src/container/SideNav/Slack.tsx b/frontend/src/container/SideNav/Slack.tsx index c0abe5d3ba..f4f1e8e5c6 100644 --- a/frontend/src/container/SideNav/Slack.tsx +++ b/frontend/src/container/SideNav/Slack.tsx @@ -1,10 +1,14 @@ import React from 'react'; -function Slack(): JSX.Element { +interface ISlackProps { + width?: number; + height?: number; +} +function Slack({ width, height }: ISlackProps): JSX.Element { return ( ); } +Slack.defaultProps = { + width: 28, + height: 28, +}; export default Slack; diff --git a/frontend/src/container/SideNav/menuItems.ts b/frontend/src/container/SideNav/menuItems.ts index d3e400dedf..f811c333a3 100644 --- a/frontend/src/container/SideNav/menuItems.ts +++ b/frontend/src/container/SideNav/menuItems.ts @@ -62,7 +62,7 @@ const menus: SidebarMenu[] = [ { Icon: ApiOutlined, to: ROUTES.INSTRUMENTATION, - name: 'Add instrumentation', + name: 'Get Started', }, ]; diff --git a/frontend/src/container/TopNav/Breadcrumbs/index.tsx b/frontend/src/container/TopNav/Breadcrumbs/index.tsx index a79e5e3885..85cb0227a1 100644 --- a/frontend/src/container/TopNav/Breadcrumbs/index.tsx +++ b/frontend/src/container/TopNav/Breadcrumbs/index.tsx @@ -8,7 +8,7 @@ const breadcrumbNameMap = { [ROUTES.TRACE]: 'Traces', [ROUTES.SERVICE_MAP]: 'Service Map', [ROUTES.USAGE_EXPLORER]: 'Usage Explorer', - [ROUTES.INSTRUMENTATION]: 'Add instrumentation', + [ROUTES.INSTRUMENTATION]: 'Get Started', [ROUTES.SETTINGS]: 'Settings', [ROUTES.DASHBOARD]: 'Dashboard', [ROUTES.ALL_ERROR]: 'Exceptions', diff --git a/frontend/src/container/Trace/Filters/Panel/PanelBody/SearchTraceID/index.tsx b/frontend/src/container/Trace/Filters/Panel/PanelBody/SearchTraceID/index.tsx new file mode 100644 index 0000000000..d98bbf18fe --- /dev/null +++ b/frontend/src/container/Trace/Filters/Panel/PanelBody/SearchTraceID/index.tsx @@ -0,0 +1,127 @@ +import { Input, notification } from 'antd'; +import getFilters from 'api/trace/getFilters'; +import { AxiosError } from 'axios'; +import React, { useEffect, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { Dispatch } from 'redux'; +import { getFilter, updateURL } from 'store/actions/trace/util'; +import { AppState } from 'store/reducers'; +import AppActions from 'types/actions'; +import { UPDATE_ALL_FILTERS } from 'types/actions/trace'; +import { GlobalReducer } from 'types/reducer/globalTime'; +import { TraceReducer } from 'types/reducer/trace'; + +const { Search } = Input; + +function TraceID(): JSX.Element { + const { + selectedFilter, + filterToFetchData, + spansAggregate, + selectedTags, + userSelectedFilter, + isFilterExclude, + } = useSelector((state) => state.traces); + const dispatch = useDispatch>(); + const globalTime = useSelector( + (state) => state.globalTime, + ); + const [isLoading, setIsLoading] = useState(false); + const [userEnteredValue, setUserEnteredValue] = useState(''); + useEffect(() => { + setUserEnteredValue(selectedFilter.get('traceID')?.[0] || ''); + }, [selectedFilter]); + const onSearch = async (value: string): Promise => { + try { + setIsLoading(true); + const preSelectedFilter = new Map(selectedFilter); + const preUserSelected = new Map(userSelectedFilter); + + if (value !== '') { + preUserSelected.set('traceID', [value]); + preSelectedFilter.set('traceID', [value]); + } else { + preUserSelected.delete('traceID'); + preSelectedFilter.delete('traceID'); + } + const response = await getFilters({ + other: Object.fromEntries(preSelectedFilter), + end: String(globalTime.maxTime), + start: String(globalTime.minTime), + getFilters: filterToFetchData, + isFilterExclude, + }); + + if (response.statusCode === 200) { + const preFilter = getFilter(response.payload); + preFilter.set('traceID', { traceID: value }); + preFilter.forEach((value, key) => { + const values = Object.keys(value); + if (key !== 'duration' && values.length) { + preUserSelected.set(key, values); + } + }); + + dispatch({ + type: UPDATE_ALL_FILTERS, + payload: { + current: spansAggregate.currentPage, + filter: preFilter, + filterToFetchData, + selectedFilter: preSelectedFilter, + selectedTags, + userSelected: preUserSelected, + isFilterExclude, + order: spansAggregate.order, + pageSize: spansAggregate.pageSize, + orderParam: spansAggregate.orderParam, + }, + }); + + updateURL( + preSelectedFilter, + filterToFetchData, + spansAggregate.currentPage, + selectedTags, + isFilterExclude, + userSelectedFilter, + spansAggregate.order, + spansAggregate.pageSize, + spansAggregate.orderParam, + ); + } + } catch (error) { + notification.error({ + message: (error as AxiosError).toString() || 'Something went wrong', + }); + } finally { + setIsLoading(false); + } + }; + const onChange = (e: React.ChangeEvent): void => { + setUserEnteredValue(e.target.value); + }; + const onBlur = (): void => { + if (userEnteredValue !== selectedFilter.get('traceID')?.[0]) { + onSearch(userEnteredValue); + } + }; + return ( +
+ +
+ ); +} + +export default TraceID; diff --git a/frontend/src/container/Trace/Filters/Panel/PanelBody/index.tsx b/frontend/src/container/Trace/Filters/Panel/PanelBody/index.tsx index 5782a02f04..fda3599305 100644 --- a/frontend/src/container/Trace/Filters/Panel/PanelBody/index.tsx +++ b/frontend/src/container/Trace/Filters/Panel/PanelBody/index.tsx @@ -1,3 +1,4 @@ +/* eslint-disable no-nested-ternary */ import { Card } from 'antd'; import Spinner from 'components/Spinner'; import React from 'react'; @@ -7,6 +8,7 @@ import { TraceFilterEnum, TraceReducer } from 'types/reducer/trace'; import CommonCheckBox from './CommonCheckBox'; import Duration from './Duration'; +import TraceID from './SearchTraceID'; function PanelBody(props: PanelBodyProps): JSX.Element { const { type } = props; @@ -22,12 +24,17 @@ function PanelBody(props: PanelBodyProps): JSX.Element { ); } - - return ( - - {type === 'duration' ? : } - - ); + const renderBody = (type: TraceFilterEnum): JSX.Element => { + switch (type) { + case 'traceID': + return ; + case 'duration': + return ; + default: + return ; + } + }; + return {renderBody(type)}; } interface PanelBodyProps { diff --git a/frontend/src/container/Trace/Filters/index.tsx b/frontend/src/container/Trace/Filters/index.tsx index 95f73f4ed9..36edd206fe 100644 --- a/frontend/src/container/Trace/Filters/index.tsx +++ b/frontend/src/container/Trace/Filters/index.tsx @@ -16,6 +16,7 @@ export const AllTraceFilterEnum: TraceFilterEnum[] = [ 'httpMethod', 'httpRoute', 'httpUrl', + 'traceID', ]; function Filters(): JSX.Element { diff --git a/frontend/src/lib/dashbaordVariables/customCommaValuesParser.ts b/frontend/src/lib/dashbaordVariables/customCommaValuesParser.ts new file mode 100644 index 0000000000..01b5ccc2eb --- /dev/null +++ b/frontend/src/lib/dashbaordVariables/customCommaValuesParser.ts @@ -0,0 +1,20 @@ +export const commaValuesParser = (query: string): (string | number)[] => { + if (!query) { + return []; + } + const match = query.match(/(?:\\,|[^,])+/g) ?? []; + + const options: string[] = match.map((text) => { + // eslint-disable-next-line no-param-reassign + text = text.replace(/\\,/g, ','); + const textMatch = /^(.+)\s:\s(.+)$/g.exec(text) ?? []; + if (textMatch.length === 3) { + const [, , value] = textMatch; + return value.trim(); + } + return text.trim(); + }); + return options.map((option): string | number => + Number.isNaN(Number(option)) ? option : Number(option), + ); +}; diff --git a/frontend/src/lib/dashbaordVariables/getDashboardVariables.ts b/frontend/src/lib/dashbaordVariables/getDashboardVariables.ts new file mode 100644 index 0000000000..1f5a66a2a2 --- /dev/null +++ b/frontend/src/lib/dashbaordVariables/getDashboardVariables.ts @@ -0,0 +1,38 @@ +import GetMinMax from 'lib/getMinMax'; +import GetStartAndEndTime from 'lib/getStartAndEndTime'; +import store from 'store'; + +export const getDashboardVariables = (): Record => { + try { + const { + globalTime, + dashboards: { dashboards }, + } = store.getState(); + const [selectedDashboard] = dashboards; + const { + data: { variables }, + } = selectedDashboard; + + const minMax = GetMinMax(globalTime.selectedTime, [ + globalTime.minTime / 1000000, + globalTime.maxTime / 1000000, + ]); + + const { start, end } = GetStartAndEndTime({ + type: 'GLOBAL_TIME', + minTime: minMax.minTime, + maxTime: minMax.maxTime, + }); + const variablesTuple: Record = { + SIGNOZ_START_TIME: parseInt(start, 10) * 1e3, + SIGNOZ_END_TIME: parseInt(end, 10) * 1e3, + }; + Object.keys(variables).forEach((key) => { + variablesTuple[key] = variables[key].selectedValue; + }); + return variablesTuple; + } catch (e) { + console.error(e); + } + return {}; +}; diff --git a/frontend/src/lib/dashbaordVariables/sortVariableValues.ts b/frontend/src/lib/dashbaordVariables/sortVariableValues.ts new file mode 100644 index 0000000000..0937f70412 --- /dev/null +++ b/frontend/src/lib/dashbaordVariables/sortVariableValues.ts @@ -0,0 +1,15 @@ +import { sortBy } from 'lodash-es'; +import { TSortVariableValuesType } from 'types/api/dashboard/getAll'; + +type TValuesDataType = (string | number | boolean)[]; +const sortValues = ( + values: TValuesDataType, + sortType: TSortVariableValuesType, +): TValuesDataType => { + if (sortType === 'ASC') return sortBy(values); + if (sortType === 'DESC') return sortBy(values).reverse(); + + return values; +}; + +export default sortValues; diff --git a/frontend/src/lib/getChartData.ts b/frontend/src/lib/getChartData.ts index c3eefb0055..9f5c0c1a61 100644 --- a/frontend/src/lib/getChartData.ts +++ b/frontend/src/lib/getChartData.ts @@ -6,6 +6,16 @@ import convertIntoEpoc from './covertIntoEpoc'; import { colors } from './getRandomColor'; const getChartData = ({ queryData }: GetChartDataProps): ChartData => { + const uniqueTimeLabels = new Set(); + queryData.forEach((data) => { + data.queryData.forEach((query) => { + query.values.forEach((value) => { + uniqueTimeLabels.add(value[0]); + }); + }); + }); + const labels = Array.from(uniqueTimeLabels).sort((a, b) => a - b); + const response = queryData.map( ({ queryData, query: queryG, legend: legendG }) => { return queryData.map((e) => { @@ -22,11 +32,24 @@ const getChartData = ({ queryData }: GetChartDataProps): ChartData => { second: Number(parseFloat(second)), }; }); + // Fill the missing data with null + const filledDataValues = Array.from(labels).map((e) => { + const td1 = new Date(parseInt(convertIntoEpoc(e * 1000), 10)); + const data = dataValue.find((e1) => { + return e1.first.getTime() === td1.getTime(); + }); + return ( + data || { + first: new Date(parseInt(convertIntoEpoc(e * 1000), 10)), + second: null, + } + ); + }); return { label: labelNames !== 'undefined' ? labelNames : '', - first: dataValue.map((e) => e.first), - second: dataValue.map((e) => e.second), + first: filledDataValues.map((e) => e.first), + second: filledDataValues.map((e) => e.second), }; }); }, diff --git a/frontend/src/pages/AddInstrumentation/index.tsx b/frontend/src/pages/AddInstrumentation/index.tsx deleted file mode 100644 index f4adc07b2c..0000000000 --- a/frontend/src/pages/AddInstrumentation/index.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { Typography } from 'antd'; -import React from 'react'; -import { useSelector } from 'react-redux'; -import { AppState } from 'store/reducers'; -import AppReducer from 'types/reducer/app'; - -import { Container, Heading } from './styles'; - -function InstrumentationPage(): JSX.Element { - const { isDarkMode } = useSelector((state) => state.app); - - return ( - <> - Instrument your application - - Congrats, you have successfully installed SigNoz!{' '} - - To start seeing YOUR application data here, follow the instructions in the - docs - - - - https://signoz.io/docs/instrumentation/overview - -  If you face any issues, join our - -  slack community  - - to ask any questions or mail us at  - - support@signoz.io - - - - ); -} - -export default InstrumentationPage; diff --git a/frontend/src/pages/GettingStarted/DocCard.tsx b/frontend/src/pages/GettingStarted/DocCard.tsx new file mode 100644 index 0000000000..38bf63c16d --- /dev/null +++ b/frontend/src/pages/GettingStarted/DocCard.tsx @@ -0,0 +1,30 @@ +import { Typography } from 'antd'; +import React from 'react'; +import { useSelector } from 'react-redux'; +import { Link } from 'react-router-dom'; +import { AppState } from 'store/reducers'; +import AppReducer from 'types/reducer/app'; + +import { DocCardContainer } from './styles'; +import { TGetStartedContentDoc } from './types'; +import UTMParams from './utmParams'; + +interface IDocCardProps { + text: TGetStartedContentDoc['title']; + icon: TGetStartedContentDoc['icon']; + url: TGetStartedContentDoc['url']; +} +function DocCard({ icon, text, url }: IDocCardProps): JSX.Element { + const { isDarkMode } = useSelector((state) => state.app); + + return ( + + + {icon} + {text} + + + ); +} + +export default DocCard; diff --git a/frontend/src/pages/GettingStarted/Section.tsx b/frontend/src/pages/GettingStarted/Section.tsx new file mode 100644 index 0000000000..4a90b746de --- /dev/null +++ b/frontend/src/pages/GettingStarted/Section.tsx @@ -0,0 +1,43 @@ +import { Col, Row, Typography } from 'antd'; +import { map } from 'lodash-es'; +import React from 'react'; + +import DocCard from './DocCard'; +import { TGetStartedContentSection } from './types'; + +interface IDocSectionProps { + sectionData: TGetStartedContentSection; +} + +function DocSection({ sectionData }: IDocSectionProps): JSX.Element { + return ( +
+ {sectionData.heading} + + {sectionData.description && ( +
+ {sectionData.description} + + )} + {map(sectionData.items, (item, idx) => ( + + + + ))} + + + ); +} + +export default DocSection; diff --git a/frontend/src/pages/GettingStarted/index.tsx b/frontend/src/pages/GettingStarted/index.tsx new file mode 100644 index 0000000000..5703239b51 --- /dev/null +++ b/frontend/src/pages/GettingStarted/index.tsx @@ -0,0 +1,21 @@ +import { Typography } from 'antd'; +import React from 'react'; + +import { GetStartedContent } from './renderConfig'; +import DocSection from './Section'; + +function InstrumentationPage(): JSX.Element { + return ( + <> + + Congrats, you have successfully installed SigNoz! Now lets get some data in + and start deriving insights from them + + {GetStartedContent().map((section) => { + return ; + })} + + ); +} + +export default InstrumentationPage; diff --git a/frontend/src/pages/GettingStarted/renderConfig.tsx b/frontend/src/pages/GettingStarted/renderConfig.tsx new file mode 100644 index 0000000000..86aa6b5ed7 --- /dev/null +++ b/frontend/src/pages/GettingStarted/renderConfig.tsx @@ -0,0 +1,175 @@ +import { + AlertFilled, + AlignLeftOutlined, + ApiFilled, + BarChartOutlined, + DashboardFilled, + SoundFilled, +} from '@ant-design/icons'; +import { Typography } from 'antd'; +import Slack from 'container/SideNav/Slack'; +import React from 'react'; +import store from 'store'; + +import { TGetStartedContentSection } from './types'; + +export const GetStartedContent = (): TGetStartedContentSection[] => { + const { + app: { currentVersion }, + } = store.getState(); + return [ + { + heading: 'Send data from your applications to SigNoz', + items: [ + { + title: 'Instrument your Java Application', + icon: ( + + ), + url: 'https://signoz.io/docs/instrumentation/java/', + }, + { + title: 'Instrument your Python Application', + icon: ( + + ), + url: 'https://signoz.io/docs/instrumentation/python/', + }, + { + title: 'Instrument your JS Application', + icon: ( + + ), + url: 'https://signoz.io/docs/instrumentation/javascript/', + }, + { + title: 'Instrument your Go Application', + icon: ( + + ), + url: 'https://signoz.io/docs/instrumentation/golang/', + }, + { + title: 'Instrument your .NET Application', + icon: ( + + ), + url: 'https://signoz.io/docs/instrumentation/dotnet/', + }, + { + title: 'Instrument your PHP Application', + icon: ( + + ), + url: 'https://signoz.io/docs/instrumentation/php/', + }, + { + title: 'Instrument your Rails Application', + icon: ( + + ), + url: 'https://signoz.io/docs/instrumentation/ruby-on-rails/', + }, + { + title: 'Instrument your Rust Application', + icon: ( + + ), + url: 'https://signoz.io/docs/instrumentation/rust/', + }, + { + title: 'Instrument your Elixir Application', + icon: ( + + ), + url: 'https://signoz.io/docs/instrumentation/elixir/', + }, + ], + }, + { + heading: 'Send Metrics from your Infrastructure & create Dashboards', + items: [ + { + title: 'Send metrics to SigNoz', + icon: , + url: 'https://signoz.io/docs/userguide/send-metrics/', + }, + { + title: 'Create and Manage Dashboards', + icon: , + url: 'https://signoz.io/docs/userguide/manage-dashboards-and-panels/', + }, + ], + }, + { + heading: 'Send your logs to SigNoz', + items: [ + { + title: 'Send your logs to SigNoz', + icon: , + url: 'https://signoz.io/docs/userguide/logs/', + }, + { + title: 'Existing log collectors to SigNoz', + icon: , + url: 'https://signoz.io/docs/userguide/fluentbit_to_signoz/', + }, + ], + }, + { + heading: 'Create alerts on Metrics', + items: [ + { + title: 'Create alert rules on metrics', + icon: , + url: 'https://signoz.io/docs/userguide/alerts-management/', + }, + { + title: 'Configure alert notification channels', + icon: , + url: + 'https://signoz.io/docs/userguide/alerts-management/#setting-up-a-notification-channel', + }, + ], + }, + { + heading: 'Need help?', + description: ( + <> + {'Join our slack community and ask any question you may have on '} + + #support + + {' or '} + + #general + + + ), + + items: [ + { + title: 'Join SigNoz slack community ', + icon: ( +
+ +
+ ), + url: 'https://signoz.io/slack', + }, + ], + }, + ]; +}; diff --git a/frontend/src/pages/AddInstrumentation/styles.ts b/frontend/src/pages/GettingStarted/styles.ts similarity index 54% rename from frontend/src/pages/AddInstrumentation/styles.ts rename to frontend/src/pages/GettingStarted/styles.ts index af4a0bfb29..6e1ec7ad44 100644 --- a/frontend/src/pages/AddInstrumentation/styles.ts +++ b/frontend/src/pages/GettingStarted/styles.ts @@ -1,4 +1,4 @@ -import { Card, Typography } from 'antd'; +import { Card, Row, Typography } from 'antd'; import styled from 'styled-components'; interface Props { @@ -18,3 +18,13 @@ export const Heading = styled(Typography)` margin-bottom: 1rem; } `; + +export const DocCardContainer = styled(Row)<{ + isDarkMode: boolean; +}>` + display: flex; + border: 1px solid ${({ isDarkMode }): string => (isDarkMode ? '#444' : '#ccc')}; + border-radius: 0.2rem; + align-items: center; + padding: 0.5rem 0.25rem; +`; diff --git a/frontend/src/pages/GettingStarted/types.ts b/frontend/src/pages/GettingStarted/types.ts new file mode 100644 index 0000000000..b544254038 --- /dev/null +++ b/frontend/src/pages/GettingStarted/types.ts @@ -0,0 +1,10 @@ +export type TGetStartedContentDoc = { + title: string; + icon: JSX.Element; + url: string; +}; +export type TGetStartedContentSection = { + heading: string; + description?: string | JSX.Element; + items: TGetStartedContentDoc[]; +}; diff --git a/frontend/src/pages/GettingStarted/utmParams.ts b/frontend/src/pages/GettingStarted/utmParams.ts new file mode 100644 index 0000000000..1aa01f503b --- /dev/null +++ b/frontend/src/pages/GettingStarted/utmParams.ts @@ -0,0 +1,3 @@ +const UTMParams = + '?utm_source=instrumentation_page&utm_medium=frontend&utm_term=language'; +export default UTMParams; diff --git a/frontend/src/store/actions/dashboard/deleteWidget.ts b/frontend/src/store/actions/dashboard/deleteWidget.ts index 3aea4902b8..8509cdbc46 100644 --- a/frontend/src/store/actions/dashboard/deleteWidget.ts +++ b/frontend/src/store/actions/dashboard/deleteWidget.ts @@ -30,6 +30,7 @@ export const DeleteWidget = ({ tags: selectedDashboard.data.tags, widgets: updatedWidgets, layout: updatedLayout, + variables: selectedDashboard.data.variables, }, uuid: selectedDashboard.uuid, }; diff --git a/frontend/src/store/actions/dashboard/getQueryResults.ts b/frontend/src/store/actions/dashboard/getQueryResults.ts index 88cd3bbf07..1090c57bea 100644 --- a/frontend/src/store/actions/dashboard/getQueryResults.ts +++ b/frontend/src/store/actions/dashboard/getQueryResults.ts @@ -19,7 +19,7 @@ import { Dispatch } from 'redux'; import store from 'store'; import AppActions from 'types/actions'; import { ErrorResponse, SuccessResponse } from 'types/api'; -import { Query } from 'types/api/dashboard/getAll'; +import { IDashboardVariable, Query } from 'types/api/dashboard/getAll'; import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; import { EDataSource, EPanelType, EQueryType } from 'types/common/dashboard'; import { GlobalReducer } from 'types/reducer/globalTime'; @@ -29,11 +29,13 @@ export async function GetMetricQueryRange({ globalSelectedInterval, graphType, selectedTime, + variables = {}, }: { query: Query; graphType: GRAPH_TYPES; selectedTime: timePreferenceType; globalSelectedInterval: Time; + variables?: Record; }): Promise | ErrorResponse> { const { queryType } = query; const queryKey: Record = @@ -138,6 +140,7 @@ export async function GetMetricQueryRange({ start: parseInt(start, 10) * 1e3, end: parseInt(end, 10) * 1e3, step: getStep({ start, end, inputFormat: 'ms' }), + variables, ...QueryPayload, }); if (response.statusCode >= 400) { @@ -173,6 +176,14 @@ export const GetQueryResults = ( ): ((dispatch: Dispatch) => void) => { return async (dispatch: Dispatch): Promise => { try { + dispatch({ + type: 'QUERY_ERROR', + payload: { + errorMessage: '', + widgetId: props.widgetId, + errorBoolean: false, + }, + }); const response = await GetMetricQueryRange(props); const isError = response.error; @@ -199,14 +210,6 @@ export const GetQueryResults = ( }, }, }); - dispatch({ - type: 'QUERY_ERROR', - payload: { - errorMessage: '', - widgetId: props.widgetId, - errorBoolean: false, - }, - }); } catch (error) { dispatch({ type: 'QUERY_ERROR', @@ -226,4 +229,5 @@ export interface GetQueryResultsProps { query: Query; graphType: ITEMS; globalSelectedInterval: GlobalReducer['selectedTime']; + variables: Record; } diff --git a/frontend/src/store/actions/dashboard/updatedDashboardVariables.ts b/frontend/src/store/actions/dashboard/updatedDashboardVariables.ts new file mode 100644 index 0000000000..20bbbd0a06 --- /dev/null +++ b/frontend/src/store/actions/dashboard/updatedDashboardVariables.ts @@ -0,0 +1,38 @@ +import { notification } from 'antd'; +import update from 'api/dashboard/update'; +import { Dispatch } from 'redux'; +import store from 'store/index'; +import AppActions from 'types/actions'; +import { UPDATE_DASHBOARD_VARIABLES } from 'types/actions/dashboard'; +import { IDashboardVariable } from 'types/api/dashboard/getAll'; + +export const UpdateDashboardVariables = ( + variables: Record, +): ((dispatch: Dispatch) => void) => { + return async (dispatch: Dispatch): Promise => { + try { + dispatch({ + type: UPDATE_DASHBOARD_VARIABLES, + payload: variables, + }); + + const reduxStoreState = store.getState(); + const [dashboard] = reduxStoreState.dashboards.dashboards; + + const response = await update({ + data: { + ...dashboard.data, + }, + uuid: dashboard.uuid, + }); + + if (response.statusCode !== 200) { + notification.error({ + message: response.error, + }); + } + } catch (error) { + console.error(error); + } + }; +}; diff --git a/frontend/src/store/reducers/dashboard.ts b/frontend/src/store/reducers/dashboard.ts index 61c5cc7351..0ef474ea32 100644 --- a/frontend/src/store/reducers/dashboard.ts +++ b/frontend/src/store/reducers/dashboard.ts @@ -18,6 +18,7 @@ import { SAVE_SETTING_TO_PANEL_SUCCESS, TOGGLE_EDIT_MODE, UPDATE_DASHBOARD, + UPDATE_DASHBOARD_VARIABLES, UPDATE_QUERY, UPDATE_TITLE_DESCRIPTION_TAGS_SUCCESS, } from 'types/actions/dashboard'; @@ -170,7 +171,6 @@ const dashboard = ( case QUERY_ERROR: { const { widgetId, errorMessage, errorBoolean = true } = action.payload; - const [selectedDashboard] = state.dashboards; const { data } = selectedDashboard; @@ -397,7 +397,25 @@ const dashboard = ( ], }; } + case UPDATE_DASHBOARD_VARIABLES: { + const variablesData = action.payload; + const { dashboards } = state; + const [selectedDashboard] = dashboards; + const { data } = selectedDashboard; + return { + ...state, + dashboards: [ + { + ...selectedDashboard, + data: { + ...data, + variables: variablesData, + }, + }, + ], + }; + } default: return state; } diff --git a/frontend/src/store/reducers/trace.ts b/frontend/src/store/reducers/trace.ts index 9b2b837478..ea6b9a7671 100644 --- a/frontend/src/store/reducers/trace.ts +++ b/frontend/src/store/reducers/trace.ts @@ -68,6 +68,7 @@ const initialValue: TraceReducer = { ['responseStatusCode', INITIAL_FILTER_VALUE], ['serviceName', INITIAL_FILTER_VALUE], ['status', INITIAL_FILTER_VALUE], + ['traceID', INITIAL_FILTER_VALUE], ]), }; diff --git a/frontend/src/types/actions/dashboard.ts b/frontend/src/types/actions/dashboard.ts index 3330f028d4..4745771cc0 100644 --- a/frontend/src/types/actions/dashboard.ts +++ b/frontend/src/types/actions/dashboard.ts @@ -1,6 +1,11 @@ import { Layout } from 'react-grid-layout'; import { ApplySettingsToPanelProps } from 'store/actions/dashboard/applySettingsToPanel'; -import { Dashboard, Query, Widgets } from 'types/api/dashboard/getAll'; +import { + Dashboard, + IDashboardVariable, + Query, + Widgets, +} from 'types/api/dashboard/getAll'; import { QueryData } from 'types/api/widgets/getQuery'; export const GET_DASHBOARD = 'GET_DASHBOARD'; @@ -42,6 +47,8 @@ export const IS_ADD_WIDGET = 'IS_ADD_WIDGET'; export const DELETE_QUERY = 'DELETE_QUERY'; export const FLUSH_DASHBOARD = 'FLUSH_DASHBOARD'; +export const UPDATE_DASHBOARD_VARIABLES = 'UPDATE_DASHBOARD_VARIABLES'; + interface GetDashboard { type: typeof GET_DASHBOARD; payload: Dashboard; @@ -174,6 +181,10 @@ interface DeleteQuery { interface FlushDashboard { type: typeof FLUSH_DASHBOARD; } +interface UpdateDashboardVariables { + type: typeof UPDATE_DASHBOARD_VARIABLES; + payload: Record; +} export type DashboardActions = | GetDashboard @@ -194,4 +205,5 @@ export type DashboardActions = | IsAddWidget | UpdateQuery | DeleteQuery - | FlushDashboard; + | FlushDashboard + | UpdateDashboardVariables; diff --git a/frontend/src/types/api/dashboard/getAll.ts b/frontend/src/types/api/dashboard/getAll.ts index c158bff826..2a2617bb99 100644 --- a/frontend/src/types/api/dashboard/getAll.ts +++ b/frontend/src/types/api/dashboard/getAll.ts @@ -11,6 +11,31 @@ import { QueryData } from '../widgets/getQuery'; export type PayloadProps = Dashboard[]; +export const VariableQueryTypeArr = ['QUERY', 'TEXTBOX', 'CUSTOM'] as const; +export type TVariableQueryType = typeof VariableQueryTypeArr[number]; + +export const VariableSortTypeArr = ['DISABLED', 'ASC', 'DESC'] as const; +export type TSortVariableValuesType = typeof VariableSortTypeArr[number]; + +export interface IDashboardVariable { + name?: string; // key will be the source of truth + description: string; + type: TVariableQueryType; + // Query + queryValue?: string; + // Custom + customValue?: string; + // Textbox + textboxValue?: string; + + sort: TSortVariableValuesType; + multiSelect: boolean; + showALLOption: boolean; + selectedValue?: null | string | string[]; + // Internal use + modificationUUID?: string; + allSelected?: boolean; +} export interface Dashboard { id: number; uuid: string; @@ -26,6 +51,7 @@ export interface DashboardData { widgets?: Widgets[]; title: string; layout?: Layout[]; + variables: Record; } export interface IBaseWidget { diff --git a/frontend/src/types/api/dashboard/variables/query.ts b/frontend/src/types/api/dashboard/variables/query.ts new file mode 100644 index 0000000000..e715378c66 --- /dev/null +++ b/frontend/src/types/api/dashboard/variables/query.ts @@ -0,0 +1,7 @@ +export type Props = { + query: string; +}; + +export type PayloadProps = { + variableValues: string[] | number[]; +}; diff --git a/frontend/src/types/api/metrics/getQueryRange.ts b/frontend/src/types/api/metrics/getQueryRange.ts index da28970542..bbe9c697a9 100644 --- a/frontend/src/types/api/metrics/getQueryRange.ts +++ b/frontend/src/types/api/metrics/getQueryRange.ts @@ -5,5 +5,6 @@ export interface MetricRangePayloadProps { data: { result: QueryData[]; resultType: string; + variables: Record; }; } diff --git a/frontend/src/types/reducer/trace.ts b/frontend/src/types/reducer/trace.ts index fed82dd0be..fa37924f5e 100644 --- a/frontend/src/types/reducer/trace.ts +++ b/frontend/src/types/reducer/trace.ts @@ -71,7 +71,8 @@ export type TraceFilterEnum = | 'serviceName' | 'status' | 'responseStatusCode' - | 'rpcMethod'; + | 'rpcMethod' + | 'traceID'; export const AllPanelHeading: { key: TraceFilterEnum; @@ -125,4 +126,8 @@ export const AllPanelHeading: { key: 'status', displayValue: 'Status', }, + { + key: 'traceID', + displayValue: 'Trace ID', + }, ]; diff --git a/pkg/query-service/app/clickhouseReader/reader.go b/pkg/query-service/app/clickhouseReader/reader.go index 464ba3e272..b6bcc7b55e 100644 --- a/pkg/query-service/app/clickhouseReader/reader.go +++ b/pkg/query-service/app/clickhouseReader/reader.go @@ -94,6 +94,7 @@ type ClickHouseReader struct { logsResourceKeys string queryEngine *promql.Engine remoteStorage *remote.Storage + fanoutStorage *storage.Storage promConfigFile string promConfig *config.Config @@ -143,7 +144,7 @@ func NewReader(localDB *sqlx.DB, configFile string) *ClickHouseReader { } } -func (r *ClickHouseReader) Start() { +func (r *ClickHouseReader) Start(readerReady chan bool) { logLevel := promlog.AllowedLevel{} logLevel.Set("debug") // allowedFormat := promlog.AllowedFormat{} @@ -311,6 +312,8 @@ func (r *ClickHouseReader) Start() { } r.queryEngine = queryEngine r.remoteStorage = remoteStorage + r.fanoutStorage = &fanoutStorage + readerReady <- true if err := g.Run(); err != nil { level.Error(logger).Log("err", err) @@ -319,6 +322,14 @@ func (r *ClickHouseReader) Start() { } +func (r *ClickHouseReader) GetQueryEngine() *promql.Engine { + return r.queryEngine +} + +func (r *ClickHouseReader) GetFanoutStorage() *storage.Storage { + return r.fanoutStorage +} + func reloadConfig(filename string, logger log.Logger, rls ...func(*config.Config) error) (promConfig *config.Config, err error) { level.Info(logger).Log("msg", "Loading configuration file", "filename", filename) @@ -925,6 +936,9 @@ func (r *ClickHouseReader) GetSpanFilters(ctx context.Context, queryParams *mode } args := []interface{}{clickhouse.Named("timestampL", strconv.FormatInt(queryParams.Start.UnixNano(), 10)), clickhouse.Named("timestampU", strconv.FormatInt(queryParams.End.UnixNano(), 10))} + if len(queryParams.TraceID) > 0 { + args = buildFilterArrayQuery(ctx, excludeMap, queryParams.TraceID, constants.TraceID, &query, args) + } if len(queryParams.ServiceName) > 0 { args = buildFilterArrayQuery(ctx, excludeMap, queryParams.ServiceName, constants.ServiceName, &query, args) } @@ -984,6 +998,8 @@ func (r *ClickHouseReader) GetSpanFilters(ctx context.Context, queryParams *mode for _, e := range queryParams.GetFilters { switch e { + case constants.TraceID: + continue case constants.ServiceName: finalQuery := fmt.Sprintf("SELECT serviceName, count() as count FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", r.traceDB, r.indexTable) finalQuery += query @@ -1260,6 +1276,9 @@ func (r *ClickHouseReader) GetFilteredSpans(ctx context.Context, queryParams *mo var query string args := []interface{}{clickhouse.Named("timestampL", strconv.FormatInt(queryParams.Start.UnixNano(), 10)), clickhouse.Named("timestampU", strconv.FormatInt(queryParams.End.UnixNano(), 10))} + if len(queryParams.TraceID) > 0 { + args = buildFilterArrayQuery(ctx, excludeMap, queryParams.TraceID, constants.TraceID, &query, args) + } if len(queryParams.ServiceName) > 0 { args = buildFilterArrayQuery(ctx, excludeMap, queryParams.ServiceName, constants.ServiceName, &query, args) } @@ -1450,6 +1469,9 @@ func (r *ClickHouseReader) GetTagFilters(ctx context.Context, queryParams *model var query string args := []interface{}{clickhouse.Named("timestampL", strconv.FormatInt(queryParams.Start.UnixNano(), 10)), clickhouse.Named("timestampU", strconv.FormatInt(queryParams.End.UnixNano(), 10))} + if len(queryParams.TraceID) > 0 { + args = buildFilterArrayQuery(ctx, excludeMap, queryParams.TraceID, constants.TraceID, &query, args) + } if len(queryParams.ServiceName) > 0 { args = buildFilterArrayQuery(ctx, excludeMap, queryParams.ServiceName, constants.ServiceName, &query, args) } @@ -1546,6 +1568,9 @@ func (r *ClickHouseReader) GetTagValues(ctx context.Context, queryParams *model. var query string args := []interface{}{clickhouse.Named("timestampL", strconv.FormatInt(queryParams.Start.UnixNano(), 10)), clickhouse.Named("timestampU", strconv.FormatInt(queryParams.End.UnixNano(), 10))} + if len(queryParams.TraceID) > 0 { + args = buildFilterArrayQuery(ctx, excludeMap, queryParams.TraceID, constants.TraceID, &query, args) + } if len(queryParams.ServiceName) > 0 { args = buildFilterArrayQuery(ctx, excludeMap, queryParams.ServiceName, constants.ServiceName, &query, args) } @@ -1853,6 +1878,9 @@ func (r *ClickHouseReader) GetFilteredSpansAggregates(ctx context.Context, query query = fmt.Sprintf("SELECT toStartOfInterval(timestamp, INTERVAL %d minute) as time, %s FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", queryParams.StepSeconds/60, aggregation_query, r.traceDB, r.indexTable) } + if len(queryParams.TraceID) > 0 { + args = buildFilterArrayQuery(ctx, excludeMap, queryParams.TraceID, constants.TraceID, &query, args) + } if len(queryParams.ServiceName) > 0 { args = buildFilterArrayQuery(ctx, excludeMap, queryParams.ServiceName, constants.ServiceName, &query, args) } @@ -2813,7 +2841,7 @@ func (r *ClickHouseReader) GetMetricResult(ctx context.Context, query string) ([ if err != nil { zap.S().Debug("Error in processing query: ", err) - return nil, fmt.Errorf("error in processing query") + return nil, err } var ( @@ -3239,3 +3267,39 @@ func (r *ClickHouseReader) AggregateLogs(ctx context.Context, params *model.Logs return &aggregateResponse, nil } + +func (r *ClickHouseReader) QueryDashboardVars(ctx context.Context, query string) (*model.DashboardVar, error) { + var result model.DashboardVar + rows, err := r.db.Query(ctx, query) + + zap.S().Info(query) + + if err != nil { + zap.S().Debug("Error in processing sql query: ", err) + return nil, err + } + + var ( + columnTypes = rows.ColumnTypes() + vars = make([]interface{}, len(columnTypes)) + ) + for i := range columnTypes { + vars[i] = reflect.New(columnTypes[i].ScanType()).Interface() + } + + defer rows.Close() + for rows.Next() { + if err := rows.Scan(vars...); err != nil { + return nil, err + } + for _, v := range vars { + switch v := v.(type) { + case *string, *int8, *int16, *int32, *int64, *uint8, *uint16, *uint32, *uint64, *float32, *float64, *time.Time, *bool: + result.VariableValues = append(result.VariableValues, reflect.ValueOf(v).Elem().Interface()) + default: + return nil, fmt.Errorf("unsupported value type encountered") + } + } + } + return &result, nil +} diff --git a/pkg/query-service/app/http_handler.go b/pkg/query-service/app/http_handler.go index 0a63e38c6b..4ee4702028 100644 --- a/pkg/query-service/app/http_handler.go +++ b/pkg/query-service/app/http_handler.go @@ -9,7 +9,9 @@ import ( "io/ioutil" "net/http" "strconv" + "strings" "sync" + "text/template" "time" "github.com/gorilla/mux" @@ -320,6 +322,7 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router) { router.HandleFunc("/api/v1/dashboards/{uuid}", ViewAccess(aH.getDashboard)).Methods(http.MethodGet) router.HandleFunc("/api/v1/dashboards/{uuid}", EditAccess(aH.updateDashboard)).Methods(http.MethodPut) router.HandleFunc("/api/v1/dashboards/{uuid}", EditAccess(aH.deleteDashboard)).Methods(http.MethodDelete) + router.HandleFunc("/api/v1/variables/query", ViewAccess(aH.queryDashboardVars)).Methods(http.MethodGet) router.HandleFunc("/api/v1/feedback", OpenAccess(aH.submitFeedback)).Methods(http.MethodPost) // router.HandleFunc("/api/v1/get_percentiles", aH.getApplicationPercentiles).Methods(http.MethodGet) @@ -483,9 +486,11 @@ func (aH *APIHandler) queryRangeMetricsV2(w http.ResponseWriter, r *http.Request type channelResult struct { Series []*model.Series Err error + Name string + Query string } - execClickHouseQueries := func(queries map[string]string) ([]*model.Series, error) { + execClickHouseQueries := func(queries map[string]string) ([]*model.Series, error, map[string]string) { var seriesList []*model.Series ch := make(chan channelResult, len(queries)) var wg sync.WaitGroup @@ -500,7 +505,7 @@ func (aH *APIHandler) queryRangeMetricsV2(w http.ResponseWriter, r *http.Request } if err != nil { - ch <- channelResult{Err: fmt.Errorf("error in query-%s: %v", name, err)} + ch <- channelResult{Err: fmt.Errorf("error in query-%s: %v", name, err), Name: name, Query: query} return } ch <- channelResult{Series: seriesList} @@ -511,21 +516,23 @@ func (aH *APIHandler) queryRangeMetricsV2(w http.ResponseWriter, r *http.Request close(ch) var errs []error + errQuriesByName := make(map[string]string) // read values from the channel for r := range ch { if r.Err != nil { errs = append(errs, r.Err) + errQuriesByName[r.Name] = r.Query continue } seriesList = append(seriesList, r.Series...) } if len(errs) != 0 { - return nil, fmt.Errorf("encountered multiple errors: %s", metrics.FormatErrs(errs, "\n")) + return nil, fmt.Errorf("encountered multiple errors: %s", metrics.FormatErrs(errs, "\n")), errQuriesByName } - return seriesList, nil + return seriesList, nil, nil } - execPromQueries := func(metricsQueryRangeParams *model.QueryRangeParamsV2) ([]*model.Series, error) { + execPromQueries := func(metricsQueryRangeParams *model.QueryRangeParamsV2) ([]*model.Series, error, map[string]string) { var seriesList []*model.Series ch := make(chan channelResult, len(metricsQueryRangeParams.CompositeMetricQuery.PromQueries)) var wg sync.WaitGroup @@ -538,6 +545,19 @@ func (aH *APIHandler) queryRangeMetricsV2(w http.ResponseWriter, r *http.Request go func(name string, query *model.PromQuery) { var seriesList []*model.Series defer wg.Done() + tmpl := template.New("promql-query") + tmpl, tmplErr := tmpl.Parse(query.Query) + if tmplErr != nil { + ch <- channelResult{Err: fmt.Errorf("error in parsing query-%s: %v", name, tmplErr), Name: name, Query: query.Query} + return + } + var queryBuf bytes.Buffer + tmplErr = tmpl.Execute(&queryBuf, metricsQueryRangeParams.Variables) + if tmplErr != nil { + ch <- channelResult{Err: fmt.Errorf("error in parsing query-%s: %v", name, tmplErr), Name: name, Query: query.Query} + return + } + query.Query = queryBuf.String() queryModel := model.QueryRangeParams{ Start: time.UnixMilli(metricsQueryRangeParams.Start), End: time.UnixMilli(metricsQueryRangeParams.End), @@ -546,7 +566,7 @@ func (aH *APIHandler) queryRangeMetricsV2(w http.ResponseWriter, r *http.Request } promResult, _, err := (*aH.reader).GetQueryRangeResult(r.Context(), &queryModel) if err != nil { - ch <- channelResult{Err: fmt.Errorf("error in query-%s: %v", name, err)} + ch <- channelResult{Err: fmt.Errorf("error in query-%s: %v", name, err), Name: name, Query: query.Query} return } matrix, _ := promResult.Matrix() @@ -567,22 +587,25 @@ func (aH *APIHandler) queryRangeMetricsV2(w http.ResponseWriter, r *http.Request close(ch) var errs []error + errQuriesByName := make(map[string]string) // read values from the channel for r := range ch { if r.Err != nil { errs = append(errs, r.Err) + errQuriesByName[r.Name] = r.Query continue } seriesList = append(seriesList, r.Series...) } if len(errs) != 0 { - return nil, fmt.Errorf("encountered multiple errors: %s", metrics.FormatErrs(errs, "\n")) + return nil, fmt.Errorf("encountered multiple errors: %s", metrics.FormatErrs(errs, "\n")), errQuriesByName } - return seriesList, nil + return seriesList, nil, nil } var seriesList []*model.Series var err error + var errQuriesByName map[string]string switch metricsQueryRangeParams.CompositeMetricQuery.QueryType { case model.QUERY_BUILDER: runQueries := metrics.PrepareBuilderMetricQueries(metricsQueryRangeParams, constants.SIGNOZ_TIMESERIES_TABLENAME) @@ -590,7 +613,7 @@ func (aH *APIHandler) queryRangeMetricsV2(w http.ResponseWriter, r *http.Request respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: runQueries.Err}, nil) return } - seriesList, err = execClickHouseQueries(runQueries.Queries) + seriesList, err, errQuriesByName = execClickHouseQueries(runQueries.Queries) case model.CLICKHOUSE: queries := make(map[string]string) @@ -598,20 +621,32 @@ func (aH *APIHandler) queryRangeMetricsV2(w http.ResponseWriter, r *http.Request if chQuery.Disabled { continue } - queries[name] = chQuery.Query + tmpl := template.New("clickhouse-query") + tmpl, err := tmpl.Parse(chQuery.Query) + if err != nil { + respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil) + return + } + var query bytes.Buffer + err = tmpl.Execute(&query, metricsQueryRangeParams.Variables) + if err != nil { + respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil) + return + } + queries[name] = query.String() } - seriesList, err = execClickHouseQueries(queries) + seriesList, err, errQuriesByName = execClickHouseQueries(queries) case model.PROM: - seriesList, err = execPromQueries(metricsQueryRangeParams) + seriesList, err, errQuriesByName = execPromQueries(metricsQueryRangeParams) default: err = fmt.Errorf("invalid query type") - respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil) + respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, errQuriesByName) return } if err != nil { apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err} - respondError(w, apiErrObj, nil) + respondError(w, apiErrObj, errQuriesByName) return } if metricsQueryRangeParams.CompositeMetricQuery.PanelType == model.QUERY_VALUE && @@ -707,6 +742,25 @@ func (aH *APIHandler) deleteDashboard(w http.ResponseWriter, r *http.Request) { } +func (aH *APIHandler) queryDashboardVars(w http.ResponseWriter, r *http.Request) { + + query := r.URL.Query().Get("query") + if query == "" { + respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("query is required")}, nil) + return + } + if strings.Contains(strings.ToLower(query), "alter table") { + respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("query shouldn't alter data")}, nil) + return + } + dashboardVars, err := (*aH.reader).QueryDashboardVars(r.Context(), query) + if err != nil { + respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil) + return + } + aH.respond(w, dashboardVars) +} + func (aH *APIHandler) updateDashboard(w http.ResponseWriter, r *http.Request) { uuid := mux.Vars(r)["uuid"] @@ -1034,11 +1088,11 @@ func (aH *APIHandler) queryRangeMetrics(w http.ResponseWriter, r *http.Request) if res.Err != nil { switch res.Err.(type) { case promql.ErrQueryCanceled: - respondError(w, &model.ApiError{model.ErrorCanceled, res.Err}, nil) + respondError(w, &model.ApiError{Typ: model.ErrorCanceled, Err: res.Err}, nil) case promql.ErrQueryTimeout: - respondError(w, &model.ApiError{model.ErrorTimeout, res.Err}, nil) + respondError(w, &model.ApiError{Typ: model.ErrorTimeout, Err: res.Err}, nil) } - respondError(w, &model.ApiError{model.ErrorExec, res.Err}, nil) + respondError(w, &model.ApiError{Typ: model.ErrorExec, Err: res.Err}, nil) } response_data := &model.QueryData{ @@ -1088,11 +1142,11 @@ func (aH *APIHandler) queryMetrics(w http.ResponseWriter, r *http.Request) { if res.Err != nil { switch res.Err.(type) { case promql.ErrQueryCanceled: - respondError(w, &model.ApiError{model.ErrorCanceled, res.Err}, nil) + respondError(w, &model.ApiError{Typ: model.ErrorCanceled, Err: res.Err}, nil) case promql.ErrQueryTimeout: - respondError(w, &model.ApiError{model.ErrorTimeout, res.Err}, nil) + respondError(w, &model.ApiError{Typ: model.ErrorTimeout, Err: res.Err}, nil) } - respondError(w, &model.ApiError{model.ErrorExec, res.Err}, nil) + respondError(w, &model.ApiError{Typ: model.ErrorExec, Err: res.Err}, nil) } response_data := &model.QueryData{ diff --git a/pkg/query-service/app/metrics/query_builder.go b/pkg/query-service/app/metrics/query_builder.go index 26f57261b9..72d7ee183d 100644 --- a/pkg/query-service/app/metrics/query_builder.go +++ b/pkg/query-service/app/metrics/query_builder.go @@ -8,6 +8,7 @@ import ( "github.com/SigNoz/govaluate" "go.signoz.io/query-service/constants" "go.signoz.io/query-service/model" + "go.uber.org/zap" ) type RunQueries struct { @@ -50,8 +51,8 @@ func GoValuateFuncs() map[string]govaluate.ExpressionFunction { return GoValuateFuncs } -// formattedValue formats the value to be used in clickhouse query -func formattedValue(v interface{}) string { +// FormattedValue formats the value to be used in clickhouse query +func FormattedValue(v interface{}) string { switch x := v.(type) { case int: return fmt.Sprintf("%d", x) @@ -62,6 +63,9 @@ func formattedValue(v interface{}) string { case bool: return fmt.Sprintf("%v", x) case []interface{}: + if len(x) == 0 { + return "" + } switch x[0].(type) { case string: str := "[" @@ -75,10 +79,12 @@ func formattedValue(v interface{}) string { return str case int, float32, float64, bool: return strings.Join(strings.Fields(fmt.Sprint(x)), ",") + default: + zap.L().Error("invalid type for formatted value", zap.Any("type", reflect.TypeOf(x[0]))) + return "" } - return "" default: - // may be log the warning here? + zap.L().Error("invalid type for formatted value", zap.Any("type", reflect.TypeOf(x))) return "" } } @@ -87,7 +93,7 @@ func formattedValue(v interface{}) string { // timeseries based on search criteria func BuildMetricsTimeSeriesFilterQuery(fs *model.FilterSet, groupTags []string, metricName string, aggregateOperator model.AggregateOperator) (string, error) { var conditions []string - conditions = append(conditions, fmt.Sprintf("metric_name = %s", formattedValue(metricName))) + conditions = append(conditions, fmt.Sprintf("metric_name = %s", FormattedValue(metricName))) if fs != nil && len(fs.Items) != 0 { for _, item := range fs.Items { toFormat := item.Value @@ -102,7 +108,7 @@ func BuildMetricsTimeSeriesFilterQuery(fs *model.FilterSet, groupTags []string, toFormat = x[0] } } - fmtVal := formattedValue(toFormat) + fmtVal := FormattedValue(toFormat) switch op { case "eq": conditions = append(conditions, fmt.Sprintf("labels_object.%s = %s", item.Key, fmtVal)) @@ -152,7 +158,7 @@ func BuildMetricQuery(qp *model.QueryRangeParamsV2, mq *model.MetricQuery, table return "", err } - samplesTableTimeFilter := fmt.Sprintf("metric_name = %s AND timestamp_ms >= %d AND timestamp_ms <= %d", formattedValue(mq.MetricName), qp.Start, qp.End) + samplesTableTimeFilter := fmt.Sprintf("metric_name = %s AND timestamp_ms >= %d AND timestamp_ms <= %d", FormattedValue(mq.MetricName), qp.Start, qp.End) // Select the aggregate value for interval queryTmpl := @@ -419,3 +425,31 @@ func PrepareBuilderMetricQueries(qp *model.QueryRangeParamsV2, tableName string) } return &RunQueries{Queries: namedQueries} } + +// PromFormattedValue formats the value to be used in promql +func PromFormattedValue(v interface{}) string { + switch x := v.(type) { + case int: + return fmt.Sprintf("%d", x) + case float32, float64: + return fmt.Sprintf("%f", x) + case string: + return fmt.Sprintf("%s", x) + case bool: + return fmt.Sprintf("%v", x) + case []interface{}: + if len(x) == 0 { + return "" + } + switch x[0].(type) { + case string, int, float32, float64, bool: + return strings.Trim(strings.Join(strings.Fields(fmt.Sprint(x)), "|"), "[]") + default: + zap.L().Error("invalid type for prom formatted value", zap.Any("type", reflect.TypeOf(x[0]))) + return "" + } + default: + zap.L().Error("invalid type for prom formatted value", zap.Any("type", reflect.TypeOf(x))) + return "" + } +} diff --git a/pkg/query-service/app/parser/metrics.go b/pkg/query-service/app/parser/metrics.go index ce4d079fa5..c8ec53a322 100644 --- a/pkg/query-service/app/parser/metrics.go +++ b/pkg/query-service/app/parser/metrics.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "net/http" + "strings" "go.signoz.io/query-service/app/metrics" "go.signoz.io/query-service/model" @@ -36,6 +37,44 @@ func ParseMetricQueryRangeParams(r *http.Request) (*model.QueryRangeParamsV2, *m if err := validateQueryRangeParamsV2(postData); err != nil { return nil, &model.ApiError{Typ: model.ErrorBadData, Err: err} } + // prepare the variables for the corrspnding query type + formattedVars := make(map[string]interface{}) + for name, value := range postData.Variables { + if postData.CompositeMetricQuery.QueryType == model.PROM { + formattedVars[name] = metrics.PromFormattedValue(value) + } else if postData.CompositeMetricQuery.QueryType == model.CLICKHOUSE { + formattedVars[name] = metrics.FormattedValue(value) + } + } + // replace the variables in metrics builder filter item with actual value + if postData.CompositeMetricQuery.QueryType == model.QUERY_BUILDER { + for _, query := range postData.CompositeMetricQuery.BuilderQueries { + for idx := range query.TagFilters.Items { + item := &query.TagFilters.Items[idx] + value := item.Value + if value != nil { + switch x := value.(type) { + case string: + variableName := strings.Trim(x, "{{ . }}") + if _, ok := postData.Variables[variableName]; ok { + item.Value = postData.Variables[variableName] + } + case []interface{}: + if len(x) > 0 { + switch x[0].(type) { + case string: + variableName := strings.Trim(x[0].(string), "{{ . }}") + if _, ok := postData.Variables[variableName]; ok { + item.Value = postData.Variables[variableName] + } + } + } + } + } + } + } + } + postData.Variables = formattedVars return postData, nil } diff --git a/pkg/query-service/app/server.go b/pkg/query-service/app/server.go index dd00aea804..2a04302ef7 100644 --- a/pkg/query-service/app/server.go +++ b/pkg/query-service/app/server.go @@ -77,18 +77,20 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) { } localDB.SetMaxOpenConns(10) + readerReady := make(chan bool) var reader interfaces.Reader storage := os.Getenv("STORAGE") if storage == "clickhouse" { zap.S().Info("Using ClickHouse as datastore ...") clickhouseReader := clickhouseReader.NewReader(localDB, serverOptions.PromConfigPath) - go clickhouseReader.Start() + go clickhouseReader.Start(readerReady) reader = clickhouseReader } else { return nil, fmt.Errorf("Storage type: %s is not supported in query service", storage) } + <-readerReady rm, err := makeRulesManager(serverOptions.PromConfigPath, constants.GetAlertManagerApiPrefix(), serverOptions.RuleRepoURL, localDB, reader, serverOptions.DisableRules) if err != nil { return nil, err @@ -232,9 +234,10 @@ func (s *Server) analyticsMiddleware(next http.Handler) http.Handler { next.ServeHTTP(lrw, r) data := map[string]interface{}{"path": path, "statusCode": lrw.statusCode} - - if _, ok := telemetry.IgnoredPaths()[path]; !ok { - telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_PATH, data) + if telemetry.GetInstance().IsSampled() { + if _, ok := telemetry.IgnoredPaths()[path]; !ok { + telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_PATH, data) + } } }) @@ -361,7 +364,7 @@ func makeRulesManager( disableRules bool) (*rules.Manager, error) { // create engine - pqle, err := pqle.FromConfigPath(promConfigPath) + pqle, err := pqle.FromReader(ch) if err != nil { return nil, fmt.Errorf("failed to create pql engine : %v", err) } diff --git a/pkg/query-service/config/prometheus.yml b/pkg/query-service/config/prometheus.yml index d7c0ce6911..88ee92961b 100644 --- a/pkg/query-service/config/prometheus.yml +++ b/pkg/query-service/config/prometheus.yml @@ -19,8 +19,7 @@ rule_files: # A scrape configuration containing exactly one endpoint to scrape: # Here it's Prometheus itself. -scrape_configs: - +scrape_configs: [] remote_read: - url: tcp://localhost:9000/?database=signoz_metrics diff --git a/pkg/query-service/constants/constants.go b/pkg/query-service/constants/constants.go index 2e01c976cb..3c6e8f6317 100644 --- a/pkg/query-service/constants/constants.go +++ b/pkg/query-service/constants/constants.go @@ -41,6 +41,7 @@ var AmChannelApiPath = GetOrDefaultEnv("ALERTMANAGER_API_CHANNEL_PATH", "v1/rout var RELATIONAL_DATASOURCE_PATH = GetOrDefaultEnv("SIGNOZ_LOCAL_DB_PATH", "/var/lib/signoz/signoz.db") const ( + TraceID = "traceID" ServiceName = "serviceName" HttpRoute = "httpRoute" HttpCode = "httpCode" diff --git a/pkg/query-service/go.mod b/pkg/query-service/go.mod index 34ddceace9..5e608f0d8b 100644 --- a/pkg/query-service/go.mod +++ b/pkg/query-service/go.mod @@ -34,6 +34,7 @@ require ( github.com/minio/md5-simd v1.1.0 // indirect github.com/minio/sha256-simd v0.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/posthog/posthog-go v0.0.0-20220817142604-0b0bbf0f9c0f // indirect gopkg.in/ini.v1 v1.42.0 // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect ) diff --git a/pkg/query-service/go.sum b/pkg/query-service/go.sum index 4fbb2b1c25..e0db02686a 100644 --- a/pkg/query-service/go.sum +++ b/pkg/query-service/go.sum @@ -93,6 +93,7 @@ github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/cmux v0.0.0-20170110192607-30d10be49292/go.mod h1:qRiX68mZX1lGBkTWyp3CLcenw9I94W2dLeRvMzcn9N4= github.com/cockroachdb/cockroach v0.0.0-20170608034007-84bc9597164f/go.mod h1:xeT/CQ0qZHangbYbWShlCGAx31aV4AjGswDUjhKS6HQ= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -378,6 +379,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posthog/posthog-go v0.0.0-20220817142604-0b0bbf0f9c0f h1:h0p1aZ9F5d6IXOygysob3g4B07b+HuVUQC0VJKD8wA4= +github.com/posthog/posthog-go v0.0.0-20220817142604-0b0bbf0f9c0f/go.mod h1:oa2sAs9tGai3VldabTV0eWejt/O4/OOD7azP8GaikqU= github.com/prometheus/client_golang v0.9.0-pre1.0.20181001174001-0a8115f42e03 h1:hqNopISksxji/N5zEy1xMN7TrnSyVG/LymiwnkXi6/Q= github.com/prometheus/client_golang v0.9.0-pre1.0.20181001174001-0a8115f42e03/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM= @@ -393,6 +396,7 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/samuel/go-zookeeper v0.0.0-20161028232340-1d7be4effb13/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da h1:p3Vo3i64TCLY7gIfzeQaUJ+kppEO5WQG3cL8iE8tGHU= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= @@ -406,6 +410,7 @@ github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/vfsgen v0.0.0-20180711163814-62bca832be04/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= @@ -433,6 +438,7 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc= github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g= diff --git a/pkg/query-service/interfaces/interface.go b/pkg/query-service/interfaces/interface.go index 5367021500..4f4da853c6 100644 --- a/pkg/query-service/interfaces/interface.go +++ b/pkg/query-service/interfaces/interface.go @@ -5,6 +5,7 @@ import ( "github.com/ClickHouse/clickhouse-go/v2" "github.com/prometheus/prometheus/promql" + "github.com/prometheus/prometheus/storage" "github.com/prometheus/prometheus/util/stats" am "go.signoz.io/query-service/integrations/alertManager" "go.signoz.io/query-service/model" @@ -70,4 +71,8 @@ type Reader interface { // Connection needed for rules, not ideal but required GetConn() clickhouse.Conn + GetQueryEngine() *promql.Engine + GetFanoutStorage() *storage.Storage + + QueryDashboardVars(ctx context.Context, query string) (*model.DashboardVar, error) } diff --git a/pkg/query-service/model/queryParams.go b/pkg/query-service/model/queryParams.go index 33827d63c1..6d78e7438f 100644 --- a/pkg/query-service/model/queryParams.go +++ b/pkg/query-service/model/queryParams.go @@ -118,11 +118,12 @@ const ( ) type QueryRangeParamsV2 struct { - DataSource DataSource `json:"dataSource"` - Start int64 `json:"start"` - End int64 `json:"end"` - Step int64 `json:"step"` - CompositeMetricQuery *CompositeMetricQuery `json:"compositeMetricQuery"` + DataSource DataSource `json:"dataSource"` + Start int64 `json:"start"` + End int64 `json:"end"` + Step int64 `json:"step"` + CompositeMetricQuery *CompositeMetricQuery `json:"compositeMetricQuery"` + Variables map[string]interface{} `json:"variables,omitempty"` } // Metric auto complete types @@ -181,6 +182,7 @@ type TagQuery struct { } type GetFilteredSpansParams struct { + TraceID []string `json:"traceID"` ServiceName []string `json:"serviceName"` Operation []string `json:"operation"` Kind string `json:"kind"` @@ -208,6 +210,7 @@ type GetFilteredSpansParams struct { } type GetFilteredSpanAggregatesParams struct { + TraceID []string `json:"traceID"` ServiceName []string `json:"serviceName"` Operation []string `json:"operation"` Kind string `json:"kind"` @@ -236,6 +239,7 @@ type GetFilteredSpanAggregatesParams struct { } type SpanFilterParams struct { + TraceID []string `json:"traceID"` Status []string `json:"status"` ServiceName []string `json:"serviceName"` HttpRoute []string `json:"httpRoute"` @@ -258,6 +262,7 @@ type SpanFilterParams struct { } type TagFilterParams struct { + TraceID []string `json:"traceID"` Status []string `json:"status"` ServiceName []string `json:"serviceName"` HttpRoute []string `json:"httpRoute"` diff --git a/pkg/query-service/model/response.go b/pkg/query-service/model/response.go index bc07bea356..a78f93d8c0 100644 --- a/pkg/query-service/model/response.go +++ b/pkg/query-service/model/response.go @@ -492,3 +492,7 @@ func (s *ServiceItem) MarshalJSON() ([]byte, error) { Alias: (*Alias)(s), }) } + +type DashboardVar struct { + VariableValues []interface{} `json:"variableValues"` +} diff --git a/pkg/query-service/pqlEngine/engine.go b/pkg/query-service/pqlEngine/engine.go index e9a45ad542..47bde314ee 100644 --- a/pkg/query-service/pqlEngine/engine.go +++ b/pkg/query-service/pqlEngine/engine.go @@ -3,6 +3,8 @@ package promql import ( "context" "fmt" + "time" + "github.com/go-kit/log" pmodel "github.com/prometheus/common/model" plog "github.com/prometheus/common/promlog" @@ -11,7 +13,7 @@ import ( pql "github.com/prometheus/prometheus/promql" pstorage "github.com/prometheus/prometheus/storage" premote "github.com/prometheus/prometheus/storage/remote" - "time" + "go.signoz.io/query-service/interfaces" ) type PqlEngine struct { @@ -29,6 +31,13 @@ func FromConfigPath(promConfigPath string) (*PqlEngine, error) { return NewPqlEngine(c) } +func FromReader(ch interfaces.Reader) (*PqlEngine, error) { + return &PqlEngine{ + engine: ch.GetQueryEngine(), + fanoutStorage: *ch.GetFanoutStorage(), + }, nil +} + func NewPqlEngine(config *pconfig.Config) (*PqlEngine, error) { logLevel := plog.AllowedLevel{} diff --git a/pkg/query-service/telemetry/telemetry.go b/pkg/query-service/telemetry/telemetry.go index 43d27484e5..62560d97f9 100644 --- a/pkg/query-service/telemetry/telemetry.go +++ b/pkg/query-service/telemetry/telemetry.go @@ -3,11 +3,14 @@ package telemetry import ( "context" "io/ioutil" + "math/rand" "net/http" "os" + "strings" "sync" "time" + ph "github.com/posthog/posthog-go" "go.signoz.io/query-service/constants" "go.signoz.io/query-service/interfaces" "go.signoz.io/query-service/model" @@ -16,15 +19,19 @@ import ( ) const ( - TELEMETRY_EVENT_PATH = "API Call" - TELEMETRY_EVENT_USER = "User" - TELEMETRY_EVENT_INPRODUCT_FEEDBACK = "InProduct Feeback Submitted" - TELEMETRY_EVENT_NUMBER_OF_SERVICES = "Number of Services" - TELEMETRY_EVENT_HEART_BEAT = "Heart Beat" - TELEMETRY_EVENT_ORG_SETTINGS = "Org Settings" + TELEMETRY_EVENT_PATH = "API Call" + TELEMETRY_EVENT_USER = "User" + TELEMETRY_EVENT_INPRODUCT_FEEDBACK = "InProduct Feeback Submitted" + TELEMETRY_EVENT_NUMBER_OF_SERVICES = "Number of Services" + TELEMETRY_EVENT_NUMBER_OF_SERVICES_PH = "Number of Services V2" + TELEMETRY_EVENT_HEART_BEAT = "Heart Beat" + TELEMETRY_EVENT_ORG_SETTINGS = "Org Settings" + DEFAULT_SAMPLING = 0.1 ) const api_key = "4Gmoa4ixJAUHx2BpJxsjwA1bEfnwEeRz" +const ph_api_key = "H-htDCae7CR3RV57gUzmol6IAKtm5IMCvbcm_fwnL-w" + const IP_NOT_FOUND_PLACEHOLDER = "NA" const HEART_BEAT_DURATION = 6 * time.Hour @@ -34,20 +41,41 @@ const HEART_BEAT_DURATION = 6 * time.Hour var telemetry *Telemetry var once sync.Once +func (a *Telemetry) IsSampled() bool { + + random_number := a.minRandInt + rand.Intn(a.maxRandInt-a.minRandInt) + 1 + + if (random_number % a.maxRandInt) == 0 { + return true + } else { + return false + } + +} + type Telemetry struct { - operator analytics.Client - ipAddress string - isEnabled bool - isAnonymous bool - distinctId string - reader interfaces.Reader + operator analytics.Client + phOperator ph.Client + ipAddress string + isEnabled bool + isAnonymous bool + distinctId string + reader interfaces.Reader + companyDomain string + minRandInt int + maxRandInt int } func createTelemetry() { telemetry = &Telemetry{ - operator: analytics.New(api_key), - ipAddress: getOutboundIP(), + operator: analytics.New(api_key), + phOperator: ph.New(ph_api_key), + ipAddress: getOutboundIP(), } + telemetry.minRandInt = 0 + telemetry.maxRandInt = int(1 / DEFAULT_SAMPLING) + + rand.Seed(time.Now().UnixNano()) data := map[string]interface{}{} @@ -106,13 +134,36 @@ func (a *Telemetry) IdentifyUser(user *model.User) { if !a.isTelemetryEnabled() || a.isTelemetryAnonymous() { return } + a.setCompanyDomain(user.Email) a.operator.Enqueue(analytics.Identify{ UserId: a.ipAddress, Traits: analytics.NewTraits().SetName(user.Name).SetEmail(user.Email).Set("ip", a.ipAddress), }) + // Updating a groups properties + a.phOperator.Enqueue(ph.GroupIdentify{ + Type: "companyDomain", + Key: a.getCompanyDomain(), + Properties: ph.NewProperties(). + Set("companyDomain", a.getCompanyDomain()), + }) } + +func (a *Telemetry) setCompanyDomain(email string) { + + email_split := strings.Split(email, "@") + if len(email_split) != 2 { + a.companyDomain = email + } + a.companyDomain = email_split[1] + +} + +func (a *Telemetry) getCompanyDomain() string { + return a.companyDomain +} + func (a *Telemetry) checkEvents(event string) bool { sendEvent := true if event == TELEMETRY_EVENT_USER && a.isTelemetryAnonymous() { @@ -136,6 +187,7 @@ func (a *Telemetry) SendEvent(event string, data map[string]interface{}) { properties := analytics.NewProperties() properties.Set("version", version.GetVersion()) properties.Set("deploymentType", getDeploymentType()) + properties.Set("companyDomain", a.getCompanyDomain()) for k, v := range data { properties.Set(k, v) @@ -151,6 +203,18 @@ func (a *Telemetry) SendEvent(event string, data map[string]interface{}) { UserId: userId, Properties: properties, }) + + if event == TELEMETRY_EVENT_NUMBER_OF_SERVICES { + + a.phOperator.Enqueue(ph.Capture{ + DistinctId: userId, + Event: TELEMETRY_EVENT_NUMBER_OF_SERVICES_PH, + Properties: ph.Properties(properties), + Groups: ph.NewGroups(). + Set("companyDomain", a.getCompanyDomain()), + }) + + } } func (a *Telemetry) GetDistinctId() string { diff --git a/pkg/query-service/tests/test-deploy/docker-compose.yaml b/pkg/query-service/tests/test-deploy/docker-compose.yaml index a8bae4a159..fef98d2007 100644 --- a/pkg/query-service/tests/test-deploy/docker-compose.yaml +++ b/pkg/query-service/tests/test-deploy/docker-compose.yaml @@ -61,7 +61,7 @@ services: condition: service_healthy otel-collector: - image: signoz-otel-collector:0.55.0 + image: signoz-otel-collector:0.55.1 command: ["--config=/etc/otel-collector-config.yaml"] user: root # required for reading docker container logs volumes: @@ -77,7 +77,7 @@ services: condition: service_healthy otel-collector-metrics: - image: signoz-otel-collector:0.55.0 + image: signoz-otel-collector:0.55.1 command: ["--config=/etc/otel-collector-metrics-config.yaml"] volumes: - ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml diff --git a/pkg/query-service/tests/test-deploy/prometheus.yml b/pkg/query-service/tests/test-deploy/prometheus.yml index 16e65ff18c..6a796ea1d0 100644 --- a/pkg/query-service/tests/test-deploy/prometheus.yml +++ b/pkg/query-service/tests/test-deploy/prometheus.yml @@ -19,8 +19,7 @@ rule_files: # A scrape configuration containing exactly one endpoint to scrape: # Here it's Prometheus itself. -scrape_configs: - +scrape_configs: [] remote_read: - url: tcp://clickhouse:9000/?database=signoz_metrics