Merge pull request #1564 from SigNoz/release/v0.11.1

Release/v0.11.1
This commit is contained in:
Ankit Nayan 2022-09-14 10:21:54 +05:30 committed by GitHub
commit adda2e8a11
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
91 changed files with 2126 additions and 288 deletions

View File

@ -40,7 +40,7 @@ services:
condition: on-failure condition: on-failure
query-service: query-service:
image: signoz/query-service:0.11.0 image: signoz/query-service:0.11.1
command: ["-config=/root/config/prometheus.yml"] command: ["-config=/root/config/prometheus.yml"]
# ports: # ports:
# - "6060:6060" # pprof port # - "6060:6060" # pprof port
@ -70,7 +70,7 @@ services:
- clickhouse - clickhouse
frontend: frontend:
image: signoz/frontend:0.11.0 image: signoz/frontend:0.11.1
deploy: deploy:
restart_policy: restart_policy:
condition: on-failure condition: on-failure
@ -83,7 +83,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector: otel-collector:
image: signoz-otel-collector:0.55.0 image: signoz-otel-collector:0.55.1
command: ["--config=/etc/otel-collector-config.yaml"] command: ["--config=/etc/otel-collector-config.yaml"]
user: root # required for reading docker container logs user: root # required for reading docker container logs
volumes: volumes:
@ -111,7 +111,7 @@ services:
- clickhouse - clickhouse
otel-collector-metrics: 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"] command: ["--config=/etc/otel-collector-metrics-config.yaml"]
volumes: volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml - ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml

View File

@ -19,8 +19,7 @@ rule_files:
# A scrape configuration containing exactly one endpoint to scrape: # A scrape configuration containing exactly one endpoint to scrape:
# Here it's Prometheus itself. # Here it's Prometheus itself.
scrape_configs: scrape_configs: []
remote_read: remote_read:
- url: tcp://clickhouse:9000/?database=signoz_metrics - url: tcp://clickhouse:9000/?database=signoz_metrics

View File

