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

View File

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

View File

@ -11,6 +11,10 @@ server {
gzip_buffers 16 8k;
gzip_http_version 1.1;
# to handle uri issue 414 from nginx
client_max_body_size 24M;
large_client_header_buffers 8 16k;
location / {
if ( $uri = '/index.html' ) {
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`
otel-collector:
container_name: otel-collector
image: signoz/signoz-otel-collector:0.55.0
image: signoz/signoz-otel-collector:0.55.1
command: ["--config=/etc/otel-collector-config.yaml"]
# user: root # required for reading docker container logs
volumes:
@ -67,7 +67,7 @@ services:
otel-collector-metrics:
container_name: otel-collector-metrics
image: signoz/signoz-otel-collector:0.55.0
image: signoz/signoz-otel-collector:0.55.1
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml

View File

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

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

View File

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

View File

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

View File

@ -11,6 +11,10 @@ server {
gzip_buffers 16 8k;
gzip_http_version 1.1;
# to handle uri issue 414 from nginx
client_max_body_size 24M;
large_client_header_buffers 8 16k;
location / {
root /usr/share/nginx/html;
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'),
);
export const InstrumentationPage = Loadable(
() =>
import(
/* webpackChunkName: "InstrumentationPage" */ 'pages/AddInstrumentation'
),
export const GettingStarted = Loadable(
() => import(/* webpackChunkName: "GettingStarted" */ 'pages/GettingStarted'),
);
export const DashboardPage = Loadable(

View File

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

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_DETAIL: '/trace/:id',
SETTINGS: '/settings',
INSTRUMENTATION: '/add-instrumentation',
INSTRUMENTATION: '/get-started',
USAGE_EXPLORER: '/usage-explorer',
APPLICATION: '/application',
ALL_DASHBOARD: '/dashboard',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

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

View File

@ -16,6 +16,7 @@ export const AllTraceFilterEnum: TraceFilterEnum[] = [
'httpMethod',
'httpRoute',
'httpUrl',
'traceID',
];
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';
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(
({ queryData, query: queryG, legend: legendG }) => {
return queryData.map((e) => {
@ -22,11 +32,24 @@ const getChartData = ({ queryData }: GetChartDataProps): ChartData => {
second: Number(parseFloat(second)),
};
});
// Fill the missing data with null
const filledDataValues = Array.from(labels).map((e) => {
const td1 = new Date(parseInt(convertIntoEpoc(e * 1000), 10));
const data = dataValue.find((e1) => {
return e1.first.getTime() === td1.getTime();
});
return (
data || {
first: new Date(parseInt(convertIntoEpoc(e * 1000), 10)),
second: null,
}
);
});
return {
label: labelNames !== 'undefined' ? labelNames : '',
first: dataValue.map((e) => e.first),
second: dataValue.map((e) => e.second),
first: filledDataValues.map((e) => e.first),
second: filledDataValues.map((e) => e.second),
};
});
},

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';
interface Props {
@ -18,3 +18,13 @@ export const Heading = styled(Typography)`
margin-bottom: 1rem;
}
`;
export const DocCardContainer = styled(Row)<{
isDarkMode: boolean;
}>`
display: flex;
border: 1px solid ${({ isDarkMode }): string => (isDarkMode ? '#444' : '#ccc')};
border-radius: 0.2rem;
align-items: center;
padding: 0.5rem 0.25rem;
`;

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,
widgets: updatedWidgets,
layout: updatedLayout,
variables: selectedDashboard.data.variables,
},
uuid: selectedDashboard.uuid,
};

View File

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

View File

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

View File

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

View File

@ -11,6 +11,31 @@ import { QueryData } from '../widgets/getQuery';
export type PayloadProps = Dashboard[];
export const VariableQueryTypeArr = ['QUERY', 'TEXTBOX', 'CUSTOM'] as const;
export type TVariableQueryType = typeof VariableQueryTypeArr[number];
export const VariableSortTypeArr = ['DISABLED', 'ASC', 'DESC'] as const;
export type TSortVariableValuesType = typeof VariableSortTypeArr[number];
export interface IDashboardVariable {
name?: string; // key will be the source of truth
description: string;
type: TVariableQueryType;
// Query
queryValue?: string;
// Custom
customValue?: string;
// Textbox
textboxValue?: string;
sort: TSortVariableValuesType;
multiSelect: boolean;
showALLOption: boolean;
selectedValue?: null | string | string[];
// Internal use
modificationUUID?: string;
allSelected?: boolean;
}
export interface Dashboard {
id: number;
uuid: string;
@ -26,6 +51,7 @@ export interface DashboardData {
widgets?: Widgets[];
title: string;
layout?: Layout[];
variables: Record<string, IDashboardVariable>;
}
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: {
result: QueryData[];
resultType: string;
variables: Record<string, unknown>;
};
}

View File

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

View File

@ -94,6 +94,7 @@ type ClickHouseReader struct {
logsResourceKeys string
queryEngine *promql.Engine
remoteStorage *remote.Storage
fanoutStorage *storage.Storage
promConfigFile string
promConfig *config.Config
@ -143,7 +144,7 @@ func NewReader(localDB *sqlx.DB, configFile string) *ClickHouseReader {
}
}
func (r *ClickHouseReader) Start() {
func (r *ClickHouseReader) Start(readerReady chan bool) {
logLevel := promlog.AllowedLevel{}
logLevel.Set("debug")
// allowedFormat := promlog.AllowedFormat{}
@ -311,6 +312,8 @@ func (r *ClickHouseReader) Start() {
}
r.queryEngine = queryEngine
r.remoteStorage = remoteStorage
r.fanoutStorage = &fanoutStorage
readerReady <- true
if err := g.Run(); err != nil {
level.Error(logger).Log("err", err)
@ -319,6 +322,14 @@ func (r *ClickHouseReader) Start() {
}
func (r *ClickHouseReader) GetQueryEngine() *promql.Engine {
return r.queryEngine
}
func (r *ClickHouseReader) GetFanoutStorage() *storage.Storage {
return r.fanoutStorage
}
func reloadConfig(filename string, logger log.Logger, rls ...func(*config.Config) error) (promConfig *config.Config, err error) {
level.Info(logger).Log("msg", "Loading configuration file", "filename", filename)
@ -925,6 +936,9 @@ func (r *ClickHouseReader) GetSpanFilters(ctx context.Context, queryParams *mode
}
args := []interface{}{clickhouse.Named("timestampL", strconv.FormatInt(queryParams.Start.UnixNano(), 10)), clickhouse.Named("timestampU", strconv.FormatInt(queryParams.End.UnixNano(), 10))}
if len(queryParams.TraceID) > 0 {
args = buildFilterArrayQuery(ctx, excludeMap, queryParams.TraceID, constants.TraceID, &query, args)
}
if len(queryParams.ServiceName) > 0 {
args = buildFilterArrayQuery(ctx, excludeMap, queryParams.ServiceName, constants.ServiceName, &query, args)
}
@ -984,6 +998,8 @@ func (r *ClickHouseReader) GetSpanFilters(ctx context.Context, queryParams *mode
for _, e := range queryParams.GetFilters {
switch e {
case constants.TraceID:
continue
case constants.ServiceName:
finalQuery := fmt.Sprintf("SELECT serviceName, count() as count FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", r.traceDB, r.indexTable)
finalQuery += query
@ -1260,6 +1276,9 @@ func (r *ClickHouseReader) GetFilteredSpans(ctx context.Context, queryParams *mo
var query string
args := []interface{}{clickhouse.Named("timestampL", strconv.FormatInt(queryParams.Start.UnixNano(), 10)), clickhouse.Named("timestampU", strconv.FormatInt(queryParams.End.UnixNano(), 10))}
if len(queryParams.TraceID) > 0 {
args = buildFilterArrayQuery(ctx, excludeMap, queryParams.TraceID, constants.TraceID, &query, args)
}
if len(queryParams.ServiceName) > 0 {
args = buildFilterArrayQuery(ctx, excludeMap, queryParams.ServiceName, constants.ServiceName, &query, args)
}
@ -1450,6 +1469,9 @@ func (r *ClickHouseReader) GetTagFilters(ctx context.Context, queryParams *model
var query string
args := []interface{}{clickhouse.Named("timestampL", strconv.FormatInt(queryParams.Start.UnixNano(), 10)), clickhouse.Named("timestampU", strconv.FormatInt(queryParams.End.UnixNano(), 10))}
if len(queryParams.TraceID) > 0 {
args = buildFilterArrayQuery(ctx, excludeMap, queryParams.TraceID, constants.TraceID, &query, args)
}
if len(queryParams.ServiceName) > 0 {
args = buildFilterArrayQuery(ctx, excludeMap, queryParams.ServiceName, constants.ServiceName, &query, args)
}
@ -1546,6 +1568,9 @@ func (r *ClickHouseReader) GetTagValues(ctx context.Context, queryParams *model.
var query string
args := []interface{}{clickhouse.Named("timestampL", strconv.FormatInt(queryParams.Start.UnixNano(), 10)), clickhouse.Named("timestampU", strconv.FormatInt(queryParams.End.UnixNano(), 10))}
if len(queryParams.TraceID) > 0 {
args = buildFilterArrayQuery(ctx, excludeMap, queryParams.TraceID, constants.TraceID, &query, args)
}
if len(queryParams.ServiceName) > 0 {
args = buildFilterArrayQuery(ctx, excludeMap, queryParams.ServiceName, constants.ServiceName, &query, args)
}
@ -1853,6 +1878,9 @@ func (r *ClickHouseReader) GetFilteredSpansAggregates(ctx context.Context, query
query = fmt.Sprintf("SELECT toStartOfInterval(timestamp, INTERVAL %d minute) as time, %s FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", queryParams.StepSeconds/60, aggregation_query, r.traceDB, r.indexTable)
}
if len(queryParams.TraceID) > 0 {
args = buildFilterArrayQuery(ctx, excludeMap, queryParams.TraceID, constants.TraceID, &query, args)
}
if len(queryParams.ServiceName) > 0 {
args = buildFilterArrayQuery(ctx, excludeMap, queryParams.ServiceName, constants.ServiceName, &query, args)
}
@ -2813,7 +2841,7 @@ func (r *ClickHouseReader) GetMetricResult(ctx context.Context, query string) ([
if err != nil {
zap.S().Debug("Error in processing query: ", err)
return nil, fmt.Errorf("error in processing query")
return nil, err
}
var (
@ -3239,3 +3267,39 @@ func (r *ClickHouseReader) AggregateLogs(ctx context.Context, params *model.Logs
return &aggregateResponse, nil
}
func (r *ClickHouseReader) QueryDashboardVars(ctx context.Context, query string) (*model.DashboardVar, error) {
var result model.DashboardVar
rows, err := r.db.Query(ctx, query)
zap.S().Info(query)
if err != nil {
zap.S().Debug("Error in processing sql query: ", err)
return nil, err
}
var (
columnTypes = rows.ColumnTypes()
vars = make([]interface{}, len(columnTypes))
)
for i := range columnTypes {
vars[i] = reflect.New(columnTypes[i].ScanType()).Interface()
}
defer rows.Close()
for rows.Next() {
if err := rows.Scan(vars...); err != nil {
return nil, err
}
for _, v := range vars {
switch v := v.(type) {
case *string, *int8, *int16, *int32, *int64, *uint8, *uint16, *uint32, *uint64, *float32, *float64, *time.Time, *bool:
result.VariableValues = append(result.VariableValues, reflect.ValueOf(v).Elem().Interface())
default:
return nil, fmt.Errorf("unsupported value type encountered")
}
}
}
return &result, nil
}

View File

@ -9,7 +9,9 @@ import (
"io/ioutil"
"net/http"
"strconv"
"strings"
"sync"
"text/template"
"time"
"github.com/gorilla/mux"
@ -320,6 +322,7 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router) {
router.HandleFunc("/api/v1/dashboards/{uuid}", ViewAccess(aH.getDashboard)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/dashboards/{uuid}", EditAccess(aH.updateDashboard)).Methods(http.MethodPut)
router.HandleFunc("/api/v1/dashboards/{uuid}", EditAccess(aH.deleteDashboard)).Methods(http.MethodDelete)
router.HandleFunc("/api/v1/variables/query", ViewAccess(aH.queryDashboardVars)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/feedback", OpenAccess(aH.submitFeedback)).Methods(http.MethodPost)
// router.HandleFunc("/api/v1/get_percentiles", aH.getApplicationPercentiles).Methods(http.MethodGet)
@ -483,9 +486,11 @@ func (aH *APIHandler) queryRangeMetricsV2(w http.ResponseWriter, r *http.Request
type channelResult struct {
Series []*model.Series
Err error
Name string
Query string
}
execClickHouseQueries := func(queries map[string]string) ([]*model.Series, error) {
execClickHouseQueries := func(queries map[string]string) ([]*model.Series, error, map[string]string) {
var seriesList []*model.Series
ch := make(chan channelResult, len(queries))
var wg sync.WaitGroup
@ -500,7 +505,7 @@ func (aH *APIHandler) queryRangeMetricsV2(w http.ResponseWriter, r *http.Request
}
if err != nil {
ch <- channelResult{Err: fmt.Errorf("error in query-%s: %v", name, err)}
ch <- channelResult{Err: fmt.Errorf("error in query-%s: %v", name, err), Name: name, Query: query}
return
}
ch <- channelResult{Series: seriesList}
@ -511,21 +516,23 @@ func (aH *APIHandler) queryRangeMetricsV2(w http.ResponseWriter, r *http.Request
close(ch)
var errs []error
errQuriesByName := make(map[string]string)
// read values from the channel
for r := range ch {
if r.Err != nil {
errs = append(errs, r.Err)
errQuriesByName[r.Name] = r.Query
continue
}
seriesList = append(seriesList, r.Series...)
}
if len(errs) != 0 {
return nil, fmt.Errorf("encountered multiple errors: %s", metrics.FormatErrs(errs, "\n"))
return nil, fmt.Errorf("encountered multiple errors: %s", metrics.FormatErrs(errs, "\n")), errQuriesByName
}
return seriesList, nil
return seriesList, nil, nil
}
execPromQueries := func(metricsQueryRangeParams *model.QueryRangeParamsV2) ([]*model.Series, error) {
execPromQueries := func(metricsQueryRangeParams *model.QueryRangeParamsV2) ([]*model.Series, error, map[string]string) {
var seriesList []*model.Series
ch := make(chan channelResult, len(metricsQueryRangeParams.CompositeMetricQuery.PromQueries))
var wg sync.WaitGroup
@ -538,6 +545,19 @@ func (aH *APIHandler) queryRangeMetricsV2(w http.ResponseWriter, r *http.Request
go func(name string, query *model.PromQuery) {
var seriesList []*model.Series
defer wg.Done()
tmpl := template.New("promql-query")
tmpl, tmplErr := tmpl.Parse(query.Query)
if tmplErr != nil {
ch <- channelResult{Err: fmt.Errorf("error in parsing query-%s: %v", name, tmplErr), Name: name, Query: query.Query}
return
}
var queryBuf bytes.Buffer
tmplErr = tmpl.Execute(&queryBuf, metricsQueryRangeParams.Variables)
if tmplErr != nil {
ch <- channelResult{Err: fmt.Errorf("error in parsing query-%s: %v", name, tmplErr), Name: name, Query: query.Query}
return
}
query.Query = queryBuf.String()
queryModel := model.QueryRangeParams{
Start: time.UnixMilli(metricsQueryRangeParams.Start),
End: time.UnixMilli(metricsQueryRangeParams.End),
@ -546,7 +566,7 @@ func (aH *APIHandler) queryRangeMetricsV2(w http.ResponseWriter, r *http.Request
}
promResult, _, err := (*aH.reader).GetQueryRangeResult(r.Context(), &queryModel)
if err != nil {
ch <- channelResult{Err: fmt.Errorf("error in query-%s: %v", name, err)}
ch <- channelResult{Err: fmt.Errorf("error in query-%s: %v", name, err), Name: name, Query: query.Query}
return
}
matrix, _ := promResult.Matrix()
@ -567,22 +587,25 @@ func (aH *APIHandler) queryRangeMetricsV2(w http.ResponseWriter, r *http.Request
close(ch)
var errs []error
errQuriesByName := make(map[string]string)
// read values from the channel
for r := range ch {
if r.Err != nil {
errs = append(errs, r.Err)
errQuriesByName[r.Name] = r.Query
continue
}
seriesList = append(seriesList, r.Series...)
}
if len(errs) != 0 {
return nil, fmt.Errorf("encountered multiple errors: %s", metrics.FormatErrs(errs, "\n"))
return nil, fmt.Errorf("encountered multiple errors: %s", metrics.FormatErrs(errs, "\n")), errQuriesByName
}
return seriesList, nil
return seriesList, nil, nil
}
var seriesList []*model.Series
var err error
var errQuriesByName map[string]string
switch metricsQueryRangeParams.CompositeMetricQuery.QueryType {
case model.QUERY_BUILDER:
runQueries := metrics.PrepareBuilderMetricQueries(metricsQueryRangeParams, constants.SIGNOZ_TIMESERIES_TABLENAME)
@ -590,7 +613,7 @@ func (aH *APIHandler) queryRangeMetricsV2(w http.ResponseWriter, r *http.Request
respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: runQueries.Err}, nil)
return
}
seriesList, err = execClickHouseQueries(runQueries.Queries)
seriesList, err, errQuriesByName = execClickHouseQueries(runQueries.Queries)
case model.CLICKHOUSE:
queries := make(map[string]string)
@ -598,20 +621,32 @@ func (aH *APIHandler) queryRangeMetricsV2(w http.ResponseWriter, r *http.Request
if chQuery.Disabled {
continue
}
queries[name] = chQuery.Query
tmpl := template.New("clickhouse-query")
tmpl, err := tmpl.Parse(chQuery.Query)
if err != nil {
respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
return
}
var query bytes.Buffer
err = tmpl.Execute(&query, metricsQueryRangeParams.Variables)
if err != nil {
respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
return
}
queries[name] = query.String()
}
seriesList, err = execClickHouseQueries(queries)
seriesList, err, errQuriesByName = execClickHouseQueries(queries)
case model.PROM:
seriesList, err = execPromQueries(metricsQueryRangeParams)
seriesList, err, errQuriesByName = execPromQueries(metricsQueryRangeParams)
default:
err = fmt.Errorf("invalid query type")
respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, errQuriesByName)
return
}
if err != nil {
apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err}
respondError(w, apiErrObj, nil)
respondError(w, apiErrObj, errQuriesByName)
return
}
if metricsQueryRangeParams.CompositeMetricQuery.PanelType == model.QUERY_VALUE &&
@ -707,6 +742,25 @@ func (aH *APIHandler) deleteDashboard(w http.ResponseWriter, r *http.Request) {
}
func (aH *APIHandler) queryDashboardVars(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query().Get("query")
if query == "" {
respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("query is required")}, nil)
return
}
if strings.Contains(strings.ToLower(query), "alter table") {
respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("query shouldn't alter data")}, nil)
return
}
dashboardVars, err := (*aH.reader).QueryDashboardVars(r.Context(), query)
if err != nil {
respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
return
}
aH.respond(w, dashboardVars)
}
func (aH *APIHandler) updateDashboard(w http.ResponseWriter, r *http.Request) {
uuid := mux.Vars(r)["uuid"]
@ -1034,11 +1088,11 @@ func (aH *APIHandler) queryRangeMetrics(w http.ResponseWriter, r *http.Request)
if res.Err != nil {
switch res.Err.(type) {
case promql.ErrQueryCanceled:
respondError(w, &model.ApiError{model.ErrorCanceled, res.Err}, nil)
respondError(w, &model.ApiError{Typ: model.ErrorCanceled, Err: res.Err}, nil)
case promql.ErrQueryTimeout:
respondError(w, &model.ApiError{model.ErrorTimeout, res.Err}, nil)
respondError(w, &model.ApiError{Typ: model.ErrorTimeout, Err: res.Err}, nil)
}
respondError(w, &model.ApiError{model.ErrorExec, res.Err}, nil)
respondError(w, &model.ApiError{Typ: model.ErrorExec, Err: res.Err}, nil)
}
response_data := &model.QueryData{
@ -1088,11 +1142,11 @@ func (aH *APIHandler) queryMetrics(w http.ResponseWriter, r *http.Request) {
if res.Err != nil {
switch res.Err.(type) {
case promql.ErrQueryCanceled:
respondError(w, &model.ApiError{model.ErrorCanceled, res.Err}, nil)
respondError(w, &model.ApiError{Typ: model.ErrorCanceled, Err: res.Err}, nil)
case promql.ErrQueryTimeout:
respondError(w, &model.ApiError{model.ErrorTimeout, res.Err}, nil)
respondError(w, &model.ApiError{Typ: model.ErrorTimeout, Err: res.Err}, nil)
}
respondError(w, &model.ApiError{model.ErrorExec, res.Err}, nil)
respondError(w, &model.ApiError{Typ: model.ErrorExec, Err: res.Err}, nil)
}
response_data := &model.QueryData{

View File

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

View File

@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"net/http"
"strings"
"go.signoz.io/query-service/app/metrics"
"go.signoz.io/query-service/model"
@ -36,6 +37,44 @@ func ParseMetricQueryRangeParams(r *http.Request) (*model.QueryRangeParamsV2, *m
if err := validateQueryRangeParamsV2(postData); err != nil {
return nil, &model.ApiError{Typ: model.ErrorBadData, Err: err}
}
// prepare the variables for the corrspnding query type
formattedVars := make(map[string]interface{})
for name, value := range postData.Variables {
if postData.CompositeMetricQuery.QueryType == model.PROM {
formattedVars[name] = metrics.PromFormattedValue(value)
} else if postData.CompositeMetricQuery.QueryType == model.CLICKHOUSE {
formattedVars[name] = metrics.FormattedValue(value)
}
}
// replace the variables in metrics builder filter item with actual value
if postData.CompositeMetricQuery.QueryType == model.QUERY_BUILDER {
for _, query := range postData.CompositeMetricQuery.BuilderQueries {
for idx := range query.TagFilters.Items {
item := &query.TagFilters.Items[idx]
value := item.Value
if value != nil {
switch x := value.(type) {
case string:
variableName := strings.Trim(x, "{{ . }}")
if _, ok := postData.Variables[variableName]; ok {
item.Value = postData.Variables[variableName]
}
case []interface{}:
if len(x) > 0 {
switch x[0].(type) {
case string:
variableName := strings.Trim(x[0].(string), "{{ . }}")
if _, ok := postData.Variables[variableName]; ok {
item.Value = postData.Variables[variableName]
}
}
}
}
}
}
}
}
postData.Variables = formattedVars
return postData, nil
}

View File

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

View File

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

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")
const (
TraceID = "traceID"
ServiceName = "serviceName"
HttpRoute = "httpRoute"
HttpCode = "httpCode"

View File

@ -34,6 +34,7 @@ require (
github.com/minio/md5-simd v1.1.0 // indirect
github.com/minio/sha256-simd v0.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/posthog/posthog-go v0.0.0-20220817142604-0b0bbf0f9c0f // indirect
gopkg.in/ini.v1 v1.42.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
)

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/cockroachdb/cmux v0.0.0-20170110192607-30d10be49292/go.mod h1:qRiX68mZX1lGBkTWyp3CLcenw9I94W2dLeRvMzcn9N4=
github.com/cockroachdb/cockroach v0.0.0-20170608034007-84bc9597164f/go.mod h1:xeT/CQ0qZHangbYbWShlCGAx31aV4AjGswDUjhKS6HQ=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@ -378,6 +379,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posthog/posthog-go v0.0.0-20220817142604-0b0bbf0f9c0f h1:h0p1aZ9F5d6IXOygysob3g4B07b+HuVUQC0VJKD8wA4=
github.com/posthog/posthog-go v0.0.0-20220817142604-0b0bbf0f9c0f/go.mod h1:oa2sAs9tGai3VldabTV0eWejt/O4/OOD7azP8GaikqU=
github.com/prometheus/client_golang v0.9.0-pre1.0.20181001174001-0a8115f42e03 h1:hqNopISksxji/N5zEy1xMN7TrnSyVG/LymiwnkXi6/Q=
github.com/prometheus/client_golang v0.9.0-pre1.0.20181001174001-0a8115f42e03/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM=
@ -393,6 +396,7 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/samuel/go-zookeeper v0.0.0-20161028232340-1d7be4effb13/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da h1:p3Vo3i64TCLY7gIfzeQaUJ+kppEO5WQG3cL8iE8tGHU=
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
@ -406,6 +410,7 @@ github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/vfsgen v0.0.0-20180711163814-62bca832be04/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
@ -433,6 +438,7 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc=
github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g=

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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