@ -11,6 +11,10 @@ server {
gzip_buffers 16 8k; 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 / { location / {
if ( $uri = '/index.html' ) { if ( $uri = '/index.html' ) {
add_header Cache-Control no-store always; add_header Cache-Control no-store always;

View File

@ -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` # 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: otel-collector:
container_name: 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"] command: ["--config=/etc/otel-collector-config.yaml"]
# user: root # required for reading docker container logs # user: root # required for reading docker container logs
volumes: volumes:
@ -67,7 +67,7 @@ services:
otel-collector-metrics: otel-collector-metrics:
container_name: 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"] command: ["--config=/etc/otel-collector-metrics-config.yaml"]
volumes: volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml - ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml

View File

@ -2,7 +2,7 @@ version: "2.4"
services: services:
query-service: query-service:
image: signoz/query-service:0.11.0 image: signoz/query-service:0.11.1
container_name: query-service container_name: query-service
command: ["-config=/root/config/prometheus.yml"] command: ["-config=/root/config/prometheus.yml"]
# ports: # ports:
@ -31,7 +31,7 @@ services:
condition: service_healthy condition: service_healthy
frontend: frontend:
image: signoz/frontend:0.11.0 image: signoz/frontend:0.11.1
container_name: frontend container_name: frontend
restart: on-failure restart: on-failure
depends_on: depends_on:

View File

@ -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` # 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: query-service:
image: signoz/query-service:0.11.0 image: signoz/query-service:0.11.1
container_name: query-service container_name: query-service
command: ["-config=/root/config/prometheus.yml"] command: ["-config=/root/config/prometheus.yml"]
# ports: # ports:
@ -68,7 +68,7 @@ services:
condition: service_healthy condition: service_healthy
frontend: frontend:
image: signoz/frontend:0.11.0 image: signoz/frontend:0.11.1
container_name: frontend container_name: frontend
restart: on-failure restart: on-failure
depends_on: depends_on:
@ -80,7 +80,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector: 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"] command: ["--config=/etc/otel-collector-config.yaml"]
user: root # required for reading docker container logs user: root # required for reading docker container logs
volumes: volumes:
@ -106,7 +106,7 @@ services:
condition: service_healthy condition: service_healthy
otel-collector-metrics: 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"] command: ["--config=/etc/otel-collector-metrics-config.yaml"]
volumes: volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml - ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml

View File

@ -19,8 +19,7 @@ rule_files:
# A scrape configuration containing exactly one endpoint to scrape: # A scrape configuration containing exactly one endpoint to scrape:
# Here it's Prometheus itself. # Here it's Prometheus itself.
scrape_configs: scrape_configs: []
remote_read: remote_read:
- url: tcp://clickhouse:9000/?database=signoz_metrics - url: tcp://clickhouse:9000/?database=signoz_metrics

View File

@ -13,7 +13,6 @@ server {
# to handle uri issue 414 from nginx # to handle uri issue 414 from nginx
client_max_body_size 24M; client_max_body_size 24M;
large_client_header_buffers 8 16k; large_client_header_buffers 8 16k;
location / { location / {

View File

@ -11,6 +11,10 @@ server {
gzip_buffers 16 8k; 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 / { location / {
root /usr/share/nginx/html; root /usr/share/nginx/html;
index index.html index.htm; index index.html index.htm;

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -35,11 +35,8 @@ export const SettingsPage = Loadable(
() => import(/* webpackChunkName: "SettingsPage" */ 'pages/Settings'), () => import(/* webpackChunkName: "SettingsPage" */ 'pages/Settings'),
); );
export const InstrumentationPage = Loadable( export const GettingStarted = Loadable(
() => () => import(/* webpackChunkName: "GettingStarted" */ 'pages/GettingStarted'),
import(
/* webpackChunkName: "InstrumentationPage" */ 'pages/AddInstrumentation'
),
); );
export const DashboardPage = Loadable( export const DashboardPage = Loadable(

View File

@ -11,7 +11,7 @@ import {
EditAlertChannelsAlerts, EditAlertChannelsAlerts,
EditRulesPage, EditRulesPage,
ErrorDetails, ErrorDetails,
InstrumentationPage, GettingStarted,
ListAllALertsPage, ListAllALertsPage,
Login, Login,
Logs, Logs,
@ -85,7 +85,7 @@ const routes: AppRoutes[] = [
{ {
path: ROUTES.INSTRUMENTATION, path: ROUTES.INSTRUMENTATION,
exact: true, exact: true,
component: InstrumentationPage, component: GettingStarted,
isPrivate: true, isPrivate: true,
key: 'INSTRUMENTATION', key: 'INSTRUMENTATION',
}, },

View File

@ -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<SuccessResponse<PayloadProps> | 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;

View File

@ -6,7 +6,7 @@ const ROUTES = {
TRACE: '/trace', TRACE: '/trace',
TRACE_DETAIL: '/trace/:id', TRACE_DETAIL: '/trace/:id',
SETTINGS: '/settings', SETTINGS: '/settings',
INSTRUMENTATION: '/add-instrumentation', INSTRUMENTATION: '/get-started',
USAGE_EXPLORER: '/usage-explorer', USAGE_EXPLORER: '/usage-explorer',
APPLICATION: '/application', APPLICATION: '/application',
ALL_DASHBOARD: '/dashboard', ALL_DASHBOARD: '/dashboard',

View File

@ -3,7 +3,7 @@ import styled from 'styled-components';
export const Layout = styled(LayoutComponent)` export const Layout = styled(LayoutComponent)`
&&& { &&& {
min-height: 91vh; min-height: 92vh;
display: flex; display: flex;
position: relative; position: relative;
} }

View File

@ -7,6 +7,7 @@ import {
timeItems, timeItems,
timePreferance, timePreferance,
} from 'container/NewWidget/RightContainer/timeItems'; } from 'container/NewWidget/RightContainer/timeItems';
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
import getChartData from 'lib/getChartData'; import getChartData from 'lib/getChartData';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import { useQuery } from 'react-query'; import { useQuery } from 'react-query';
@ -52,6 +53,7 @@ function FullView({
graphType: widget.panelTypes, graphType: widget.panelTypes,
query: widget.query, query: widget.query,
globalSelectedInterval: globalSelectedTime, globalSelectedInterval: globalSelectedTime,
variables: getDashboardVariables(),
}), }),
); );

View File

@ -3,6 +3,7 @@ import { AxiosError } from 'axios';
import { ChartData } from 'chart.js'; import { ChartData } from 'chart.js';
import Spinner from 'components/Spinner'; import Spinner from 'components/Spinner';
import GridGraphComponent from 'container/GridGraphComponent'; import GridGraphComponent from 'container/GridGraphComponent';
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
import getChartData from 'lib/getChartData'; import getChartData from 'lib/getChartData';
import isEmpty from 'lodash-es/isEmpty'; import isEmpty from 'lodash-es/isEmpty';
import React, { memo, useCallback, useEffect, useState } from 'react'; import React, { memo, useCallback, useEffect, useState } from 'react';
@ -104,11 +105,18 @@ function GridCardGraph({
useEffect(() => { useEffect(() => {
(async (): Promise<void> => { (async (): Promise<void> => {
try { try {
setState((state) => ({
...state,
error: false,
errorMessage: '',
loading: true,
}));
const response = await GetMetricQueryRange({ const response = await GetMetricQueryRange({
selectedTime: widget.timePreferance, selectedTime: widget.timePreferance,
graphType: widget.panelTypes, graphType: widget.panelTypes,
query: widget.query, query: widget.query,
globalSelectedInterval, globalSelectedInterval,
variables: getDashboardVariables(),
}); });
const isError = response.error; const isError = response.error;
@ -144,6 +152,11 @@ function GridCardGraph({
errorMessage: (error as AxiosError).toString(), errorMessage: (error as AxiosError).toString(),
loading: false, loading: false,
})); }));
} finally {
setState((state) => ({
...state,
loading: false,
}));
} }
})(); })();
}, [widget, maxTime, minTime, globalSelectedInterval]); }, [widget, maxTime, minTime, globalSelectedInterval]);

View File

@ -121,6 +121,7 @@ function GridGraph(props: Props): JSX.Element {
name: data.name, name: data.name,
tags: data.tags, tags: data.tags,
widgets: data.widgets, widgets: data.widgets,
variables: data.variables,
layout, layout,
}, },
uuid: selectedDashboard.uuid, uuid: selectedDashboard.uuid,
@ -157,6 +158,7 @@ function GridGraph(props: Props): JSX.Element {
data.name, data.name,
data.tags, data.tags,
data.title, data.title,
data.variables,
data.widgets, data.widgets,
dispatch, dispatch,
saveLayoutPermission, saveLayoutPermission,

View File

@ -27,6 +27,7 @@ export const UpdateDashboard = async ({
description: data.description, description: data.description,
name: data.name, name: data.name,
tags: data.tags, tags: data.tags,
variables: data.variables,
widgets: [ widgets: [
...(data.widgets || []), ...(data.widgets || []),
{ {

View File

@ -12,6 +12,7 @@ describe('executeSearchQueries', () => {
updated_at: '', updated_at: '',
data: { data: {
title: 'first dashboard', title: 'first dashboard',
variables: {},
}, },
}; };
const secondDashboard: Dashboard = { const secondDashboard: Dashboard = {
@ -21,6 +22,7 @@ describe('executeSearchQueries', () => {
updated_at: '', updated_at: '',
data: { data: {
title: 'second dashboard', title: 'second dashboard',
variables: {},
}, },
}; };
const thirdDashboard: Dashboard = { const thirdDashboard: Dashboard = {
@ -30,6 +32,7 @@ describe('executeSearchQueries', () => {
updated_at: '', updated_at: '',
data: { data: {
title: 'third dashboard (with special characters +?\\)', title: 'third dashboard (with special characters +?\\)',
variables: {},
}, },
}; };
const dashboards = [firstDashboard, secondDashboard, thirdDashboard]; const dashboards = [firstDashboard, secondDashboard, thirdDashboard];

View File

@ -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<AppState, DashboardReducer>(
(state) => state.dashboards,
);
const [selectedDashboard] = dashboards;
const selectedData = selectedDashboard.data;
const { title } = selectedData;
const { tags } = selectedData;
const { description } = selectedData;
const [updatedTitle, setUpdatedTitle] = useState<string>(title);
const [updatedTags, setUpdatedTags] = useState<string[]>(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 (
<Col>
<Space direction="vertical" style={{ width: '100%' }}>
<div>
<Typography style={{ marginBottom: '0.5rem' }}>Name</Typography>
<Input
value={updatedTitle}
onChange={(e): void => setUpdatedTitle(e.target.value)}
/>
</div>
<div>
<Typography style={{ marginBottom: '0.5rem' }}>Description</Typography>
<Input.TextArea
value={updatedDescription}
onChange={(e): void => setUpdatedDescription(e.target.value)}
/>
</div>
<div>
<Typography style={{ marginBottom: '0.5rem' }}>Tags</Typography>
<AddTags tags={updatedTags} setTags={setUpdatedTags} />
</div>
<div>
<Divider />
<Button icon={<SaveOutlined />} onClick={onSaveHandler} type="primary">
{t('save')}
</Button>
</div>
</Space>
</Col>
);
}
interface DispatchProps {
updateDashboardTitleDescriptionTags: (
props: UpdateDashboardTitleDescriptionTagsProps,
) => (dispatch: Dispatch<AppActions>) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
updateDashboardTitleDescriptionTags: bindActionCreators(
UpdateDashboardTitleDescriptionTags,
dispatch,
),
});
type DescriptionOfDashboardProps = DispatchProps;
export default connect(null, mapDispatchToProps)(GeneralDashboardSettings);

View File

@ -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;
}
`;

View File

@ -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<string>(
variableData.name || '',
);
const [variableDescription, setVariableDescription] = useState<string>(
variableData.description || '',
);
const [queryType, setQueryType] = useState<TVariableQueryType>(
variableData.type || 'QUERY',
);
const [variableQueryValue, setVariableQueryValue] = useState<string>(
variableData.queryValue || '',
);
const [variableCustomValue, setVariableCustomValue] = useState<string>(
variableData.customValue || '',
);
const [variableTextboxValue, setVariableTextboxValue] = useState<string>(
variableData.textboxValue || '',
);
const [
variableSortType,
setVariableSortType,
] = useState<TSortVariableValuesType>(
variableData.sort || VariableSortTypeArr[0],
);
const [variableMultiSelect, setVariableMultiSelect] = useState<boolean>(
variableData.multiSelect || false,
);
const [variableShowALLOption, setVariableShowALLOption] = useState<boolean>(
variableData.showALLOption || false,
);
const [previewValues, setPreviewValues] = useState<string[]>([]);
// Internal states
const [previewLoading, setPreviewLoading] = useState<boolean>(false);
// Error messages
const [errorName, setErrorName] = useState<boolean>(false);
const [errorPreview, setErrorPreview] = useState<string | null>(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<void> => {
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 (
<Col>
{/* <Typography.Title level={3}>Add Variable</Typography.Title> */}
<VariableItemRow>
<LabelContainer>
<Typography>Name</Typography>
</LabelContainer>
<div>
<Input
placeholder="Unique name of the variable"
style={{ width: 400 }}
value={variableName}
onChange={(e): void => {
setVariableName(e.target.value);
setErrorName(
!validateName(e.target.value) && e.target.value !== variableData.name,
);
}}
/>
<div>
<Typography.Text type="warning">
{errorName ? 'Variable name already exists' : ''}
</Typography.Text>
</div>
</div>
</VariableItemRow>
<VariableItemRow>
<LabelContainer>
<Typography>Description</Typography>
</LabelContainer>
<Input.TextArea
value={variableDescription}
placeholder="Write description of the variable"
style={{ width: 400 }}
onChange={(e): void => setVariableDescription(e.target.value)}
/>
</VariableItemRow>
<VariableItemRow>
<LabelContainer>
<Typography>Type</Typography>
</LabelContainer>
<Select
defaultActiveFirstOption
style={{ width: 400 }}
onChange={(e: TVariableQueryType): void => {
setQueryType(e);
}}
value={queryType}
>
<Option value={VariableQueryTypeArr[0]}>Query</Option>
<Option value={VariableQueryTypeArr[1]}>Textbox</Option>
<Option value={VariableQueryTypeArr[2]}>Custom</Option>
</Select>
</VariableItemRow>
<Typography.Title
level={5}
style={{ marginTop: '1rem', marginBottom: '1rem' }}
>
Options
</Typography.Title>
{queryType === 'QUERY' && (
<VariableItemRow>
<LabelContainer>
<Typography>Query</Typography>
</LabelContainer>
<div style={{ flex: 1, position: 'relative' }}>
<Editor
language="sql"
value={variableQueryValue}
onChange={(e): void => setVariableQueryValue(e)}
height="300px"
/>
<Button
type="primary"
onClick={handleQueryResult}
style={{
position: 'absolute',
bottom: 0,
}}
loading={previewLoading}
>
Test Run Query
</Button>
</div>
</VariableItemRow>
)}
{queryType === 'CUSTOM' && (
<VariableItemRow>
<LabelContainer>
<Typography>Values separated by comma</Typography>
</LabelContainer>
<Input.TextArea
value={variableCustomValue}
placeholder="1, 10, mykey, mykey:myvalue"
style={{ width: 400 }}
onChange={(e): void => {
setVariableCustomValue(e.target.value);
setPreviewValues(
sortValues(
commaValuesParser(e.target.value),
variableSortType,
) as never,
);
}}
/>
</VariableItemRow>
)}
{queryType === 'TEXTBOX' && (
<VariableItemRow>
<LabelContainer>
<Typography>Default Value</Typography>
</LabelContainer>
<Input
value={variableTextboxValue}
onChange={(e): void => {
setVariableTextboxValue(e.target.value);
}}
placeholder="Default value if any"
style={{ width: 400 }}
/>
</VariableItemRow>
)}
{(queryType === 'QUERY' || queryType === 'CUSTOM') && (
<>
<VariableItemRow>
<LabelContainer>
<Typography>Preview of Values</Typography>
</LabelContainer>
<div style={{ flex: 1 }}>
{errorPreview ? (
<Typography style={{ color: orange[5] }}>{errorPreview}</Typography>
) : (
map(previewValues, (value, idx) => (
<Tag key={`${value}${idx}`}>{value.toString()}</Tag>
))
)}
</div>
</VariableItemRow>
<VariableItemRow>
<LabelContainer>
<Typography>Sort</Typography>
</LabelContainer>
<Select
defaultActiveFirstOption
style={{ width: 400 }}
defaultValue={VariableSortTypeArr[0]}
value={variableSortType}
onChange={(value: TSortVariableValuesType): void =>
setVariableSortType(value)
}
>
<Option value={VariableSortTypeArr[0]}>Disabled</Option>
<Option value={VariableSortTypeArr[1]}>Ascending</Option>
<Option value={VariableSortTypeArr[2]}>Descending</Option>
</Select>
</VariableItemRow>
<VariableItemRow>
<LabelContainer>
<Typography>Enable multiple values to be checked</Typography>
</LabelContainer>
<Switch
checked={variableMultiSelect}
onChange={(e): void => {
setVariableMultiSelect(e);
if (!e) {
setVariableShowALLOption(false);
}
}}
/>
</VariableItemRow>
{variableMultiSelect && (
<VariableItemRow>
<LabelContainer>
<Typography>Include an option for ALL values</Typography>
</LabelContainer>
<Switch
checked={variableShowALLOption}
onChange={(e): void => setVariableShowALLOption(e)}
/>
</VariableItemRow>
)}
</>
)}
<Divider />
<VariableItemRow>
<Button type="primary" onClick={handleSave} disabled={errorName}>
Save
</Button>
<Button type="dashed" onClick={onCancel}>
Cancel
</Button>
</VariableItemRow>
</Col>
);
}
export default VariableItem;

View File

@ -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;
`;

View File

@ -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<string | null>(null);
const [deleteVariableModal, setDeleteVariableModal] = useState(false);
const { dashboards } = useSelector<AppState, DashboardReducer>(
(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 | TVariableViewMode>(null);
const [
variableEditData,
setVariableEditData,
] = useState<null | IDashboardVariable>(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 => (
<Space>
<Button
type="text"
style={{ padding: 0, cursor: 'pointer', color: blue[5] }}
onClick={(): void => onVariableViewModeEnter('EDIT', _)}
>
Edit
</Button>
<Button
type="text"
style={{ padding: 0, color: red[6], cursor: 'pointer' }}
onClick={(): void => {
if (_.name) onVariableDeleteHandler(_.name);
}}
>
Delete
</Button>
</Space>
),
},
];
return (
<>
{variableViewMode ? (
<VariableItem
variableData={{ ...variableEditData } as IDashboardVariable}
onSave={onVariableSaveHandler}
onCancel={onDoneVariableViewMode}
validateName={validateVariableName}
variableViewMode={variableViewMode}
/>
) : (
<>
<Row style={{ flexDirection: 'row-reverse', padding: '0.5rem 0' }}>
<Button
type="primary"
onClick={(): void =>
onVariableViewModeEnter('ADD', {} as IDashboardVariable)
}
>
<PlusOutlined /> New Variables
</Button>
</Row>
<Table columns={columns} dataSource={variablesTableData} />
</>
)}
<Modal
title="Delete variable"
centered
visible={deleteVariableModal}
onOk={handleDeleteConfirm}
onCancel={handleDeleteCancel}
>
Are you sure you want to delete variable{' '}
<Tag>{variableToDelete.current}</Tag>?
</Modal>
</>
);
}
interface DispatchProps {
updateDashboardVariables: (
props: Record<string, IDashboardVariable>,
) => (dispatch: Dispatch<AppActions>) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
updateDashboardVariables: bindActionCreators(
UpdateDashboardVariables,
dispatch,
),
});
export default connect(null, mapDispatchToProps)(VariablesSetting);

View File

@ -0,0 +1 @@
export type TVariableViewMode = 'EDIT' | 'ADD';

View File

@ -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 (
<Tabs>
<TabPane tab="General" key="general">
<GeneralDashboardSettings />
</TabPane>
<TabPane tab="Variables" key="variables">
<VariablesSetting />
</TabPane>
</Tabs>
);
}
export default DashboardSettingsContent;

View File

@ -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<boolean>(false);
const [errorMessage, setErrorMessage] = useState<null | string>(null);
const getOptions = useCallback(async (): Promise<void> => {
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 (
<VariableContainer>
<VariableName>${variableData.name}</VariableName>
{variableData.type === 'TEXTBOX' ? (
<Input
placeholder="Enter value"
bordered={false}
value={variableData.selectedValue?.toString()}
onChange={(e): void => {
handleChange(e.target.value || '');
}}
style={{
width: 50 + ((variableData.selectedValue?.length || 0) * 7 || 50),
}}
/>
) : (
<Select
value={variableData.allSelected ? 'ALL' : variableData.selectedValue}
onChange={handleChange}
bordered={false}
placeholder="Select value"
mode={
(variableData.multiSelect && !variableData.allSelected
? 'multiple'
: null) as never
}
dropdownMatchSelectWidth={false}
style={{
minWidth: 120,
fontSize: '0.8rem',
}}
loading={isLoading}
showArrow
>
{variableData.multiSelect && variableData.showALLOption && (
<Option value={ALL_SELECT_VALUE}>ALL</Option>
)}
{map(optionsData, (option) => {
return <Option value={option}>{(option as string).toString()}</Option>;
})}
</Select>
)}
{errorMessage && (
<span style={{ margin: '0 0.5rem' }}>
<Popover placement="top" content={<Typography>{errorMessage}</Typography>}>
<WarningOutlined style={{ color: orange[5] }} />
</Popover>
</span>
)}
</VariableContainer>
);
}
export default VariableItem;

View File

@ -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<AppState, DashboardReducer>(
(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 (
<Row style={{ gap: '1rem' }}>
{map(sortBy(Object.keys(variables)), (variableName) => (
<VariableItem
key={`${variableName}${variables[variableName].modificationUUID}`}
variableData={{ name: variableName, ...variables[variableName] }}
onValueUpdate={onValueUpdate as never}
onAllSelectedUpdate={onAllSelectedUpdate as never}
/>
))}
</Row>
);
}
interface DispatchProps {
updateDashboardVariables: (
props: Parameters<typeof UpdateDashboardVariables>[0],
) => (dispatch: Dispatch<AppActions>) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
updateDashboardVariables: bindActionCreators(
UpdateDashboardVariables,
dispatch,
),
});
export default connect(null, mapDispatchToProps)(DashboardVariableSelection);

View File

@ -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]};
`;

View File

@ -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 (
<>
<Button type="dashed" onClick={showDrawer}>
<SettingOutlined /> Configure
</Button>
<DrawerContainer
placement="right"
width="70%"
onClose={onClose}
visible={visible}
maskClosable={false}
>
<DashboardSettingsContent />
</DrawerContainer>
</>
);
}
export default SettingsDrawer;

View File

@ -1,135 +1,69 @@
import { import { ShareAltOutlined } from '@ant-design/icons';
EditOutlined, import { Button, Card, Col, Row, Space, Tag, Typography } from 'antd';
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 useComponentPermission from 'hooks/useComponentPermission'; import useComponentPermission from 'hooks/useComponentPermission';
import React, { useCallback, useState } from 'react'; import React, { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { connect, useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import {
ToggleEditMode,
UpdateDashboardTitleDescriptionTags,
UpdateDashboardTitleDescriptionTagsProps,
} from 'store/actions';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import AppReducer from 'types/reducer/app'; import AppReducer from 'types/reducer/app';
import DashboardReducer from 'types/reducer/dashboards'; import DashboardReducer from 'types/reducer/dashboards';
import Description from './Description'; import DashboardVariableSelection from '../DashboardVariablesSelection';
import SettingsDrawer from './SettingsDrawer';
import ShareModal from './ShareModal'; import ShareModal from './ShareModal';
import { Button, Container } from './styles';
function DescriptionOfDashboard({ function DescriptionOfDashboard(): JSX.Element {
updateDashboardTitleDescriptionTags, const { dashboards } = useSelector<AppState, DashboardReducer>(
toggleEditMode,
}: DescriptionOfDashboardProps): JSX.Element {
const { dashboards, isEditMode } = useSelector<AppState, DashboardReducer>(
(state) => state.dashboards, (state) => state.dashboards,
); );
const [selectedDashboard] = dashboards; const [selectedDashboard] = dashboards;
const selectedData = selectedDashboard.data; const selectedData = selectedDashboard.data;
const { title } = selectedData; const { title, tags, description } = selectedData;
const { tags } = selectedData;
const { description } = selectedData;
const [updatedTitle, setUpdatedTitle] = useState<string>(title);
const [updatedTags, setUpdatedTags] = useState<string[]>(tags || []);
const [updatedDescription, setUpdatedDescription] = useState(
description || '',
);
const [isJSONModalVisible, isIsJSONModalVisible] = useState<boolean>(false); const [isJSONModalVisible, isIsJSONModalVisible] = useState<boolean>(false);
const { t } = useTranslation('common'); const { t } = useTranslation('common');
const { role } = useSelector<AppState, AppReducer>((state) => state.app); const { role } = useSelector<AppState, AppReducer>((state) => state.app);
const [editDashboard] = useComponentPermission(['edit_dashboard'], role); 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 => { const onToggleHandler = (): void => {
isIsJSONModalVisible((state) => !state); isIsJSONModalVisible((state) => !state);
}; };
return ( return (
<Card> <Card>
<Row align="top" justify="space-between"> <Row>
{!isEditMode ? ( <Col style={{ flex: 1 }}>
<Col> <Typography.Title level={4} style={{ padding: 0, margin: 0 }}>
<Typography>{title}</Typography> {title}
<Container> </Typography.Title>
{tags?.map((e) => ( <Typography>{description}</Typography>
<Tag key={e}>{e}</Tag> <div style={{ margin: '0.5rem 0' }}>
))} {tags?.map((e) => (
</Container> <Tag key={e}>{e}</Tag>
<Container> ))}
<Typography>{description}</Typography> </div>
</Container> <DashboardVariableSelection />
</Col> </Col>
) : (
<Col lg={8}>
<NameOfTheDashboard name={updatedTitle} setName={setUpdatedTitle} />
<AddTags tags={updatedTags} setTags={setUpdatedTags} />
<Description
description={updatedDescription}
setDescription={setUpdatedDescription}
/>
</Col>
)}
<ShareModal
{...{
isJSONModalVisible,
onToggleHandler,
selectedData,
}}
/>
<Col> <Col>
<ShareModal
{...{
isJSONModalVisible,
onToggleHandler,
selectedData,
}}
/>
<Space direction="vertical"> <Space direction="vertical">
<Button onClick={onToggleHandler} icon={<ShareAltOutlined />}> {editDashboard && <SettingsDrawer />}
<Button
style={{ width: '100%' }}
type="dashed"
onClick={onToggleHandler}
icon={<ShareAltOutlined />}
>
{t('share')} {t('share')}
</Button> </Button>
{editDashboard && (
<Button
icon={!isEditMode ? <EditOutlined /> : <SaveOutlined />}
onClick={onClickEditHandler}
>
{isEditMode ? t('save') : t('edit')}
</Button>
)}
</Space> </Space>
</Col> </Col>
</Row> </Row>
@ -137,23 +71,4 @@ function DescriptionOfDashboard({
); );
} }
interface DispatchProps { export default DescriptionOfDashboard;
updateDashboardTitleDescriptionTags: (
props: UpdateDashboardTitleDescriptionTagsProps,
) => (dispatch: Dispatch<AppActions>) => void;
toggleEditMode: () => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
updateDashboardTitleDescriptionTags: bindActionCreators(
UpdateDashboardTitleDescriptionTags,
dispatch,
),
toggleEditMode: bindActionCreators(ToggleEditMode, dispatch),
});
type DescriptionOfDashboardProps = DispatchProps;
export default connect(null, mapDispatchToProps)(DescriptionOfDashboard);

View File

@ -1,4 +1,4 @@
import { Button as ButtonComponent } from 'antd'; import { Button as ButtonComponent, Drawer } from 'antd';
import styled from 'styled-components'; import styled from 'styled-components';
export const Container = styled.div` export const Container = styled.div`
@ -11,3 +11,10 @@ export const Button = styled(ButtonComponent)`
align-items: center; align-items: center;
} }
`; `;
export const DrawerContainer = styled(Drawer)`
.ant-drawer-header {
padding: 0;
border: none;
}
`;

View File

@ -1,6 +1,7 @@
import { Button, Modal, Typography } from 'antd'; import { Button, Modal, Typography } from 'antd';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
import history from 'lib/history'; import history from 'lib/history';
import { DashboardWidgetPageParams } from 'pages/DashboardWidget'; import { DashboardWidgetPageParams } from 'pages/DashboardWidget';
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
@ -143,6 +144,7 @@ function NewWidget({
widgetId: selectedWidget?.id || '', widgetId: selectedWidget?.id || '',
graphType: selectedGraph, graphType: selectedGraph,
globalSelectedInterval, globalSelectedInterval,
variables: getDashboardVariables(),
}); });
} }
}, [ }, [

View File

@ -1,10 +1,14 @@
import React from 'react'; import React from 'react';
function Slack(): JSX.Element { interface ISlackProps {
width?: number;
height?: number;
}
function Slack({ width, height }: ISlackProps): JSX.Element {
return ( return (
<svg <svg
width="28" width={`${width}`}
height="28" height={`${height}`}
viewBox="0 0 28 28" viewBox="0 0 28 28"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -44,5 +48,9 @@ function Slack(): JSX.Element {
</svg> </svg>
); );
} }
Slack.defaultProps = {
width: 28,
height: 28,
};
export default Slack; export default Slack;

View File

@ -62,7 +62,7 @@ const menus: SidebarMenu[] = [
{ {
Icon: ApiOutlined, Icon: ApiOutlined,
to: ROUTES.INSTRUMENTATION, to: ROUTES.INSTRUMENTATION,
name: 'Add instrumentation', name: 'Get Started',
}, },
]; ];

View File

@ -8,7 +8,7 @@ const breadcrumbNameMap = {
[ROUTES.TRACE]: 'Traces', [ROUTES.TRACE]: 'Traces',
[ROUTES.SERVICE_MAP]: 'Service Map', [ROUTES.SERVICE_MAP]: 'Service Map',
[ROUTES.USAGE_EXPLORER]: 'Usage Explorer', [ROUTES.USAGE_EXPLORER]: 'Usage Explorer',
[ROUTES.INSTRUMENTATION]: 'Add instrumentation', [ROUTES.INSTRUMENTATION]: 'Get Started',
[ROUTES.SETTINGS]: 'Settings', [ROUTES.SETTINGS]: 'Settings',
[ROUTES.DASHBOARD]: 'Dashboard', [ROUTES.DASHBOARD]: 'Dashboard',
[ROUTES.ALL_ERROR]: 'Exceptions', [ROUTES.ALL_ERROR]: 'Exceptions',

View File

@ -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<AppState, TraceReducer>((state) => state.traces);
const dispatch = useDispatch<Dispatch<AppActions>>();
const globalTime = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const [isLoading, setIsLoading] = useState(false);
const [userEnteredValue, setUserEnteredValue] = useState<string>('');
useEffect(() => {
setUserEnteredValue(selectedFilter.get('traceID')?.[0] || '');
}, [selectedFilter]);
const onSearch = async (value: string): Promise<void> => {
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<HTMLInputElement>): void => {
setUserEnteredValue(e.target.value);
};
const onBlur = (): void => {
if (userEnteredValue !== selectedFilter.get('traceID')?.[0]) {
onSearch(userEnteredValue);
}
};
return (
<div>
<Search
placeholder="Filter by Trace ID"
onSearch={onSearch}
style={{
marginBottom: '5rem',
padding: '0 3%',
}}
loading={isLoading}
value={userEnteredValue}
onChange={onChange}
onBlur={onBlur}
/>
</div>
);
}
export default TraceID;

View File

@ -1,3 +1,4 @@
/* eslint-disable no-nested-ternary */
import { Card } from 'antd'; import { Card } from 'antd';
import Spinner from 'components/Spinner'; import Spinner from 'components/Spinner';
import React from 'react'; import React from 'react';
@ -7,6 +8,7 @@ import { TraceFilterEnum, TraceReducer } from 'types/reducer/trace';
import CommonCheckBox from './CommonCheckBox'; import CommonCheckBox from './CommonCheckBox';
import Duration from './Duration'; import Duration from './Duration';
import TraceID from './SearchTraceID';
function PanelBody(props: PanelBodyProps): JSX.Element { function PanelBody(props: PanelBodyProps): JSX.Element {
const { type } = props; const { type } = props;
@ -22,12 +24,17 @@ function PanelBody(props: PanelBodyProps): JSX.Element {
</Card> </Card>
); );
} }
const renderBody = (type: TraceFilterEnum): JSX.Element => {
return ( switch (type) {
<Card bordered={false}> case 'traceID':
{type === 'duration' ? <Duration /> : <CommonCheckBox name={type} />} return <TraceID />;
</Card> case 'duration':
); return <Duration />;
default:
return <CommonCheckBox name={type} />;
}
};
return <Card bordered={false}>{renderBody(type)}</Card>;
} }
interface PanelBodyProps { interface PanelBodyProps {

View File

@ -16,6 +16,7 @@ export const AllTraceFilterEnum: TraceFilterEnum[] = [
'httpMethod', 'httpMethod',
'httpRoute', 'httpRoute',
'httpUrl', 'httpUrl',
'traceID',
]; ];
function Filters(): JSX.Element { function Filters(): JSX.Element {

View File

@ -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),
);
};

View File

@ -0,0 +1,38 @@
import GetMinMax from 'lib/getMinMax';
import GetStartAndEndTime from 'lib/getStartAndEndTime';
import store from 'store';
export const getDashboardVariables = (): Record<string, unknown> => {
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<string, unknown> = {
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 {};
};

View File

@ -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;

View File

@ -6,6 +6,16 @@ import convertIntoEpoc from './covertIntoEpoc';
import { colors } from './getRandomColor'; import { colors } from './getRandomColor';
const getChartData = ({ queryData }: GetChartDataProps): ChartData => { const getChartData = ({ queryData }: GetChartDataProps): ChartData => {
const uniqueTimeLabels = new Set<number>();
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( const response = queryData.map(
({ queryData, query: queryG, legend: legendG }) => { ({ queryData, query: queryG, legend: legendG }) => {
return queryData.map((e) => { return queryData.map((e) => {
@ -22,11 +32,24 @@ const getChartData = ({ queryData }: GetChartDataProps): ChartData => {
second: Number(parseFloat(second)), 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 { return {
label: labelNames !== 'undefined' ? labelNames : '', label: labelNames !== 'undefined' ? labelNames : '',
first: dataValue.map((e) => e.first), first: filledDataValues.map((e) => e.first),
second: dataValue.map((e) => e.second), second: filledDataValues.map((e) => e.second),
}; };
}); });
}, },

View File

@ -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<AppState, AppReducer>((state) => state.app);
return (
<>
<Heading>Instrument your application</Heading>
<Container isDarkMode={isDarkMode}>
<Typography>Congrats, you have successfully installed SigNoz!</Typography>{' '}
<Typography>
To start seeing YOUR application data here, follow the instructions in the
docs -
</Typography>
<a
href="https://signoz.io/docs/instrumentation/overview"
target="_blank"
rel="noreferrer"
>
https://signoz.io/docs/instrumentation/overview
</a>
&nbsp;If you face any issues, join our
<a
href="https://signoz-community.slack.com/join/shared_invite/zt-lrjknbbp-J_mI13rlw8pGF4EWBnorJA"
target="_blank"
rel="noreferrer"
>
&nbsp;slack community&nbsp;
</a>
to ask any questions or mail us at&nbsp;
<a href="mailto:support@signoz.io" target="_blank" rel="noreferrer">
support@signoz.io
</a>
</Container>
</>
);
}
export default InstrumentationPage;

View File

@ -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<AppState, AppReducer>((state) => state.app);
return (
<Link to={{ pathname: `${url}${UTMParams}` }} target="_blank">
<DocCardContainer isDarkMode={isDarkMode}>
<span style={{ color: isDarkMode ? '#ddd' : '#333' }}>{icon}</span>
<Typography.Text style={{ marginLeft: '0.5rem' }}>{text}</Typography.Text>
</DocCardContainer>
</Link>
);
}
export default DocCard;

View File

@ -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 (
<div style={{ marginTop: '2rem' }}>
<Typography.Text strong>{sectionData.heading}</Typography.Text>
<Row
gutter={{ xs: 0, sm: 8, md: 16, lg: 24 }}
style={{ padding: '0 3%', marginTop: '0.5rem' }}
>
{sectionData.description && (
<Col span={24}>
<Typography.Text>{sectionData.description}</Typography.Text>
</Col>
)}
{map(sectionData.items, (item, idx) => (
<Col
key={`${item.title}+${idx}`}
sm={24}
md={12}
lg={12}
xl={8}
xxl={6}
style={{ margin: '1rem 0' }}
>
<DocCard icon={item.icon} text={item.title} url={item.url} />
</Col>
))}
</Row>
</div>
);
}
export default DocSection;

View File

@ -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 (
<>
<Typography>
Congrats, you have successfully installed SigNoz! Now lets get some data in
and start deriving insights from them
</Typography>
{GetStartedContent().map((section) => {
return <DocSection key={section.heading} sectionData={section} />;
})}
</>
);
}
export default InstrumentationPage;

View File

@ -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: (
<img src={`/Logos/java.png?currentVersion=${currentVersion}`} alt="" />
),
url: 'https://signoz.io/docs/instrumentation/java/',
},
{
title: 'Instrument your Python Application',
icon: (
<img src={`/Logos/python.png?currentVersion=${currentVersion}`} alt="" />
),
url: 'https://signoz.io/docs/instrumentation/python/',
},
{
title: 'Instrument your JS Application',
icon: (
<img
src={`/Logos/javascript.png?currentVersion=${currentVersion}`}
alt=""
/>
),
url: 'https://signoz.io/docs/instrumentation/javascript/',
},
{
title: 'Instrument your Go Application',
icon: (
<img src={`/Logos/go.png?currentVersion=${currentVersion}`} alt="" />
),
url: 'https://signoz.io/docs/instrumentation/golang/',
},
{
title: 'Instrument your .NET Application',
icon: (
<img
src={`/Logos/ms-net-framework.png?currentVersion=${currentVersion}`}
alt=""
/>
),
url: 'https://signoz.io/docs/instrumentation/dotnet/',
},
{
title: 'Instrument your PHP Application',
icon: (
<img src={`/Logos/php.png?currentVersion=${currentVersion}`} alt="" />
),
url: 'https://signoz.io/docs/instrumentation/php/',
},
{
title: 'Instrument your Rails Application',
icon: (
<img src={`/Logos/rails.png?currentVersion=${currentVersion}`} alt="" />
),
url: 'https://signoz.io/docs/instrumentation/ruby-on-rails/',
},
{
title: 'Instrument your Rust Application',
icon: (
<img src={`/Logos/rust.png?currentVersion=${currentVersion}`} alt="" />
),
url: 'https://signoz.io/docs/instrumentation/rust/',
},
{
title: 'Instrument your Elixir Application',
icon: (
<img src={`/Logos/elixir.png?currentVersion=${currentVersion}`} alt="" />
),
url: 'https://signoz.io/docs/instrumentation/elixir/',
},
],
},
{
heading: 'Send Metrics from your Infrastructure & create Dashboards',
items: [
{
title: 'Send metrics to SigNoz',
icon: <BarChartOutlined style={{ fontSize: '3.5rem' }} />,
url: 'https://signoz.io/docs/userguide/send-metrics/',
},
{
title: 'Create and Manage Dashboards',
icon: <DashboardFilled style={{ fontSize: '3.5rem' }} />,
url: 'https://signoz.io/docs/userguide/manage-dashboards-and-panels/',
},
],
},
{
heading: 'Send your logs to SigNoz',
items: [
{
title: 'Send your logs to SigNoz',
icon: <AlignLeftOutlined style={{ fontSize: '3.5rem' }} />,
url: 'https://signoz.io/docs/userguide/logs/',
},
{
title: 'Existing log collectors to SigNoz',
icon: <ApiFilled style={{ fontSize: '3.5rem' }} />,
url: 'https://signoz.io/docs/userguide/fluentbit_to_signoz/',
},
],
},
{
heading: 'Create alerts on Metrics',
items: [
{
title: 'Create alert rules on metrics',
icon: <AlertFilled style={{ fontSize: '3.5rem' }} />,
url: 'https://signoz.io/docs/userguide/alerts-management/',
},
{
title: 'Configure alert notification channels',
icon: <SoundFilled style={{ fontSize: '3.5rem' }} />,
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 '}
<Typography.Link
href="https://signoz-community.slack.com/archives/C01HWUTP4HH"
target="_blank"
>
#support
</Typography.Link>
{' or '}
<Typography.Link
href="https://signoz-community.slack.com/archives/C01HWQ1R0BC"
target="_blank"
>
#general
</Typography.Link>
</>
),
items: [
{
title: 'Join SigNoz slack community ',
icon: (
<div style={{ padding: '0.7rem' }}>
<Slack width={30} height={30} />
</div>
),
url: 'https://signoz.io/slack',
},
],
},
];
};

View File

@ -1,4 +1,4 @@
import { Card, Typography } from 'antd'; import { Card, Row, Typography } from 'antd';
import styled from 'styled-components'; import styled from 'styled-components';
interface Props { interface Props {
@ -18,3 +18,13 @@ export const Heading = styled(Typography)`
margin-bottom: 1rem; 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;
`;

View File

@ -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[];
};

View File

@ -0,0 +1,3 @@
const UTMParams =
'?utm_source=instrumentation_page&utm_medium=frontend&utm_term=language';
export default UTMParams;

View File

@ -30,6 +30,7 @@ export const DeleteWidget = ({
tags: selectedDashboard.data.tags, tags: selectedDashboard.data.tags,
widgets: updatedWidgets, widgets: updatedWidgets,
layout: updatedLayout, layout: updatedLayout,
variables: selectedDashboard.data.variables,
}, },
uuid: selectedDashboard.uuid, uuid: selectedDashboard.uuid,
}; };

View File

@ -19,7 +19,7 @@ import { Dispatch } from 'redux';
import store from 'store'; import store from 'store';
import AppActions from 'types/actions'; import AppActions from 'types/actions';
import { ErrorResponse, SuccessResponse } from 'types/api'; 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 { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { EDataSource, EPanelType, EQueryType } from 'types/common/dashboard'; import { EDataSource, EPanelType, EQueryType } from 'types/common/dashboard';
import { GlobalReducer } from 'types/reducer/globalTime'; import { GlobalReducer } from 'types/reducer/globalTime';
@ -29,11 +29,13 @@ export async function GetMetricQueryRange({
globalSelectedInterval, globalSelectedInterval,
graphType, graphType,
selectedTime, selectedTime,
variables = {},
}: { }: {
query: Query; query: Query;
graphType: GRAPH_TYPES; graphType: GRAPH_TYPES;
selectedTime: timePreferenceType; selectedTime: timePreferenceType;
globalSelectedInterval: Time; globalSelectedInterval: Time;
variables?: Record<string, unknown>;
}): Promise<SuccessResponse<MetricRangePayloadProps> | ErrorResponse> { }): Promise<SuccessResponse<MetricRangePayloadProps> | ErrorResponse> {
const { queryType } = query; const { queryType } = query;
const queryKey: Record<EQueryTypeToQueryKeyMapping, string> = const queryKey: Record<EQueryTypeToQueryKeyMapping, string> =
@ -138,6 +140,7 @@ export async function GetMetricQueryRange({
start: parseInt(start, 10) * 1e3, start: parseInt(start, 10) * 1e3,
end: parseInt(end, 10) * 1e3, end: parseInt(end, 10) * 1e3,
step: getStep({ start, end, inputFormat: 'ms' }), step: getStep({ start, end, inputFormat: 'ms' }),
variables,
...QueryPayload, ...QueryPayload,
}); });
if (response.statusCode >= 400) { if (response.statusCode >= 400) {
@ -173,6 +176,14 @@ export const GetQueryResults = (
): ((dispatch: Dispatch<AppActions>) => void) => { ): ((dispatch: Dispatch<AppActions>) => void) => {
return async (dispatch: Dispatch<AppActions>): Promise<void> => { return async (dispatch: Dispatch<AppActions>): Promise<void> => {
try { try {
dispatch({
type: 'QUERY_ERROR',
payload: {
errorMessage: '',
widgetId: props.widgetId,
errorBoolean: false,
},
});
const response = await GetMetricQueryRange(props); const response = await GetMetricQueryRange(props);
const isError = response.error; const isError = response.error;
@ -199,14 +210,6 @@ export const GetQueryResults = (
}, },
}, },
}); });
dispatch({
type: 'QUERY_ERROR',
payload: {
errorMessage: '',
widgetId: props.widgetId,
errorBoolean: false,
},
});
} catch (error) { } catch (error) {
dispatch({ dispatch({
type: 'QUERY_ERROR', type: 'QUERY_ERROR',
@ -226,4 +229,5 @@ export interface GetQueryResultsProps {
query: Query; query: Query;
graphType: ITEMS; graphType: ITEMS;
globalSelectedInterval: GlobalReducer['selectedTime']; globalSelectedInterval: GlobalReducer['selectedTime'];
variables: Record<string, unknown>;
} }

View File

@ -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<string, IDashboardVariable>,
): ((dispatch: Dispatch<AppActions>) => void) => {
return async (dispatch: Dispatch<AppActions>): Promise<void> => {
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);
}
};
};

View File

@ -18,6 +18,7 @@ import {
SAVE_SETTING_TO_PANEL_SUCCESS, SAVE_SETTING_TO_PANEL_SUCCESS,
TOGGLE_EDIT_MODE, TOGGLE_EDIT_MODE,
UPDATE_DASHBOARD, UPDATE_DASHBOARD,
UPDATE_DASHBOARD_VARIABLES,
UPDATE_QUERY, UPDATE_QUERY,
UPDATE_TITLE_DESCRIPTION_TAGS_SUCCESS, UPDATE_TITLE_DESCRIPTION_TAGS_SUCCESS,
} from 'types/actions/dashboard'; } from 'types/actions/dashboard';
@ -170,7 +171,6 @@ const dashboard = (
case QUERY_ERROR: { case QUERY_ERROR: {
const { widgetId, errorMessage, errorBoolean = true } = action.payload; const { widgetId, errorMessage, errorBoolean = true } = action.payload;
const [selectedDashboard] = state.dashboards; const [selectedDashboard] = state.dashboards;
const { data } = selectedDashboard; 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: default:
return state; return state;
} }

View File

@ -68,6 +68,7 @@ const initialValue: TraceReducer = {
['responseStatusCode', INITIAL_FILTER_VALUE], ['responseStatusCode', INITIAL_FILTER_VALUE],
['serviceName', INITIAL_FILTER_VALUE], ['serviceName', INITIAL_FILTER_VALUE],
['status', INITIAL_FILTER_VALUE], ['status', INITIAL_FILTER_VALUE],
['traceID', INITIAL_FILTER_VALUE],
]), ]),
}; };

View File

@ -1,6 +1,11 @@
import { Layout } from 'react-grid-layout'; import { Layout } from 'react-grid-layout';
import { ApplySettingsToPanelProps } from 'store/actions/dashboard/applySettingsToPanel'; 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'; import { QueryData } from 'types/api/widgets/getQuery';
export const GET_DASHBOARD = 'GET_DASHBOARD'; 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 DELETE_QUERY = 'DELETE_QUERY';
export const FLUSH_DASHBOARD = 'FLUSH_DASHBOARD'; export const FLUSH_DASHBOARD = 'FLUSH_DASHBOARD';
export const UPDATE_DASHBOARD_VARIABLES = 'UPDATE_DASHBOARD_VARIABLES';
interface GetDashboard { interface GetDashboard {
type: typeof GET_DASHBOARD; type: typeof GET_DASHBOARD;
payload: Dashboard; payload: Dashboard;
@ -174,6 +181,10 @@ interface DeleteQuery {
interface FlushDashboard { interface FlushDashboard {
type: typeof FLUSH_DASHBOARD; type: typeof FLUSH_DASHBOARD;
} }
interface UpdateDashboardVariables {
type: typeof UPDATE_DASHBOARD_VARIABLES;
payload: Record<string, IDashboardVariable>;
}
export type DashboardActions = export type DashboardActions =
| GetDashboard | GetDashboard
@ -194,4 +205,5 @@ export type DashboardActions =
| IsAddWidget | IsAddWidget
| UpdateQuery | UpdateQuery
| DeleteQuery | DeleteQuery
| FlushDashboard; | FlushDashboard
| UpdateDashboardVariables;

View File

@ -11,6 +11,31 @@ import { QueryData } from '../widgets/getQuery';
export type PayloadProps = Dashboard[]; 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 { export interface Dashboard {
id: number; id: number;
uuid: string; uuid: string;
@ -26,6 +51,7 @@ export interface DashboardData {
widgets?: Widgets[]; widgets?: Widgets[];
title: string; title: string;
layout?: Layout[]; layout?: Layout[];
variables: Record<string, IDashboardVariable>;
} }
export interface IBaseWidget { export interface IBaseWidget {

View File

@ -0,0 +1,7 @@
export type Props = {
query: string;
};
export type PayloadProps = {
variableValues: string[] | number[];
};

View File

@ -5,5 +5,6 @@ export interface MetricRangePayloadProps {
data: { data: {
result: QueryData[]; result: QueryData[];
resultType: string; resultType: string;
variables: Record<string, unknown>;
}; };
} }

View File

@ -71,7 +71,8 @@ export type TraceFilterEnum =
| 'serviceName' | 'serviceName'
| 'status' | 'status'
| 'responseStatusCode' | 'responseStatusCode'
| 'rpcMethod'; | 'rpcMethod'
| 'traceID';
export const AllPanelHeading: { export const AllPanelHeading: {
key: TraceFilterEnum; key: TraceFilterEnum;
@ -125,4 +126,8 @@ export const AllPanelHeading: {
key: 'status', key: 'status',
displayValue: 'Status', displayValue: 'Status',
}, },
{
key: 'traceID',
displayValue: 'Trace ID',
},
]; ];

View File

@ -94,6 +94,7 @@ type ClickHouseReader struct {
logsResourceKeys string logsResourceKeys string
queryEngine *promql.Engine queryEngine *promql.Engine
remoteStorage *remote.Storage remoteStorage *remote.Storage
fanoutStorage *storage.Storage
promConfigFile string promConfigFile string
promConfig *config.Config 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 := promlog.AllowedLevel{}
logLevel.Set("debug") logLevel.Set("debug")
// allowedFormat := promlog.AllowedFormat{} // allowedFormat := promlog.AllowedFormat{}
@ -311,6 +312,8 @@ func (r *ClickHouseReader) Start() {
} }
r.queryEngine = queryEngine r.queryEngine = queryEngine
r.remoteStorage = remoteStorage r.remoteStorage = remoteStorage
r.fanoutStorage = &fanoutStorage
readerReady <- true
if err := g.Run(); err != nil { if err := g.Run(); err != nil {
level.Error(logger).Log("err", err) 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) { 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) 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))} 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 { if len(queryParams.ServiceName) > 0 {
args = buildFilterArrayQuery(ctx, excludeMap, queryParams.ServiceName, constants.ServiceName, &query, args) 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 { for _, e := range queryParams.GetFilters {
switch e { switch e {
case constants.TraceID:
continue
case constants.ServiceName: 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 := fmt.Sprintf("SELECT serviceName, count() as count FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", r.traceDB, r.indexTable)
finalQuery += query finalQuery += query
@ -1260,6 +1276,9 @@ func (r *ClickHouseReader) GetFilteredSpans(ctx context.Context, queryParams *mo
var query string var query string
args := []interface{}{clickhouse.Named("timestampL", strconv.FormatInt(queryParams.Start.UnixNano(), 10)), clickhouse.Named("timestampU", strconv.FormatInt(queryParams.End.UnixNano(), 10))} 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 { if len(queryParams.ServiceName) > 0 {
args = buildFilterArrayQuery(ctx, excludeMap, queryParams.ServiceName, constants.ServiceName, &query, args) 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 var query string
args := []interface{}{clickhouse.Named("timestampL", strconv.FormatInt(queryParams.Start.UnixNano(), 10)), clickhouse.Named("timestampU", strconv.FormatInt(queryParams.End.UnixNano(), 10))} 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 { if len(queryParams.ServiceName) > 0 {
args = buildFilterArrayQuery(ctx, excludeMap, queryParams.ServiceName, constants.ServiceName, &query, args) 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 var query string
args := []interface{}{clickhouse.Named("timestampL", strconv.FormatInt(queryParams.Start.UnixNano(), 10)), clickhouse.Named("timestampU", strconv.FormatInt(queryParams.End.UnixNano(), 10))} 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 { if len(queryParams.ServiceName) > 0 {
args = buildFilterArrayQuery(ctx, excludeMap, queryParams.ServiceName, constants.ServiceName, &query, args) 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) 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 { if len(queryParams.ServiceName) > 0 {
args = buildFilterArrayQuery(ctx, excludeMap, queryParams.ServiceName, constants.ServiceName, &query, args) 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 { if err != nil {
zap.S().Debug("Error in processing query: ", err) zap.S().Debug("Error in processing query: ", err)
return nil, fmt.Errorf("error in processing query") return nil, err
} }
var ( var (
@ -3239,3 +3267,39 @@ func (r *ClickHouseReader) AggregateLogs(ctx context.Context, params *model.Logs
return &aggregateResponse, nil 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
}

View File

@ -9,7 +9,9 @@ import (
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"strconv" "strconv"
"strings"
"sync" "sync"
"text/template"
"time" "time"
"github.com/gorilla/mux" "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}", 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.updateDashboard)).Methods(http.MethodPut)
router.HandleFunc("/api/v1/dashboards/{uuid}", EditAccess(aH.deleteDashboard)).Methods(http.MethodDelete) 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/feedback", OpenAccess(aH.submitFeedback)).Methods(http.MethodPost)
// router.HandleFunc("/api/v1/get_percentiles", aH.getApplicationPercentiles).Methods(http.MethodGet) // 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 { type channelResult struct {
Series []*model.Series Series []*model.Series
Err error 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 var seriesList []*model.Series
ch := make(chan channelResult, len(queries)) ch := make(chan channelResult, len(queries))
var wg sync.WaitGroup var wg sync.WaitGroup
@ -500,7 +505,7 @@ func (aH *APIHandler) queryRangeMetricsV2(w http.ResponseWriter, r *http.Request
} }
if err != nil { 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 return
} }
ch <- channelResult{Series: seriesList} ch <- channelResult{Series: seriesList}
@ -511,21 +516,23 @@ func (aH *APIHandler) queryRangeMetricsV2(w http.ResponseWriter, r *http.Request
close(ch) close(ch)
var errs []error var errs []error
errQuriesByName := make(map[string]string)
// read values from the channel // read values from the channel
for r := range ch { for r := range ch {
if r.Err != nil { if r.Err != nil {
errs = append(errs, r.Err) errs = append(errs, r.Err)
errQuriesByName[r.Name] = r.Query
continue continue
} }
seriesList = append(seriesList, r.Series...) seriesList = append(seriesList, r.Series...)
} }
if len(errs) != 0 { 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 var seriesList []*model.Series
ch := make(chan channelResult, len(metricsQueryRangeParams.CompositeMetricQuery.PromQueries)) ch := make(chan channelResult, len(metricsQueryRangeParams.CompositeMetricQuery.PromQueries))
var wg sync.WaitGroup 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) { go func(name string, query *model.PromQuery) {
var seriesList []*model.Series var seriesList []*model.Series
defer wg.Done() 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{ queryModel := model.QueryRangeParams{
Start: time.UnixMilli(metricsQueryRangeParams.Start), Start: time.UnixMilli(metricsQueryRangeParams.Start),
End: time.UnixMilli(metricsQueryRangeParams.End), 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) promResult, _, err := (*aH.reader).GetQueryRangeResult(r.Context(), &queryModel)
if err != nil { 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 return
} }
matrix, _ := promResult.Matrix() matrix, _ := promResult.Matrix()
@ -567,22 +587,25 @@ func (aH *APIHandler) queryRangeMetricsV2(w http.ResponseWriter, r *http.Request
close(ch) close(ch)
var errs []error var errs []error
errQuriesByName := make(map[string]string)
// read values from the channel // read values from the channel
for r := range ch { for r := range ch {
if r.Err != nil { if r.Err != nil {
errs = append(errs, r.Err) errs = append(errs, r.Err)
errQuriesByName[r.Name] = r.Query
continue continue
} }
seriesList = append(seriesList, r.Series...) seriesList = append(seriesList, r.Series...)
} }
if len(errs) != 0 { 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 seriesList []*model.Series
var err error var err error
var errQuriesByName map[string]string
switch metricsQueryRangeParams.CompositeMetricQuery.QueryType { switch metricsQueryRangeParams.CompositeMetricQuery.QueryType {
case model.QUERY_BUILDER: case model.QUERY_BUILDER:
runQueries := metrics.PrepareBuilderMetricQueries(metricsQueryRangeParams, constants.SIGNOZ_TIMESERIES_TABLENAME) 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) respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: runQueries.Err}, nil)
return return
} }
seriesList, err = execClickHouseQueries(runQueries.Queries) seriesList, err, errQuriesByName = execClickHouseQueries(runQueries.Queries)
case model.CLICKHOUSE: case model.CLICKHOUSE:
queries := make(map[string]string) queries := make(map[string]string)
@ -598,20 +621,32 @@ func (aH *APIHandler) queryRangeMetricsV2(w http.ResponseWriter, r *http.Request
if chQuery.Disabled { if chQuery.Disabled {
continue 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: case model.PROM:
seriesList, err = execPromQueries(metricsQueryRangeParams) seriesList, err, errQuriesByName = execPromQueries(metricsQueryRangeParams)
default: default:
err = fmt.Errorf("invalid query type") 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 return
} }
if err != nil { if err != nil {
apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err} apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err}
respondError(w, apiErrObj, nil) respondError(w, apiErrObj, errQuriesByName)
return return
} }
if metricsQueryRangeParams.CompositeMetricQuery.PanelType == model.QUERY_VALUE && 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) { func (aH *APIHandler) updateDashboard(w http.ResponseWriter, r *http.Request) {
uuid := mux.Vars(r)["uuid"] uuid := mux.Vars(r)["uuid"]
@ -1034,11 +1088,11 @@ func (aH *APIHandler) queryRangeMetrics(w http.ResponseWriter, r *http.Request)
if res.Err != nil { if res.Err != nil {
switch res.Err.(type) { switch res.Err.(type) {
case promql.ErrQueryCanceled: 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: 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{ response_data := &model.QueryData{
@ -1088,11 +1142,11 @@ func (aH *APIHandler) queryMetrics(w http.ResponseWriter, r *http.Request) {
if res.Err != nil { if res.Err != nil {
switch res.Err.(type) { switch res.Err.(type) {
case promql.ErrQueryCanceled: 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: 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{ response_data := &model.QueryData{

View File

@ -8,6 +8,7 @@ import (
"github.com/SigNoz/govaluate" "github.com/SigNoz/govaluate"
"go.signoz.io/query-service/constants" "go.signoz.io/query-service/constants"
"go.signoz.io/query-service/model" "go.signoz.io/query-service/model"
"go.uber.org/zap"
) )
type RunQueries struct { type RunQueries struct {
@ -50,8 +51,8 @@ func GoValuateFuncs() map[string]govaluate.ExpressionFunction {
return GoValuateFuncs return GoValuateFuncs
} }
// formattedValue formats the value to be used in clickhouse query // FormattedValue formats the value to be used in clickhouse query
func formattedValue(v interface{}) string { func FormattedValue(v interface{}) string {
switch x := v.(type) { switch x := v.(type) {
case int: case int:
return fmt.Sprintf("%d", x) return fmt.Sprintf("%d", x)
@ -62,6 +63,9 @@ func formattedValue(v interface{}) string {
case bool: case bool:
return fmt.Sprintf("%v", x) return fmt.Sprintf("%v", x)
case []interface{}: case []interface{}:
if len(x) == 0 {
return ""
}
switch x[0].(type) { switch x[0].(type) {
case string: case string:
str := "[" str := "["
@ -75,10 +79,12 @@ func formattedValue(v interface{}) string {
return str return str
case int, float32, float64, bool: case int, float32, float64, bool:
return strings.Join(strings.Fields(fmt.Sprint(x)), ",") 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: default:
// may be log the warning here? zap.L().Error("invalid type for formatted value", zap.Any("type", reflect.TypeOf(x)))
return "" return ""
} }
} }
@ -87,7 +93,7 @@ func formattedValue(v interface{}) string {
// timeseries based on search criteria // timeseries based on search criteria
func BuildMetricsTimeSeriesFilterQuery(fs *model.FilterSet, groupTags []string, metricName string, aggregateOperator model.AggregateOperator) (string, error) { func BuildMetricsTimeSeriesFilterQuery(fs *model.FilterSet, groupTags []string, metricName string, aggregateOperator model.AggregateOperator) (string, error) {
var conditions []string 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 { if fs != nil && len(fs.Items) != 0 {
for _, item := range fs.Items { for _, item := range fs.Items {
toFormat := item.Value toFormat := item.Value
@ -102,7 +108,7 @@ func BuildMetricsTimeSeriesFilterQuery(fs *model.FilterSet, groupTags []string,
toFormat = x[0] toFormat = x[0]
} }
} }
fmtVal := formattedValue(toFormat) fmtVal := FormattedValue(toFormat)
switch op { switch op {
case "eq": case "eq":
conditions = append(conditions, fmt.Sprintf("labels_object.%s = %s", item.Key, fmtVal)) 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 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 // Select the aggregate value for interval
queryTmpl := queryTmpl :=
@ -419,3 +425,31 @@ func PrepareBuilderMetricQueries(qp *model.QueryRangeParamsV2, tableName string)
} }
return &RunQueries{Queries: namedQueries} 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 ""
}
}

View File

@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"strings"
"go.signoz.io/query-service/app/metrics" "go.signoz.io/query-service/app/metrics"
"go.signoz.io/query-service/model" "go.signoz.io/query-service/model"
@ -36,6 +37,44 @@ func ParseMetricQueryRangeParams(r *http.Request) (*model.QueryRangeParamsV2, *m
if err := validateQueryRangeParamsV2(postData); err != nil { if err := validateQueryRangeParamsV2(postData); err != nil {
return nil, &model.ApiError{Typ: model.ErrorBadData, Err: err} 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 return postData, nil
} }

View File

@ -77,18 +77,20 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
} }
localDB.SetMaxOpenConns(10) localDB.SetMaxOpenConns(10)
readerReady := make(chan bool)
var reader interfaces.Reader var reader interfaces.Reader
storage := os.Getenv("STORAGE") storage := os.Getenv("STORAGE")
if storage == "clickhouse" { if storage == "clickhouse" {
zap.S().Info("Using ClickHouse as datastore ...") zap.S().Info("Using ClickHouse as datastore ...")
clickhouseReader := clickhouseReader.NewReader(localDB, serverOptions.PromConfigPath) clickhouseReader := clickhouseReader.NewReader(localDB, serverOptions.PromConfigPath)
go clickhouseReader.Start() go clickhouseReader.Start(readerReady)
reader = clickhouseReader reader = clickhouseReader
} else { } else {
return nil, fmt.Errorf("Storage type: %s is not supported in query service", storage) 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) rm, err := makeRulesManager(serverOptions.PromConfigPath, constants.GetAlertManagerApiPrefix(), serverOptions.RuleRepoURL, localDB, reader, serverOptions.DisableRules)
if err != nil { if err != nil {
return nil, err return nil, err
@ -232,9 +234,10 @@ func (s *Server) analyticsMiddleware(next http.Handler) http.Handler {
next.ServeHTTP(lrw, r) next.ServeHTTP(lrw, r)
data := map[string]interface{}{"path": path, "statusCode": lrw.statusCode} data := map[string]interface{}{"path": path, "statusCode": lrw.statusCode}
if telemetry.GetInstance().IsSampled() {
if _, ok := telemetry.IgnoredPaths()[path]; !ok { if _, ok := telemetry.IgnoredPaths()[path]; !ok {
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_PATH, data) telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_PATH, data)
}
} }
}) })
@ -361,7 +364,7 @@ func makeRulesManager(
disableRules bool) (*rules.Manager, error) { disableRules bool) (*rules.Manager, error) {
// create engine // create engine
pqle, err := pqle.FromConfigPath(promConfigPath) pqle, err := pqle.FromReader(ch)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create pql engine : %v", err) return nil, fmt.Errorf("failed to create pql engine : %v", err)
} }

View File

@ -19,8 +19,7 @@ rule_files:
# A scrape configuration containing exactly one endpoint to scrape: # A scrape configuration containing exactly one endpoint to scrape:
# Here it's Prometheus itself. # Here it's Prometheus itself.
scrape_configs: scrape_configs: []
remote_read: remote_read:
- url: tcp://localhost:9000/?database=signoz_metrics - url: tcp://localhost:9000/?database=signoz_metrics

View File

@ -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") var RELATIONAL_DATASOURCE_PATH = GetOrDefaultEnv("SIGNOZ_LOCAL_DB_PATH", "/var/lib/signoz/signoz.db")
const ( const (
TraceID = "traceID"
ServiceName = "serviceName" ServiceName = "serviceName"
HttpRoute = "httpRoute" HttpRoute = "httpRoute"
HttpCode = "httpCode" HttpCode = "httpCode"

View File

@ -34,6 +34,7 @@ require (
github.com/minio/md5-simd v1.1.0 // indirect github.com/minio/md5-simd v1.1.0 // indirect
github.com/minio/sha256-simd v0.1.1 // indirect github.com/minio/sha256-simd v0.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // 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/ini.v1 v1.42.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
) )

View File

@ -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/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/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/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/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.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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 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_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= 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/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 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= 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-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 h1:p3Vo3i64TCLY7gIfzeQaUJ+kppEO5WQG3cL8iE8tGHU=
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= 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 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 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/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/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.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= 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.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc=
github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= 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= github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g=

View File

@ -5,6 +5,7 @@ import (
"github.com/ClickHouse/clickhouse-go/v2" "github.com/ClickHouse/clickhouse-go/v2"
"github.com/prometheus/prometheus/promql" "github.com/prometheus/prometheus/promql"
"github.com/prometheus/prometheus/storage"
"github.com/prometheus/prometheus/util/stats" "github.com/prometheus/prometheus/util/stats"
am "go.signoz.io/query-service/integrations/alertManager" am "go.signoz.io/query-service/integrations/alertManager"
"go.signoz.io/query-service/model" "go.signoz.io/query-service/model"
@ -70,4 +71,8 @@ type Reader interface {
// Connection needed for rules, not ideal but required // Connection needed for rules, not ideal but required
GetConn() clickhouse.Conn GetConn() clickhouse.Conn
GetQueryEngine() *promql.Engine
GetFanoutStorage() *storage.Storage
QueryDashboardVars(ctx context.Context, query string) (*model.DashboardVar, error)
} }

View File

@ -118,11 +118,12 @@ const (
) )
type QueryRangeParamsV2 struct { type QueryRangeParamsV2 struct {
DataSource DataSource `json:"dataSource"` DataSource DataSource `json:"dataSource"`
Start int64 `json:"start"` Start int64 `json:"start"`
End int64 `json:"end"` End int64 `json:"end"`
Step int64 `json:"step"` Step int64 `json:"step"`
CompositeMetricQuery *CompositeMetricQuery `json:"compositeMetricQuery"` CompositeMetricQuery *CompositeMetricQuery `json:"compositeMetricQuery"`
Variables map[string]interface{} `json:"variables,omitempty"`
} }
// Metric auto complete types // Metric auto complete types
@ -181,6 +182,7 @@ type TagQuery struct {
} }
type GetFilteredSpansParams struct { type GetFilteredSpansParams struct {
TraceID []string `json:"traceID"`
ServiceName []string `json:"serviceName"` ServiceName []string `json:"serviceName"`
Operation []string `json:"operation"` Operation []string `json:"operation"`
Kind string `json:"kind"` Kind string `json:"kind"`
@ -208,6 +210,7 @@ type GetFilteredSpansParams struct {
} }
type GetFilteredSpanAggregatesParams struct { type GetFilteredSpanAggregatesParams struct {
TraceID []string `json:"traceID"`
ServiceName []string `json:"serviceName"` ServiceName []string `json:"serviceName"`
Operation []string `json:"operation"` Operation []string `json:"operation"`
Kind string `json:"kind"` Kind string `json:"kind"`
@ -236,6 +239,7 @@ type GetFilteredSpanAggregatesParams struct {
} }
type SpanFilterParams struct { type SpanFilterParams struct {
TraceID []string `json:"traceID"`
Status []string `json:"status"` Status []string `json:"status"`
ServiceName []string `json:"serviceName"` ServiceName []string `json:"serviceName"`
HttpRoute []string `json:"httpRoute"` HttpRoute []string `json:"httpRoute"`
@ -258,6 +262,7 @@ type SpanFilterParams struct {
} }
type TagFilterParams struct { type TagFilterParams struct {
TraceID []string `json:"traceID"`
Status []string `json:"status"` Status []string `json:"status"`
ServiceName []string `json:"serviceName"` ServiceName []string `json:"serviceName"`
HttpRoute []string `json:"httpRoute"` HttpRoute []string `json:"httpRoute"`

View File

@ -492,3 +492,7 @@ func (s *ServiceItem) MarshalJSON() ([]byte, error) {
Alias: (*Alias)(s), Alias: (*Alias)(s),
}) })
} }
type DashboardVar struct {
VariableValues []interface{} `json:"variableValues"`
}

View File

@ -3,6 +3,8 @@ package promql
import ( import (
"context" "context"
"fmt" "fmt"
"time"
"github.com/go-kit/log" "github.com/go-kit/log"
pmodel "github.com/prometheus/common/model" pmodel "github.com/prometheus/common/model"
plog "github.com/prometheus/common/promlog" plog "github.com/prometheus/common/promlog"
@ -11,7 +13,7 @@ import (
pql "github.com/prometheus/prometheus/promql" pql "github.com/prometheus/prometheus/promql"
pstorage "github.com/prometheus/prometheus/storage" pstorage "github.com/prometheus/prometheus/storage"
premote "github.com/prometheus/prometheus/storage/remote" premote "github.com/prometheus/prometheus/storage/remote"
"time" "go.signoz.io/query-service/interfaces"
) )
type PqlEngine struct { type PqlEngine struct {
@ -29,6 +31,13 @@ func FromConfigPath(promConfigPath string) (*PqlEngine, error) {
return NewPqlEngine(c) 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) { func NewPqlEngine(config *pconfig.Config) (*PqlEngine, error) {
logLevel := plog.AllowedLevel{} logLevel := plog.AllowedLevel{}

View File

@ -3,11 +3,14 @@ package telemetry
import ( import (
"context" "context"
"io/ioutil" "io/ioutil"
"math/rand"
"net/http" "net/http"
"os" "os"
"strings"
"sync" "sync"
"time" "time"
ph "github.com/posthog/posthog-go"
"go.signoz.io/query-service/constants" "go.signoz.io/query-service/constants"
"go.signoz.io/query-service/interfaces" "go.signoz.io/query-service/interfaces"
"go.signoz.io/query-service/model" "go.signoz.io/query-service/model"
@ -16,15 +19,19 @@ import (
) )
const ( const (
TELEMETRY_EVENT_PATH = "API Call" TELEMETRY_EVENT_PATH = "API Call"
TELEMETRY_EVENT_USER = "User" TELEMETRY_EVENT_USER = "User"
TELEMETRY_EVENT_INPRODUCT_FEEDBACK = "InProduct Feeback Submitted" TELEMETRY_EVENT_INPRODUCT_FEEDBACK = "InProduct Feeback Submitted"
TELEMETRY_EVENT_NUMBER_OF_SERVICES = "Number of Services" TELEMETRY_EVENT_NUMBER_OF_SERVICES = "Number of Services"
TELEMETRY_EVENT_HEART_BEAT = "Heart Beat" TELEMETRY_EVENT_NUMBER_OF_SERVICES_PH = "Number of Services V2"
TELEMETRY_EVENT_ORG_SETTINGS = "Org Settings" TELEMETRY_EVENT_HEART_BEAT = "Heart Beat"
TELEMETRY_EVENT_ORG_SETTINGS = "Org Settings"
DEFAULT_SAMPLING = 0.1
) )
const api_key = "4Gmoa4ixJAUHx2BpJxsjwA1bEfnwEeRz" const api_key = "4Gmoa4ixJAUHx2BpJxsjwA1bEfnwEeRz"
const ph_api_key = "H-htDCae7CR3RV57gUzmol6IAKtm5IMCvbcm_fwnL-w"
const IP_NOT_FOUND_PLACEHOLDER = "NA" const IP_NOT_FOUND_PLACEHOLDER = "NA"
const HEART_BEAT_DURATION = 6 * time.Hour const HEART_BEAT_DURATION = 6 * time.Hour
@ -34,20 +41,41 @@ const HEART_BEAT_DURATION = 6 * time.Hour
var telemetry *Telemetry var telemetry *Telemetry
var once sync.Once 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 { type Telemetry struct {
operator analytics.Client operator analytics.Client
ipAddress string phOperator ph.Client
isEnabled bool ipAddress string
isAnonymous bool isEnabled bool
distinctId string isAnonymous bool
reader interfaces.Reader distinctId string
reader interfaces.Reader
companyDomain string
minRandInt int
maxRandInt int
} }
func createTelemetry() { func createTelemetry() {
telemetry = &Telemetry{ telemetry = &Telemetry{
operator: analytics.New(api_key), operator: analytics.New(api_key),
ipAddress: getOutboundIP(), 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{}{} data := map[string]interface{}{}
@ -106,13 +134,36 @@ func (a *Telemetry) IdentifyUser(user *model.User) {
if !a.isTelemetryEnabled() || a.isTelemetryAnonymous() { if !a.isTelemetryEnabled() || a.isTelemetryAnonymous() {
return return
} }
a.setCompanyDomain(user.Email)
a.operator.Enqueue(analytics.Identify{ a.operator.Enqueue(analytics.Identify{
UserId: a.ipAddress, UserId: a.ipAddress,
Traits: analytics.NewTraits().SetName(user.Name).SetEmail(user.Email).Set("ip", 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 { func (a *Telemetry) checkEvents(event string) bool {
sendEvent := true sendEvent := true
if event == TELEMETRY_EVENT_USER && a.isTelemetryAnonymous() { 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 := analytics.NewProperties()
properties.Set("version", version.GetVersion()) properties.Set("version", version.GetVersion())
properties.Set("deploymentType", getDeploymentType()) properties.Set("deploymentType", getDeploymentType())
properties.Set("companyDomain", a.getCompanyDomain())
for k, v := range data { for k, v := range data {
properties.Set(k, v) properties.Set(k, v)
@ -151,6 +203,18 @@ func (a *Telemetry) SendEvent(event string, data map[string]interface{}) {
UserId: userId, UserId: userId,
Properties: properties, 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 { func (a *Telemetry) GetDistinctId() string {

View File

@ -61,7 +61,7 @@ services:
condition: service_healthy condition: service_healthy
otel-collector: otel-collector:
image: signoz-otel-collector:0.55.0 image: signoz-otel-collector:0.55.1
command: ["--config=/etc/otel-collector-config.yaml"] command: ["--config=/etc/otel-collector-config.yaml"]
user: root # required for reading docker container logs user: root # required for reading docker container logs
volumes: volumes:
@ -77,7 +77,7 @@ services:
condition: service_healthy condition: service_healthy
otel-collector-metrics: 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"] command: ["--config=/etc/otel-collector-metrics-config.yaml"]
volumes: volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml - ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml

View File

@ -19,8 +19,7 @@ rule_files:
# A scrape configuration containing exactly one endpoint to scrape: # A scrape configuration containing exactly one endpoint to scrape:
# Here it's Prometheus itself. # Here it's Prometheus itself.
scrape_configs: scrape_configs: []
remote_read: remote_read:
- url: tcp://clickhouse:9000/?database=signoz_metrics - url: tcp://clickhouse:9000/?database=signoz_metrics