Merge pull request #4278 from SigNoz/release/v0.36.0

Release/v0.36.0
This commit is contained in:
Ankit Nayan 2023-12-22 15:18:38 +05:30 committed by GitHub
commit 16b846006a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
190 changed files with 5188 additions and 832 deletions

View File

@ -199,10 +199,13 @@ Not sure how to get started? Just ping us on `#contributing` in our [slack commu
#### Frontend #### Frontend
- [Palash Gupta](https://github.com/palashgdev) - [Palash Gupta](https://github.com/palashgdev)
- [Yunus M](https://github.com/YounixM)
- [Rajat Dabade](https://github.com/Rajat-Dabade)
#### DevOps #### DevOps
- [Prashant Shahi](https://github.com/prashant-shahi) - [Prashant Shahi](https://github.com/prashant-shahi)
- [Dhawal Sanghvi](https://github.com/dhawal1248)
<br /><br /> <br /><br />

View File

@ -1,7 +1,7 @@
version: "3.9" version: "3.9"
x-clickhouse-defaults: &clickhouse-defaults x-clickhouse-defaults: &clickhouse-defaults
image: clickhouse/clickhouse-server:23.7.3-alpine image: clickhouse/clickhouse-server:23.11.1-alpine
tty: true tty: true
deploy: deploy:
restart_policy: restart_policy:
@ -146,7 +146,7 @@ services:
condition: on-failure condition: on-failure
query-service: query-service:
image: signoz/query-service:0.35.1 image: signoz/query-service:0.36.0
command: command:
[ [
"-config=/root/config/prometheus.yml", "-config=/root/config/prometheus.yml",
@ -186,7 +186,7 @@ services:
<<: *db-depend <<: *db-depend
frontend: frontend:
image: signoz/frontend:0.35.1 image: signoz/frontend:0.36.0
deploy: deploy:
restart_policy: restart_policy:
condition: on-failure condition: on-failure
@ -199,7 +199,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector: otel-collector:
image: signoz/signoz-otel-collector:0.88.3 image: signoz/signoz-otel-collector:0.88.4
command: command:
[ [
"--config=/etc/otel-collector-config.yaml", "--config=/etc/otel-collector-config.yaml",
@ -237,7 +237,7 @@ services:
- query-service - query-service
otel-collector-migrator: otel-collector-migrator:
image: signoz/signoz-schema-migrator:0.88.3 image: signoz/signoz-schema-migrator:0.88.4
deploy: deploy:
restart_policy: restart_policy:
condition: on-failure condition: on-failure
@ -250,7 +250,7 @@ services:
# - clickhouse-3 # - clickhouse-3
otel-collector-metrics: otel-collector-metrics:
image: signoz/signoz-otel-collector:0.88.3 image: signoz/signoz-otel-collector:0.88.4
command: command:
[ [
"--config=/etc/otel-collector-metrics-config.yaml", "--config=/etc/otel-collector-metrics-config.yaml",

View File

@ -66,7 +66,7 @@ services:
- --storage.path=/data - --storage.path=/data
otel-collector-migrator: otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.3} image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.4}
container_name: otel-migrator container_name: otel-migrator
command: command:
- "--dsn=tcp://clickhouse:9000" - "--dsn=tcp://clickhouse:9000"
@ -81,7 +81,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md` # Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
otel-collector: otel-collector:
container_name: signoz-otel-collector container_name: signoz-otel-collector
image: signoz/signoz-otel-collector:0.88.3 image: signoz/signoz-otel-collector:0.88.4
command: command:
[ [
"--config=/etc/otel-collector-config.yaml", "--config=/etc/otel-collector-config.yaml",
@ -118,7 +118,7 @@ services:
otel-collector-metrics: otel-collector-metrics:
container_name: signoz-otel-collector-metrics container_name: signoz-otel-collector-metrics
image: signoz/signoz-otel-collector:0.88.3 image: signoz/signoz-otel-collector:0.88.4
command: command:
[ [
"--config=/etc/otel-collector-metrics-config.yaml", "--config=/etc/otel-collector-metrics-config.yaml",

View File

@ -3,7 +3,7 @@ version: "2.4"
x-clickhouse-defaults: &clickhouse-defaults x-clickhouse-defaults: &clickhouse-defaults
restart: on-failure restart: on-failure
# addding non LTS version due to this fix https://github.com/ClickHouse/ClickHouse/commit/32caf8716352f45c1b617274c7508c86b7d1afab # addding non LTS version due to this fix https://github.com/ClickHouse/ClickHouse/commit/32caf8716352f45c1b617274c7508c86b7d1afab
image: clickhouse/clickhouse-server:23.7.3-alpine image: clickhouse/clickhouse-server:23.11.1-alpine
tty: true tty: true
depends_on: depends_on:
- zookeeper-1 - zookeeper-1
@ -164,7 +164,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md` # Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
query-service: query-service:
image: signoz/query-service:${DOCKER_TAG:-0.35.1} image: signoz/query-service:${DOCKER_TAG:-0.36.0}
container_name: signoz-query-service container_name: signoz-query-service
command: command:
[ [
@ -203,7 +203,7 @@ services:
<<: *db-depend <<: *db-depend
frontend: frontend:
image: signoz/frontend:${DOCKER_TAG:-0.35.1} image: signoz/frontend:${DOCKER_TAG:-0.36.0}
container_name: signoz-frontend container_name: signoz-frontend
restart: on-failure restart: on-failure
depends_on: depends_on:
@ -215,7 +215,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector-migrator: otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.3} image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.4}
container_name: otel-migrator container_name: otel-migrator
command: command:
- "--dsn=tcp://clickhouse:9000" - "--dsn=tcp://clickhouse:9000"
@ -229,7 +229,7 @@ services:
otel-collector: otel-collector:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.3} image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.4}
container_name: signoz-otel-collector container_name: signoz-otel-collector
command: command:
[ [
@ -269,7 +269,7 @@ services:
condition: service_healthy condition: service_healthy
otel-collector-metrics: otel-collector-metrics:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.3} image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.4}
container_name: signoz-otel-collector-metrics container_name: signoz-otel-collector-metrics
command: command:
[ [

View File

@ -18,6 +18,7 @@ COPY ee/query-service/bin/query-service-${TARGETOS}-${TARGETARCH} /root/query-se
# copy prometheus YAML config # copy prometheus YAML config
COPY pkg/query-service/config/prometheus.yml /root/config/prometheus.yml COPY pkg/query-service/config/prometheus.yml /root/config/prometheus.yml
COPY pkg/query-service/templates /root/templates
# Make query-service executable for non-root users # Make query-service executable for non-root users
RUN chmod 755 /root /root/query-service RUN chmod 755 /root /root/query-service

View File

@ -86,6 +86,7 @@ module.exports = {
}, },
], ],
'import/no-extraneous-dependencies': ['error', { devDependencies: true }], 'import/no-extraneous-dependencies': ['error', { devDependencies: true }],
'no-plusplus': 'off',
'jsx-a11y/label-has-associated-control': [ 'jsx-a11y/label-has-associated-control': [
'error', 'error',
{ {
@ -109,7 +110,6 @@ module.exports = {
// eslint rules need to remove // eslint rules need to remove
'@typescript-eslint/no-shadow': 'off', '@typescript-eslint/no-shadow': 'off',
'import/no-cycle': 'off', 'import/no-cycle': 'off',
'prettier/prettier': [ 'prettier/prettier': [
'error', 'error',
{}, {},

View File

@ -29,6 +29,9 @@
"dependencies": { "dependencies": {
"@ant-design/colors": "6.0.0", "@ant-design/colors": "6.0.0",
"@ant-design/icons": "4.8.0", "@ant-design/icons": "4.8.0",
"@dnd-kit/core": "6.1.0",
"@dnd-kit/modifiers": "7.0.0",
"@dnd-kit/sortable": "8.0.0",
"@grafana/data": "^9.5.2", "@grafana/data": "^9.5.2",
"@mdx-js/loader": "2.3.0", "@mdx-js/loader": "2.3.0",
"@mdx-js/react": "2.3.0", "@mdx-js/react": "2.3.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 957 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View File

@ -21,5 +21,9 @@
"error_while_updating_variable": "Error while updating variable", "error_while_updating_variable": "Error while updating variable",
"dashboard_has_been_updated": "Dashboard has been updated", "dashboard_has_been_updated": "Dashboard has been updated",
"do_you_want_to_refresh_the_dashboard": "Do you want to refresh the dashboard?", "do_you_want_to_refresh_the_dashboard": "Do you want to refresh the dashboard?",
"delete_dashboard_success": "{{name}} dashboard deleted successfully" "delete_dashboard_success": "{{name}} dashboard deleted successfully",
"dashboard_unsave_changes": "There are unsaved changes in the Query builder, please stage and run the query or the changes will be lost. Press OK to discard.",
"dashboard_save_changes": "Your graph built with {{queryTag}} query will be saved. Press OK to confirm.",
"your_graph_build_with": "Your graph built with",
"dashboar_ok_confirm": "query will be saved. Press OK to confirm."
} }

View File

@ -14,5 +14,6 @@
"delete_domain_message": "Are you sure you want to delete this domain?", "delete_domain_message": "Are you sure you want to delete this domain?",
"delete_domain": "Delete Domain", "delete_domain": "Delete Domain",
"add_domain": "Add Domains", "add_domain": "Add Domains",
"saml_settings":"Your SAML settings have been saved, please login from incognito window to confirm that it has been set up correctly" "saml_settings": "Your SAML settings have been saved, please login from incognito window to confirm that it has been set up correctly",
"invite_link_share_manually": "After inviting members, please copy the invite link and send them the link manually"
} }

View File

@ -0,0 +1,3 @@
{
"rps_over_100": "You are sending data at more than 100 RPS, your ingestion may be rate limited. Please reach out to us via Intercom support."
}

View File

@ -24,5 +24,9 @@
"do_you_want_to_refresh_the_dashboard": "Do you want to refresh the dashboard?", "do_you_want_to_refresh_the_dashboard": "Do you want to refresh the dashboard?",
"locked_dashboard_delete_tooltip_admin_author": "Dashboard is locked. Please unlock the dashboard to enable delete.", "locked_dashboard_delete_tooltip_admin_author": "Dashboard is locked. Please unlock the dashboard to enable delete.",
"locked_dashboard_delete_tooltip_editor": "Dashboard is locked. Please contact admin to delete the dashboard.", "locked_dashboard_delete_tooltip_editor": "Dashboard is locked. Please contact admin to delete the dashboard.",
"delete_dashboard_success": "{{name}} dashboard deleted successfully" "delete_dashboard_success": "{{name}} dashboard deleted successfully",
"dashboard_unsave_changes": "There are unsaved changes in the Query builder, please stage and run the query or the changes will be lost. Press OK to discard.",
"dashboard_save_changes": "Your graph built with {{queryTag}} query will be saved. Press OK to confirm.",
"your_graph_build_with": "Your graph built with",
"dashboar_ok_confirm": "query will be saved. Press OK to confirm."
} }

View File

@ -14,5 +14,6 @@
"delete_domain_message": "Are you sure you want to delete this domain?", "delete_domain_message": "Are you sure you want to delete this domain?",
"delete_domain": "Delete Domain", "delete_domain": "Delete Domain",
"add_domain": "Add Domains", "add_domain": "Add Domains",
"saml_settings":"Your SAML settings have been saved, please login from incognito window to confirm that it has been set up correctly" "saml_settings": "Your SAML settings have been saved, please login from incognito window to confirm that it has been set up correctly",
"invite_link_share_manually": "After inviting members, please copy the invite link and send them the link manually"
} }

View File

@ -0,0 +1,3 @@
{
"rps_over_100": "You are sending data at more than 100 RPS, your ingestion may be rate limited. Please reach out to us via Intercom support."
}

View File

@ -0,0 +1,15 @@
import { ApiV3Instance as axios } from 'api';
import { ApiResponse } from 'types/api';
import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery';
import { QueryRangePayload } from 'types/api/metrics/getQueryRange';
interface IQueryRangeFormat {
compositeQuery: ICompositeMetricQuery;
}
export const getQueryRangeFormat = (
props?: Partial<QueryRangePayload>,
): Promise<IQueryRangeFormat> =>
axios
.post<ApiResponse<IQueryRangeFormat>>('/query_range/format', props)
.then((res) => res.data.data);

View File

@ -5,6 +5,7 @@ import { QueryParams } from 'constants/query';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi'; import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
import isEqual from 'lodash-es/isEqual'; import isEqual from 'lodash-es/isEqual';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { import {
DeleteViewHandlerProps, DeleteViewHandlerProps,
@ -35,24 +36,12 @@ export const getViewDetailsUsingViewKey: GetViewDetailsUsingViewKey = (
return undefined; return undefined;
}; };
export const isQueryUpdatedInView = ({ // eslint-disable-next-line @typescript-eslint/no-explicit-any
viewKey, export const omitIdFromQuery = (query: Query | null): any => ({
data, ...query,
stagedQuery,
currentPanelType,
}: IsQueryUpdatedInViewProps): boolean => {
const currentViewDetails = getViewDetailsUsingViewKey(viewKey, data);
if (!currentViewDetails) {
return false;
}
const { query, panelType } = currentViewDetails;
// Omitting id from aggregateAttribute and groupBy
const updatedCurrentQuery = {
...stagedQuery,
builder: { builder: {
...stagedQuery?.builder, ...query?.builder,
queryData: stagedQuery?.builder.queryData.map((queryData) => { queryData: query?.builder.queryData.map((queryData) => {
const { id, ...rest } = queryData.aggregateAttribute; const { id, ...rest } = queryData.aggregateAttribute;
const newAggregateAttribute = rest; const newAggregateAttribute = rest;
const newGroupByAttributes = queryData.groupBy.map((groupByAttribute) => { const newGroupByAttributes = queryData.groupBy.map((groupByAttribute) => {
@ -84,7 +73,22 @@ export const isQueryUpdatedInView = ({
}; };
}), }),
}, },
}; });
export const isQueryUpdatedInView = ({
viewKey,
data,
stagedQuery,
currentPanelType,
}: IsQueryUpdatedInViewProps): boolean => {
const currentViewDetails = getViewDetailsUsingViewKey(viewKey, data);
if (!currentViewDetails) {
return false;
}
const { query, panelType } = currentViewDetails;
// Omitting id from aggregateAttribute and groupBy
const updatedCurrentQuery = omitIdFromQuery(stagedQuery);
return ( return (
panelType !== currentPanelType || panelType !== currentPanelType ||

View File

@ -27,6 +27,7 @@ function DynamicColumnTable({
); );
useEffect(() => { useEffect(() => {
setColumnsData(columns);
const visibleColumns = getVisibleColumns({ const visibleColumns = getVisibleColumns({
tablesource, tablesource,
columnsData: columns, columnsData: columns,
@ -42,7 +43,7 @@ function DynamicColumnTable({
: undefined, : undefined,
); );
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, [columns]);
const onToggleHandler = (index: number) => ( const onToggleHandler = (index: number) => (
checked: boolean, checked: boolean,

View File

@ -13,3 +13,11 @@
height: 100%; height: 100%;
width: 100%; width: 100%;
} }
.uplot-no-data {
position: relative;
display: flex;
width: 100%;
flex-direction: column;
gap: 8px;
}

View File

@ -1,8 +1,9 @@
/* eslint-disable sonarjs/cognitive-complexity */ /* eslint-disable sonarjs/cognitive-complexity */
import './uplot.scss'; import './Uplot.styles.scss';
import { Typography } from 'antd'; import { Typography } from 'antd';
import { ToggleGraphProps } from 'components/Graph/types'; import { ToggleGraphProps } from 'components/Graph/types';
import { LineChart } from 'lucide-react';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback'; import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { import {
forwardRef, forwardRef,
@ -127,6 +128,16 @@ const Uplot = forwardRef<ToggleGraphProps | undefined, UplotProps>(
} }
}, [data, resetScales, create]); }, [data, resetScales, create]);
if (data && data[0] && data[0]?.length === 0) {
return (
<div className="uplot-no-data not-found">
<LineChart size={48} strokeWidth={0.5} />
<Typography>No Data</Typography>
</div>
);
}
return ( return (
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}> <ErrorBoundary FallbackComponent={ErrorBoundaryFallback}>
<div className="uplot-graph-container" ref={targetRef}> <div className="uplot-graph-container" ref={targetRef}>

View File

@ -0,0 +1,2 @@
const MAX_RPS_LIMIT = 100;
export { MAX_RPS_LIMIT };

View File

@ -10,7 +10,7 @@ import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions'; import { useResizeObserver } from 'hooks/useDimensions';
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions'; import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData'; import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { useMemo, useRef } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
@ -18,6 +18,7 @@ import { AlertDef } from 'types/api/alerts/def';
import { Query } from 'types/api/queryBuilder/queryBuilderData'; import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard'; import { EQueryType } from 'types/common/dashboard';
import { GlobalReducer } from 'types/reducer/globalTime'; import { GlobalReducer } from 'types/reducer/globalTime';
import { getTimeRange } from 'utils/getTimeRange';
import { ChartContainer, FailedMessageContainer } from './styles'; import { ChartContainer, FailedMessageContainer } from './styles';
import { getThresholdLabel } from './utils'; import { getThresholdLabel } from './utils';
@ -49,9 +50,13 @@ function ChartPreview({
}: ChartPreviewProps): JSX.Element | null { }: ChartPreviewProps): JSX.Element | null {
const { t } = useTranslation('alerts'); const { t } = useTranslation('alerts');
const threshold = alertDef?.condition.target || 0; const threshold = alertDef?.condition.target || 0;
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>( const [minTimeScale, setMinTimeScale] = useState<number>();
(state) => state.globalTime, const [maxTimeScale, setMaxTimeScale] = useState<number>();
);
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const canQuery = useMemo((): boolean => { const canQuery = useMemo((): boolean => {
if (!query || query == null) { if (!query || query == null) {
@ -101,6 +106,13 @@ function ChartPreview({
const graphRef = useRef<HTMLDivElement>(null); const graphRef = useRef<HTMLDivElement>(null);
useEffect((): void => {
const { startTime, endTime } = getTimeRange(queryResponse);
setMinTimeScale(startTime);
setMaxTimeScale(endTime);
}, [maxTime, minTime, globalSelectedInterval, queryResponse]);
const chartData = getUPlotChartData(queryResponse?.data?.payload); const chartData = getUPlotChartData(queryResponse?.data?.payload);
const containerDimensions = useResizeObserver(graphRef); const containerDimensions = useResizeObserver(graphRef);
@ -117,6 +129,8 @@ function ChartPreview({
yAxisUnit, yAxisUnit,
apiResponse: queryResponse?.data?.payload, apiResponse: queryResponse?.data?.payload,
dimensions: containerDimensions, dimensions: containerDimensions,
minTimeScale,
maxTimeScale,
isDarkMode, isDarkMode,
thresholds: [ thresholds: [
{ {
@ -141,6 +155,8 @@ function ChartPreview({
yAxisUnit, yAxisUnit,
queryResponse?.data?.payload, queryResponse?.data?.payload,
containerDimensions, containerDimensions,
minTimeScale,
maxTimeScale,
isDarkMode, isDarkMode,
threshold, threshold,
t, t,

View File

@ -0,0 +1,16 @@
.span-container {
.spanDetails {
position: absolute;
height: 50px;
padding: 8px;
min-width: 150px;
background: lightcyan;
color: black;
bottom: 24px;
left: 0;
display: flex;
justify-content: center;
align-items: center;
}
}

View File

@ -0,0 +1,96 @@
import '../GantChart.styles.scss';
import { Popover, Typography } from 'antd';
import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils';
import dayjs from 'dayjs';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useEffect } from 'react';
import { toFixed } from 'utils/toFixed';
import { SpanBorder, SpanLine, SpanText, SpanWrapper } from './styles';
interface SpanLengthProps {
globalStart: number;
startTime: number;
name: string;
width: string;
leftOffset: string;
bgColor: string;
inMsCount: number;
}
function Span(props: SpanLengthProps): JSX.Element {
const {
width,
leftOffset,
bgColor,
inMsCount,
startTime,
name,
globalStart,
} = props;
const isDarkMode = useIsDarkMode();
const { time, timeUnitName } = convertTimeToRelevantUnit(inMsCount);
useEffect(() => {
document.documentElement.scrollTop = document.documentElement.clientHeight;
document.documentElement.scrollLeft = document.documentElement.clientWidth;
}, []);
const getContent = (): JSX.Element => {
const timeStamp = dayjs(startTime).format('h:mm:ss:SSS A');
const startTimeInMs = startTime - globalStart;
return (
<div>
<Typography.Text style={{ marginBottom: '8px' }}>
{' '}
Duration : {inMsCount}
</Typography.Text>
<br />
<Typography.Text style={{ marginBottom: '8px' }}>
Start Time: {startTimeInMs}ms [{timeStamp}]{' '}
</Typography.Text>
</div>
);
};
return (
<SpanWrapper className="span-container">
<SpanLine
className="spanLine"
isDarkMode={isDarkMode}
bgColor={bgColor}
leftOffset={leftOffset}
width={width}
/>
<div>
<Popover
style={{
left: `${leftOffset}%`,
}}
title={name}
content={getContent()}
trigger="hover"
placement="left"
autoAdjustOverflow
>
<SpanBorder
className="spanTrack"
isDarkMode={isDarkMode}
bgColor={bgColor}
leftOffset={leftOffset}
width={width}
/>
</Popover>
</div>
<SpanText isDarkMode={isDarkMode} leftOffset={leftOffset}>{`${toFixed(
time,
2,
)} ${timeUnitName}`}</SpanText>
</SpanWrapper>
);
}
export default Span;

View File

@ -1,40 +0,0 @@
import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { toFixed } from 'utils/toFixed';
import { SpanBorder, SpanLine, SpanText, SpanWrapper } from './styles';
interface SpanLengthProps {
width: string;
leftOffset: string;
bgColor: string;
inMsCount: number;
}
function SpanLength(props: SpanLengthProps): JSX.Element {
const { width, leftOffset, bgColor, inMsCount } = props;
const isDarkMode = useIsDarkMode();
const { time, timeUnitName } = convertTimeToRelevantUnit(inMsCount);
return (
<SpanWrapper>
<SpanLine
isDarkMode={isDarkMode}
bgColor={bgColor}
leftOffset={leftOffset}
width={width}
/>
<SpanBorder
isDarkMode={isDarkMode}
bgColor={bgColor}
leftOffset={leftOffset}
width={width}
/>
<SpanText isDarkMode={isDarkMode} leftOffset={leftOffset}>{`${toFixed(
time,
2,
)} ${timeUnitName}`}</SpanText>
</SpanWrapper>
);
}
export default SpanLength;

View File

@ -16,7 +16,7 @@ import {
import { ITraceTree } from 'types/api/trace/getTraceItem'; import { ITraceTree } from 'types/api/trace/getTraceItem';
import { ITraceMetaData } from '..'; import { ITraceMetaData } from '..';
import SpanLength from '../SpanLength'; import Span from '../Span';
import SpanName from '../SpanName'; import SpanName from '../SpanName';
import { getMetaDataFromSpanTree, getTopLeftFromBody } from '../utils'; import { getMetaDataFromSpanTree, getTopLeftFromBody } from '../utils';
import { import {
@ -169,7 +169,10 @@ function Trace(props: TraceProps): JSX.Element {
</StyledRow> </StyledRow>
</StyledCol> </StyledCol>
<Col flex="1"> <Col flex="1">
<SpanLength <Span
globalStart={globalStart}
startTime={startTime}
name={name}
leftOffset={nodeLeftOffset.toString()} leftOffset={nodeLeftOffset.toString()}
width={width.toString()} width={width.toString()}
bgColor={serviceColour} bgColor={serviceColour}

View File

@ -41,7 +41,11 @@ function GanttChart(props: GanttChartProps): JSX.Element {
onClick={handleCollapse} onClick={handleCollapse}
title={isExpandAll ? 'Collapse All' : 'Expand All'} title={isExpandAll ? 'Collapse All' : 'Expand All'}
> >
{isExpandAll ? <MinusSquareOutlined /> : <PlusSquareOutlined />} {isExpandAll ? (
<MinusSquareOutlined style={{ fontSize: '16px', color: '#08c' }} />
) : (
<PlusSquareOutlined style={{ fontSize: '16px', color: '#08c' }} />
)}
</CollapseButton> </CollapseButton>
<CardWrapper> <CardWrapper>
<Trace <Trace

View File

@ -11,6 +11,7 @@ export const Wrapper = styled.ul`
border-left: 1px solid #434343; border-left: 1px solid #434343;
padding-left: 1rem; padding-left: 1rem;
width: 100%; width: 100%;
margin: 0px;
} }
ul li { ul li {
@ -44,6 +45,4 @@ export const CardContainer = styled.li`
export const CollapseButton = styled.div` export const CollapseButton = styled.div`
position: absolute; position: absolute;
top: 0; top: 0;
left: 0;
font-size: 1.2rem;
`; `;

View File

@ -1,3 +1,4 @@
import { Tooltip } from 'antd';
import { useIsDarkMode } from 'hooks/useDarkMode'; import { useIsDarkMode } from 'hooks/useDarkMode';
import { LabelContainer } from '../styles'; import { LabelContainer } from '../styles';
@ -23,7 +24,9 @@ function Label({
disabled={disabled} disabled={disabled}
onClick={onClickHandler} onClick={onClickHandler}
> >
<Tooltip title={label} placement="topLeft">
{getAbbreviatedLabel(label)} {getAbbreviatedLabel(label)}
</Tooltip>
</LabelContainer> </LabelContainer>
); );
} }

View File

@ -23,6 +23,7 @@ import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime'; import { GlobalReducer } from 'types/reducer/globalTime';
import uPlot from 'uplot'; import uPlot from 'uplot';
import { getTimeRange } from 'utils/getTimeRange';
import { PANEL_TYPES_VS_FULL_VIEW_TABLE } from './contants'; import { PANEL_TYPES_VS_FULL_VIEW_TABLE } from './contants';
import GraphManager from './GraphManager'; import GraphManager from './GraphManager';
@ -92,6 +93,21 @@ function FullView({
const isDarkMode = useIsDarkMode(); const isDarkMode = useIsDarkMode();
const [minTimeScale, setMinTimeScale] = useState<number>();
const [maxTimeScale, setMaxTimeScale] = useState<number>();
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
useEffect((): void => {
const { startTime, endTime } = getTimeRange(response);
setMinTimeScale(startTime);
setMaxTimeScale(endTime);
}, [maxTime, minTime, globalSelectedInterval, response]);
useEffect(() => { useEffect(() => {
if (!response.isFetching && fullViewRef.current) { if (!response.isFetching && fullViewRef.current) {
const width = fullViewRef.current?.clientWidth const width = fullViewRef.current?.clientWidth
@ -114,6 +130,8 @@ function FullView({
graphsVisibilityStates, graphsVisibilityStates,
setGraphsVisibilityStates, setGraphsVisibilityStates,
thresholds: widget.thresholds, thresholds: widget.thresholds,
minTimeScale,
maxTimeScale,
}); });
setChartOptions(newChartOptions); setChartOptions(newChartOptions);

View File

@ -1,4 +1,5 @@
import { Skeleton, Typography } from 'antd'; import { Skeleton, Typography } from 'antd';
import cx from 'classnames';
import { ToggleGraphProps } from 'components/Graph/types'; import { ToggleGraphProps } from 'components/Graph/types';
import { SOMETHING_WENT_WRONG } from 'constants/api'; import { SOMETHING_WENT_WRONG } from 'constants/api';
import { QueryParams } from 'constants/query'; import { QueryParams } from 'constants/query';
@ -298,7 +299,10 @@ function WidgetGraphComponent({
</div> </div>
{queryResponse.isLoading && <Skeleton />} {queryResponse.isLoading && <Skeleton />}
{queryResponse.isSuccess && ( {queryResponse.isSuccess && (
<div style={{ height: '90%' }} ref={graphRef}> <div
className={cx('widget-graph-container', widget.panelTypes)}
ref={graphRef}
>
<GridPanelSwitch <GridPanelSwitch
panelType={widget.panelTypes} panelType={widget.panelTypes}
data={data} data={data}

View File

@ -15,6 +15,7 @@ import { useDispatch, useSelector } from 'react-redux';
import { UpdateTimeInterval } from 'store/actions'; import { UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime'; import { GlobalReducer } from 'types/reducer/globalTime';
import { getTimeRange } from 'utils/getTimeRange';
import EmptyWidget from '../EmptyWidget'; import EmptyWidget from '../EmptyWidget';
import { MenuItemKeys } from '../WidgetHeader/contants'; import { MenuItemKeys } from '../WidgetHeader/contants';
@ -34,6 +35,8 @@ function GridCardGraph({
const dispatch = useDispatch(); const dispatch = useDispatch();
const [errorMessage, setErrorMessage] = useState<string>(); const [errorMessage, setErrorMessage] = useState<string>();
const { toScrollWidgetId, setToScrollWidgetId } = useDashboard(); const { toScrollWidgetId, setToScrollWidgetId } = useDashboard();
const [minTimeScale, setMinTimeScale] = useState<number>();
const [maxTimeScale, setMaxTimeScale] = useState<number>();
const onDragSelect = useCallback( const onDragSelect = useCallback(
(start: number, end: number): void => { (start: number, end: number): void => {
@ -62,16 +65,16 @@ function GridCardGraph({
} }
}, [toScrollWidgetId, setToScrollWidgetId, widget.id]); }, [toScrollWidgetId, setToScrollWidgetId, widget.id]);
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const updatedQuery = useStepInterval(widget?.query); const updatedQuery = useStepInterval(widget?.query);
const isEmptyWidget = const isEmptyWidget =
widget?.id === PANEL_TYPES.EMPTY_WIDGET || isEmpty(widget); widget?.id === PANEL_TYPES.EMPTY_WIDGET || isEmpty(widget);
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const queryResponse = useGetQueryRange( const queryResponse = useGetQueryRange(
{ {
selectedTime: widget?.timePreferance, selectedTime: widget?.timePreferance,
@ -103,6 +106,13 @@ function GridCardGraph({
const containerDimensions = useResizeObserver(graphRef); const containerDimensions = useResizeObserver(graphRef);
useEffect((): void => {
const { startTime, endTime } = getTimeRange(queryResponse);
setMinTimeScale(startTime);
setMaxTimeScale(endTime);
}, [maxTime, minTime, globalSelectedInterval, queryResponse]);
const chartData = getUPlotChartData(queryResponse?.data?.payload, fillSpans); const chartData = getUPlotChartData(queryResponse?.data?.payload, fillSpans);
const isDarkMode = useIsDarkMode(); const isDarkMode = useIsDarkMode();
@ -123,6 +133,8 @@ function GridCardGraph({
yAxisUnit: widget?.yAxisUnit, yAxisUnit: widget?.yAxisUnit,
onClickHandler, onClickHandler,
thresholds: widget.thresholds, thresholds: widget.thresholds,
minTimeScale,
maxTimeScale,
}), }),
[ [
widget?.id, widget?.id,
@ -133,6 +145,8 @@ function GridCardGraph({
isDarkMode, isDarkMode,
onDragSelect, onDragSelect,
onClickHandler, onClickHandler,
minTimeScale,
maxTimeScale,
], ],
); );

View File

@ -5,3 +5,11 @@
border: none !important; border: none !important;
} }
} }
.widget-graph-container {
height: 100%;
&.graph {
height: calc(100% - 30px);
}
}

View File

@ -2,9 +2,12 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
height: 30px; height: 40px;
width: 100%; width: 100%;
padding: 0.5rem; padding: 0.5rem;
box-sizing: border-box;
font-size: 14px;
font-weight: 600;
} }
.widget-header-title { .widget-header-title {
@ -19,6 +22,10 @@
visibility: hidden; visibility: hidden;
border: none; border: none;
box-shadow: none; box-shadow: none;
cursor: pointer;
font: 14px;
font-weight: 600;
padding: 8px;
} }
.widget-header-hover { .widget-header-hover {

View File

@ -10,11 +10,11 @@ import {
MoreOutlined, MoreOutlined,
WarningOutlined, WarningOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { Button, Dropdown, MenuProps, Tooltip, Typography } from 'antd'; import { Dropdown, MenuProps, Tooltip, Typography } from 'antd';
import Spinner from 'components/Spinner'; import Spinner from 'components/Spinner';
import { QueryParams } from 'constants/query'; import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder'; import { PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes'; import useCreateAlerts from 'hooks/queryBuilder/useCreateAlerts';
import useComponentPermission from 'hooks/useComponentPermission'; import useComponentPermission from 'hooks/useComponentPermission';
import history from 'lib/history'; import history from 'lib/history';
import { ReactNode, useCallback, useMemo } from 'react'; import { ReactNode, useCallback, useMemo } from 'react';
@ -71,13 +71,7 @@ function WidgetHeader({
); );
}, [widget.id, widget.panelTypes, widget.query]); }, [widget.id, widget.panelTypes, widget.query]);
const onCreateAlertsHandler = useCallback(() => { const onCreateAlertsHandler = useCreateAlerts(widget);
history.push(
`${ROUTES.ALERTS_NEW}?${QueryParams.compositeQuery}=${encodeURIComponent(
JSON.stringify(widget.query),
)}`,
);
}, [widget]);
const keyMethodMapping = useMemo( const keyMethodMapping = useMemo(
() => ({ () => ({
@ -199,9 +193,7 @@ function WidgetHeader({
</Tooltip> </Tooltip>
)} )}
<Dropdown menu={menu} trigger={['hover']} placement="bottomRight"> <Dropdown menu={menu} trigger={['hover']} placement="bottomRight">
<Button <MoreOutlined
type="default"
icon={<MoreOutlined />}
className={`widget-header-more-options ${ className={`widget-header-more-options ${
parentHover ? 'widget-header-hover' : '' parentHover ? 'widget-header-hover' : ''
}`} }`}

View File

@ -2,7 +2,7 @@ import { Button as ButtonComponent, Card as CardComponent, Space } from 'antd';
import { PANEL_TYPES } from 'constants/queryBuilder'; import { PANEL_TYPES } from 'constants/queryBuilder';
import { StyledCSS } from 'container/GantChart/Trace/styles'; import { StyledCSS } from 'container/GantChart/Trace/styles';
import RGL, { WidthProvider } from 'react-grid-layout'; import RGL, { WidthProvider } from 'react-grid-layout';
import styled, { css, FlattenSimpleInterpolation } from 'styled-components'; import styled, { css } from 'styled-components';
const ReactGridLayoutComponent = WidthProvider(RGL); const ReactGridLayoutComponent = WidthProvider(RGL);
@ -17,14 +17,8 @@ export const Card = styled(CardComponent)<CardProps>`
} }
.ant-card-body { .ant-card-body {
height: 90%; height: calc(100% - 40px);
padding: 0; padding: 0;
${({ $panelType }): FlattenSimpleInterpolation =>
$panelType === PANEL_TYPES.TABLE
? css`
padding-top: 1.8rem;
`
: css``}
} }
`; `;

View File

@ -1,7 +1,7 @@
/* eslint-disable react/display-name */ /* eslint-disable react/display-name */
import { PlusOutlined } from '@ant-design/icons'; import { PlusOutlined } from '@ant-design/icons';
import { Typography } from 'antd'; import { Input, Typography } from 'antd';
import { ColumnsType } from 'antd/lib/table'; import type { ColumnsType } from 'antd/es/table/interface';
import saveAlertApi from 'api/alerts/save'; import saveAlertApi from 'api/alerts/save';
import DropDown from 'components/DropDown/DropDown'; import DropDown from 'components/DropDown/DropDown';
import { import {
@ -14,9 +14,12 @@ import LabelColumn from 'components/TableRenderer/LabelColumn';
import TextToolTip from 'components/TextToolTip'; import TextToolTip from 'components/TextToolTip';
import { QueryParams } from 'constants/query'; import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import useSortableTable from 'hooks/ResizeTable/useSortableTable';
import useComponentPermission from 'hooks/useComponentPermission'; import useComponentPermission from 'hooks/useComponentPermission';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import useInterval from 'hooks/useInterval'; import useInterval from 'hooks/useInterval';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history'; import history from 'lib/history';
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi'; import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
@ -29,12 +32,19 @@ import { GettableAlert } from 'types/api/alerts/get';
import AppReducer from 'types/reducer/app'; import AppReducer from 'types/reducer/app';
import DeleteAlert from './DeleteAlert'; import DeleteAlert from './DeleteAlert';
import { Button, ButtonContainer, ColumnButton } from './styles'; import {
Button,
ButtonContainer,
ColumnButton,
SearchContainer,
} from './styles';
import Status from './TableComponents/Status'; import Status from './TableComponents/Status';
import ToggleAlertState from './ToggleAlertState'; import ToggleAlertState from './ToggleAlertState';
import { filterAlerts } from './utils';
const { Search } = Input;
function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element { function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
const [data, setData] = useState<GettableAlert[]>(allAlertRules || []);
const { t } = useTranslation('common'); const { t } = useTranslation('common');
const { role, featureResponse } = useSelector<AppState, AppReducer>( const { role, featureResponse } = useSelector<AppState, AppReducer>(
(state) => state.app, (state) => state.app,
@ -44,13 +54,39 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
role, role,
); );
const params = useUrlQuery();
const orderColumnParam = params.get('columnKey');
const orderQueryParam = params.get('order');
const paginationParam = params.get('page');
const searchParams = params.get('search');
const [searchString, setSearchString] = useState<string>(searchParams || '');
const [data, setData] = useState<GettableAlert[]>(() => {
const value = searchString.toLowerCase();
const filteredData = filterAlerts(allAlertRules, value);
return filteredData || [];
});
// Type asuring
const sortingOrder: 'ascend' | 'descend' | null =
orderQueryParam === 'ascend' || orderQueryParam === 'descend'
? orderQueryParam
: null;
const { sortedInfo, handleChange } = useSortableTable<GettableAlert>(
sortingOrder,
orderColumnParam || '',
searchString,
);
const { notifications: notificationsApi } = useNotifications(); const { notifications: notificationsApi } = useNotifications();
useInterval(() => { useInterval(() => {
(async (): Promise<void> => { (async (): Promise<void> => {
const { data: refetchData, status } = await refetch(); const { data: refetchData, status } = await refetch();
if (status === 'success') { if (status === 'success') {
setData(refetchData?.payload || []); const value = searchString.toLowerCase();
const filteredData = filterAlerts(refetchData.payload || [], value);
setData(filteredData || []);
} }
if (status === 'error') { if (status === 'error') {
notificationsApi.error({ notificationsApi.error({
@ -128,6 +164,13 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
} }
}; };
const handleSearch = useDebouncedFn((e: unknown) => {
const value = (e as React.BaseSyntheticEvent).target.value.toLowerCase();
setSearchString(value);
const filteredData = filterAlerts(allAlertRules, value);
setData(filteredData);
});
const dynamicColumns: ColumnsType<GettableAlert> = [ const dynamicColumns: ColumnsType<GettableAlert> = [
{ {
title: 'Created At', title: 'Created At',
@ -142,6 +185,10 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
return prev - next; return prev - next;
}, },
render: DateComponent, render: DateComponent,
sortOrder:
sortedInfo.columnKey === DynamicColumnsKey.CreatedAt
? sortedInfo.order
: null,
}, },
{ {
title: 'Created By', title: 'Created By',
@ -163,6 +210,10 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
return prev - next; return prev - next;
}, },
render: DateComponent, render: DateComponent,
sortOrder:
sortedInfo.columnKey === DynamicColumnsKey.UpdatedAt
? sortedInfo.order
: null,
}, },
{ {
title: 'Updated By', title: 'Updated By',
@ -183,6 +234,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
(b.state ? b.state.charCodeAt(0) : 1000) - (b.state ? b.state.charCodeAt(0) : 1000) -
(a.state ? a.state.charCodeAt(0) : 1000), (a.state ? a.state.charCodeAt(0) : 1000),
render: (value): JSX.Element => <Status status={value} />, render: (value): JSX.Element => <Status status={value} />,
sortOrder: sortedInfo.columnKey === 'state' ? sortedInfo.order : null,
}, },
{ {
title: 'Alert Name', title: 'Alert Name',
@ -198,6 +250,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
render: (value, record): JSX.Element => ( render: (value, record): JSX.Element => (
<Typography.Link onClick={onEditHandler(record)}>{value}</Typography.Link> <Typography.Link onClick={onEditHandler(record)}>{value}</Typography.Link>
), ),
sortOrder: sortedInfo.columnKey === 'name' ? sortedInfo.order : null,
}, },
{ {
title: 'Severity', title: 'Severity',
@ -214,6 +267,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
return <Typography>{severityValue}</Typography>; return <Typography>{severityValue}</Typography>;
}, },
sortOrder: sortedInfo.columnKey === 'severity' ? sortedInfo.order : null,
}, },
{ {
title: 'Labels', title: 'Labels',
@ -271,6 +325,12 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
return ( return (
<> <>
<SearchContainer>
<Search
placeholder="Search by Alert Name, Severity and Labels"
onChange={handleSearch}
defaultValue={searchString}
/>
<ButtonContainer> <ButtonContainer>
<TextToolTip <TextToolTip
{...{ {...{
@ -285,12 +345,17 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
</Button> </Button>
)} )}
</ButtonContainer> </ButtonContainer>
</SearchContainer>
<DynamicColumnTable <DynamicColumnTable
tablesource={TableDataSource.Alert} tablesource={TableDataSource.Alert}
columns={columns} columns={columns}
rowKey="id" rowKey="id"
dataSource={data} dataSource={data}
dynamicColumns={dynamicColumns} dynamicColumns={dynamicColumns}
onChange={handleChange}
pagination={{
defaultCurrent: Number(paginationParam) || 1,
}}
/> />
</> </>
); );

View File

@ -1,11 +1,17 @@
import { Button as ButtonComponent } from 'antd'; import { Button as ButtonComponent } from 'antd';
import styled from 'styled-components'; import styled from 'styled-components';
export const SearchContainer = styled.div`
&&& {
display: flex;
margin-bottom: 2rem;
align-items: center;
gap: 2rem;
}
`;
export const ButtonContainer = styled.div` export const ButtonContainer = styled.div`
&&& { &&& {
display: flex; display: flex;
justify-content: flex-end;
margin-bottom: 2rem;
align-items: center; align-items: center;
} }
`; `;

View File

@ -0,0 +1,25 @@
import { GettableAlert } from 'types/api/alerts/get';
export const filterAlerts = (
allAlertRules: GettableAlert[],
filter: string,
): GettableAlert[] => {
const value = filter.toLowerCase();
return allAlertRules.filter((alert) => {
const alertName = alert.alert.toLowerCase();
const severity = alert.labels?.severity.toLowerCase();
const labels = Object.keys(alert.labels || {})
.filter((e) => e !== 'severity')
.join(' ')
.toLowerCase();
const labelValue = Object.values(alert.labels || {});
return (
alertName.includes(value) ||
severity?.includes(value) ||
labels.includes(value) ||
labelValue.includes(value)
);
});
};

View File

@ -22,6 +22,7 @@ import { ILog } from 'types/api/logs/log';
import ActionItem, { ActionItemProps } from './ActionItem'; import ActionItem, { ActionItemProps } from './ActionItem';
import FieldRenderer from './FieldRenderer'; import FieldRenderer from './FieldRenderer';
import { import {
filterKeyForField,
flattenObject, flattenObject,
jsonToDataNodes, jsonToDataNodes,
recursiveParseJSON, recursiveParseJSON,
@ -98,11 +99,12 @@ function TableView({
title: 'Action', title: 'Action',
width: 11, width: 11,
render: (fieldData: Record<string, string>): JSX.Element | null => { render: (fieldData: Record<string, string>): JSX.Element | null => {
const fieldKey = fieldData.field.split('.').slice(-1); const fieldFilterKey = filterKeyForField(fieldData.field);
if (!RESTRICTED_FIELDS.includes(fieldKey[0])) {
if (!RESTRICTED_FIELDS.includes(fieldFilterKey)) {
return ( return (
<ActionItem <ActionItem
fieldKey={fieldKey[0]} fieldKey={fieldFilterKey}
fieldValue={fieldData.value} fieldValue={fieldData.value}
onClickActionItem={onClickActionItem} onClickActionItem={onClickActionItem}
/> />
@ -119,7 +121,6 @@ function TableView({
align: 'left', align: 'left',
ellipsis: true, ellipsis: true,
render: (field: string, record): JSX.Element => { render: (field: string, record): JSX.Element => {
const fieldKey = field.split('.').slice(-1);
const renderedField = <FieldRenderer field={field} />; const renderedField = <FieldRenderer field={field} />;
if (record.field === 'trace_id') { if (record.field === 'trace_id') {
@ -148,10 +149,11 @@ function TableView({
); );
} }
if (!RESTRICTED_FIELDS.includes(fieldKey[0])) { const fieldFilterKey = filterKeyForField(field);
if (!RESTRICTED_FIELDS.includes(fieldFilterKey)) {
return ( return (
<AddToQueryHOC <AddToQueryHOC
fieldKey={fieldKey[0]} fieldKey={fieldFilterKey}
fieldValue={flattenLogData[field]} fieldValue={flattenLogData[field]}
onAddToQuery={onAddToQuery} onAddToQuery={onAddToQuery}
> >

View File

@ -132,6 +132,16 @@ export const generateFieldKeyForArray = (
export const removeObjectFromString = (str: string): string => export const removeObjectFromString = (str: string): string =>
str.replace(/\[object Object\]./g, ''); str.replace(/\[object Object\]./g, '');
// Split `str` on the first occurrence of `delimiter`
// For example, will return `['a', 'b.c']` when splitting `'a.b.c'` at dots
const splitOnce = (str: string, delimiter: string): string[] => {
const parts = str.split(delimiter);
if (parts.length < 2) {
return parts;
}
return [parts[0], parts.slice(1).join(delimiter)];
};
export const getFieldAttributes = (field: string): IFieldAttributes => { export const getFieldAttributes = (field: string): IFieldAttributes => {
let dataType; let dataType;
let newField; let newField;
@ -140,18 +150,30 @@ export const getFieldAttributes = (field: string): IFieldAttributes => {
if (field.startsWith('attributes_')) { if (field.startsWith('attributes_')) {
logType = MetricsType.Tag; logType = MetricsType.Tag;
const stringWithoutPrefix = field.slice('attributes_'.length); const stringWithoutPrefix = field.slice('attributes_'.length);
const parts = stringWithoutPrefix.split('.'); const parts = splitOnce(stringWithoutPrefix, '.');
[dataType, newField] = parts; [dataType, newField] = parts;
} else if (field.startsWith('resources_')) { } else if (field.startsWith('resources_')) {
logType = MetricsType.Resource; logType = MetricsType.Resource;
const stringWithoutPrefix = field.slice('resources_'.length); const stringWithoutPrefix = field.slice('resources_'.length);
const parts = stringWithoutPrefix.split('.'); const parts = splitOnce(stringWithoutPrefix, '.');
[dataType, newField] = parts; [dataType, newField] = parts;
} }
return { dataType, newField, logType }; return { dataType, newField, logType };
}; };
// Returns key to be used when filtering for `field` via
// the query builder. This is useful for powering filtering
// by field values from log details view.
export const filterKeyForField = (field: string): string => {
// Must work for all 3 of the following types of cases
// timestamp -> timestamp
// attributes_string.log.file -> log.file
// resources_string.k8s.pod.name -> k8s.pod.name
const fieldAttribs = getFieldAttributes(field);
return fieldAttribs?.newField || field;
};
export const aggregateAttributesResourcesToString = (logData: ILog): string => { export const aggregateAttributesResourcesToString = (logData: ILog): string => {
const outputJson: ILogAggregateAttributesResources = { const outputJson: ILogAggregateAttributesResources = {
body: logData.body, body: logData.body,

View File

@ -147,13 +147,13 @@ function LogsExplorerViews(): JSX.Element {
[currentQuery, updateAllQueriesOperators], [currentQuery, updateAllQueriesOperators],
); );
const listChartData = useGetExplorerQueryRange( const {
listChartQuery, data: listChartData,
PANEL_TYPES.TIME_SERIES, isFetching: isFetchingListChartData,
{ isLoading: isLoadingListChartData,
} = useGetExplorerQueryRange(listChartQuery, PANEL_TYPES.TIME_SERIES, {
enabled: !!listChartQuery && panelType === PANEL_TYPES.LIST, enabled: !!listChartQuery && panelType === PANEL_TYPES.LIST,
}, });
);
const { data, isFetching, isError } = useGetExplorerQueryRange( const { data, isFetching, isError } = useGetExplorerQueryRange(
requestData, requestData,
@ -445,12 +445,8 @@ function LogsExplorerViews(): JSX.Element {
if (!stagedQuery) return []; if (!stagedQuery) return [];
if (panelType === PANEL_TYPES.LIST) { if (panelType === PANEL_TYPES.LIST) {
if ( if (listChartData && listChartData.payload.data.result.length > 0) {
listChartData && return listChartData.payload.data.result;
listChartData.data &&
listChartData.data.payload.data.result.length > 0
) {
return listChartData.data.payload.data.result;
} }
return []; return [];
} }
@ -472,7 +468,10 @@ function LogsExplorerViews(): JSX.Element {
return ( return (
<> <>
<LogsExplorerChart isLoading={isFetching} data={chartData} /> <LogsExplorerChart
isLoading={isFetchingListChartData || isLoadingListChartData}
data={chartData}
/>
{stagedQuery && ( {stagedQuery && (
<ActionsWrapper> <ActionsWrapper>
<ExportPanel <ExportPanel

View File

@ -29,7 +29,7 @@ function SettingsDrawer({ drawerTitle }: { drawerTitle: string }): JSX.Element {
<DrawerContainer <DrawerContainer
title={drawerTitle} title={drawerTitle}
placement="right" placement="right"
width="50%" width="60%"
onClose={onClose} onClose={onClose}
open={visible} open={visible}
> >

View File

@ -0,0 +1,5 @@
.delete-variable-name {
font-weight: 700;
color: rgb(207, 19, 34);
font-style: italic;
}

View File

@ -18,10 +18,10 @@ import {
VariableQueryTypeArr, VariableQueryTypeArr,
VariableSortTypeArr, VariableSortTypeArr,
} from 'types/api/dashboard/getAll'; } from 'types/api/dashboard/getAll';
import { v4 } from 'uuid'; import { v4 as generateUUID } from 'uuid';
import { variablePropsToPayloadVariables } from '../../../utils'; import { variablePropsToPayloadVariables } from '../../../utils';
import { TVariableViewMode } from '../types'; import { TVariableMode } from '../types';
import { LabelContainer, VariableItemRow } from './styles'; import { LabelContainer, VariableItemRow } from './styles';
const { Option } = Select; const { Option } = Select;
@ -30,9 +30,9 @@ interface VariableItemProps {
variableData: IDashboardVariable; variableData: IDashboardVariable;
existingVariables: Record<string, IDashboardVariable>; existingVariables: Record<string, IDashboardVariable>;
onCancel: () => void; onCancel: () => void;
onSave: (name: string, arg0: IDashboardVariable, arg1: string) => void; onSave: (mode: TVariableMode, variableData: IDashboardVariable) => void;
validateName: (arg0: string) => boolean; validateName: (arg0: string) => boolean;
variableViewMode: TVariableViewMode; mode: TVariableMode;
} }
function VariableItem({ function VariableItem({
variableData, variableData,
@ -40,7 +40,7 @@ function VariableItem({
onCancel, onCancel,
onSave, onSave,
validateName, validateName,
variableViewMode, mode,
}: VariableItemProps): JSX.Element { }: VariableItemProps): JSX.Element {
const [variableName, setVariableName] = useState<string>( const [variableName, setVariableName] = useState<string>(
variableData.name || '', variableData.name || '',
@ -97,7 +97,7 @@ function VariableItem({
]); ]);
const handleSave = (): void => { const handleSave = (): void => {
const newVariableData: IDashboardVariable = { const variable: IDashboardVariable = {
name: variableName, name: variableName,
description: variableDescription, description: variableDescription,
type: queryType, type: queryType,
@ -111,16 +111,12 @@ function VariableItem({
selectedValue: (variableData.selectedValue || selectedValue: (variableData.selectedValue ||
variableTextboxValue) as never, variableTextboxValue) as never,
}), }),
modificationUUID: v4(), modificationUUID: generateUUID(),
id: variableData.id || generateUUID(),
order: variableData.order,
}; };
onSave(
variableName, onSave(mode, variable);
newVariableData,
(variableViewMode === 'EDIT' && variableName !== variableData.name
? variableData.name
: '') as string,
);
onCancel();
}; };
// Fetches the preview values for the SQL variable query // Fetches the preview values for the SQL variable query
@ -175,7 +171,6 @@ function VariableItem({
return ( return (
<div className="variable-item-container"> <div className="variable-item-container">
<div className="variable-item-content"> <div className="variable-item-content">
{/* <Typography.Title level={3}>Add Variable</Typography.Title> */}
<VariableItemRow> <VariableItemRow>
<LabelContainer> <LabelContainer>
<Typography>Name</Typography> <Typography>Name</Typography>

View File

@ -1,20 +1,78 @@
import '../DashboardSettings.styles.scss';
import { blue, red } from '@ant-design/colors'; import { blue, red } from '@ant-design/colors';
import { PlusOutlined } from '@ant-design/icons'; import { MenuOutlined, PlusOutlined } from '@ant-design/icons';
import { Button, Modal, Row, Space, Tag } from 'antd'; import type { DragEndEvent, UniqueIdentifier } from '@dnd-kit/core';
import { ResizeTable } from 'components/ResizeTable'; import {
DndContext,
PointerSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
import { arrayMove, SortableContext, useSortable } from '@dnd-kit/sortable';
// eslint-disable-next-line import/no-extraneous-dependencies
import { CSS } from '@dnd-kit/utilities';
import { Button, Modal, Row, Space, Table, Typography } from 'antd';
import { RowProps } from 'antd/lib';
import { convertVariablesToDbFormat } from 'container/NewDashboard/DashboardVariablesSelection/util';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import { PencilIcon, TrashIcon } from 'lucide-react'; import { PencilIcon, TrashIcon } from 'lucide-react';
import { useDashboard } from 'providers/Dashboard/Dashboard'; import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll'; import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
import { TVariableViewMode } from './types'; import { TVariableMode } from './types';
import VariableItem from './VariableItem/VariableItem'; import VariableItem from './VariableItem/VariableItem';
function TableRow({ children, ...props }: RowProps): JSX.Element {
const {
attributes,
listeners,
setNodeRef,
setActivatorNodeRef,
transform,
transition,
isDragging,
} = useSortable({
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
id: props['data-row-key'],
});
const style: React.CSSProperties = {
...props.style,
transform: CSS.Transform.toString(transform && { ...transform, scaleY: 1 }),
transition,
...(isDragging ? { position: 'relative', zIndex: 9999 } : {}),
};
return (
// eslint-disable-next-line react/jsx-props-no-spreading
<tr {...props} ref={setNodeRef} style={style} {...attributes}>
{React.Children.map(children, (child) => {
if ((child as React.ReactElement).key === 'sort') {
return React.cloneElement(child as React.ReactElement, {
children: (
<MenuOutlined
ref={setActivatorNodeRef}
style={{ touchAction: 'none', cursor: 'move' }}
// eslint-disable-next-line react/jsx-props-no-spreading
{...listeners}
/>
),
});
}
return child;
})}
</tr>
);
}
function VariablesSetting(): JSX.Element { function VariablesSetting(): JSX.Element {
const variableToDelete = useRef<string | null>(null); const variableToDelete = useRef<IDashboardVariable | null>(null);
const [deleteVariableModal, setDeleteVariableModal] = useState(false); const [deleteVariableModal, setDeleteVariableModal] = useState(false);
const { t } = useTranslation(['dashboard']); const { t } = useTranslation(['dashboard']);
@ -25,16 +83,15 @@ function VariablesSetting(): JSX.Element {
const { variables = {} } = selectedDashboard?.data || {}; const { variables = {} } = selectedDashboard?.data || {};
const variablesTableData = Object.keys(variables).map((variableName) => ({ const [variablesTableData, setVariablesTableData] = useState<any>([]);
key: variableName, const [variblesOrderArr, setVariablesOrderArr] = useState<number[]>([]);
name: variableName, const [existingVariableNamesMap, setExistingVariableNamesMap] = useState<
...variables[variableName], Record<string, string>
})); >({});
const [ const [variableViewMode, setVariableViewMode] = useState<null | TVariableMode>(
variableViewMode, null,
setVariableViewMode, );
] = useState<null | TVariableViewMode>(null);
const [ const [
variableEditData, variableEditData,
@ -47,7 +104,7 @@ function VariablesSetting(): JSX.Element {
}; };
const onVariableViewModeEnter = ( const onVariableViewModeEnter = (
viewType: TVariableViewMode, viewType: TVariableMode,
varData: IDashboardVariable, varData: IDashboardVariable,
): void => { ): void => {
setVariableEditData(varData); setVariableEditData(varData);
@ -56,6 +113,41 @@ function VariablesSetting(): JSX.Element {
const updateMutation = useUpdateDashboard(); const updateMutation = useUpdateDashboard();
useEffect(() => {
const tableRowData = [];
const variableOrderArr = [];
const variableNamesMap = {};
// eslint-disable-next-line no-restricted-syntax
for (const [key, value] of Object.entries(variables)) {
const { order, id, name } = value;
tableRowData.push({
key,
name: key,
...variables[key],
id,
});
if (name) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
variableNamesMap[name] = name;
}
if (order) {
variableOrderArr.push(order);
}
}
tableRowData.sort((a, b) => a.order - b.order);
variableOrderArr.sort((a, b) => a - b);
setVariablesTableData(tableRowData);
setVariablesOrderArr(variableOrderArr);
setExistingVariableNamesMap(variableNamesMap);
}, [variables]);
const updateVariables = ( const updateVariables = (
updatedVariablesData: Dashboard['data']['variables'], updatedVariablesData: Dashboard['data']['variables'],
): void => { ): void => {
@ -89,34 +181,58 @@ function VariablesSetting(): JSX.Element {
); );
}; };
const getVariableOrder = (): number => {
if (variblesOrderArr && variblesOrderArr.length > 0) {
return variblesOrderArr[variblesOrderArr.length - 1] + 1;
}
return 0;
};
const onVariableSaveHandler = ( const onVariableSaveHandler = (
name: string, mode: TVariableMode,
variableData: IDashboardVariable, variableData: IDashboardVariable,
oldName: string,
): void => { ): void => {
if (!variableData.name) { const updatedVariableData = {
return; ...variableData,
order: variableData?.order >= 0 ? variableData.order : getVariableOrder(),
};
const newVariablesArr = variablesTableData.map(
(variable: IDashboardVariable) => {
if (variable.id === updatedVariableData.id) {
return updatedVariableData;
} }
const newVariables = { ...variables }; return variable;
newVariables[name] = variableData; },
);
if (oldName) { if (mode === 'ADD') {
delete newVariables[oldName]; newVariablesArr.push(updatedVariableData);
} }
updateVariables(newVariables);
const variables = convertVariablesToDbFormat(newVariablesArr);
setVariablesTableData(newVariablesArr);
updateVariables(variables);
onDoneVariableViewMode(); onDoneVariableViewMode();
}; };
const onVariableDeleteHandler = (variableName: string): void => { const onVariableDeleteHandler = (variable: IDashboardVariable): void => {
variableToDelete.current = variableName; variableToDelete.current = variable;
setDeleteVariableModal(true); setDeleteVariableModal(true);
}; };
const handleDeleteConfirm = (): void => { const handleDeleteConfirm = (): void => {
const newVariables = { ...variables }; const newVariablesArr = variablesTableData.filter(
if (variableToDelete?.current) delete newVariables[variableToDelete?.current]; (variable: IDashboardVariable) =>
updateVariables(newVariables); variable.id !== variableToDelete?.current?.id,
);
const updatedVariables = convertVariablesToDbFormat(newVariablesArr);
updateVariables(updatedVariables);
variableToDelete.current = null; variableToDelete.current = null;
setDeleteVariableModal(false); setDeleteVariableModal(false);
}; };
@ -125,31 +241,36 @@ function VariablesSetting(): JSX.Element {
setDeleteVariableModal(false); setDeleteVariableModal(false);
}; };
const validateVariableName = (name: string): boolean => !variables[name]; const validateVariableName = (name: string): boolean =>
!existingVariableNamesMap[name];
const columns = [ const columns = [
{
key: 'sort',
width: '10%',
},
{ {
title: 'Variable', title: 'Variable',
dataIndex: 'name', dataIndex: 'name',
width: 100, width: '40%',
key: 'name', key: 'name',
}, },
{ {
title: 'Description', title: 'Description',
dataIndex: 'description', dataIndex: 'description',
width: 100, width: '35%',
key: 'description', key: 'description',
}, },
{ {
title: 'Actions', title: 'Actions',
width: 50, width: '15%',
key: 'action', key: 'action',
render: (_: IDashboardVariable): JSX.Element => ( render: (variable: IDashboardVariable): JSX.Element => (
<Space> <Space>
<Button <Button
type="text" type="text"
style={{ padding: 8, cursor: 'pointer', color: blue[5] }} style={{ padding: 8, cursor: 'pointer', color: blue[5] }}
onClick={(): void => onVariableViewModeEnter('EDIT', _)} onClick={(): void => onVariableViewModeEnter('EDIT', variable)}
> >
<PencilIcon size={14} /> <PencilIcon size={14} />
</Button> </Button>
@ -157,7 +278,9 @@ function VariablesSetting(): JSX.Element {
type="text" type="text"
style={{ padding: 8, color: red[6], cursor: 'pointer' }} style={{ padding: 8, color: red[6], cursor: 'pointer' }}
onClick={(): void => { onClick={(): void => {
if (_.name) onVariableDeleteHandler(_.name); if (variable) {
onVariableDeleteHandler(variable);
}
}} }}
> >
<TrashIcon size={14} /> <TrashIcon size={14} />
@ -167,6 +290,51 @@ function VariablesSetting(): JSX.Element {
}, },
]; ];
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
// https://docs.dndkit.com/api-documentation/sensors/pointer#activation-constraints
distance: 1,
},
}),
);
const onDragEnd = ({ active, over }: DragEndEvent): void => {
if (active.id !== over?.id) {
const activeIndex = variablesTableData.findIndex(
(i: { key: UniqueIdentifier }) => i.key === active.id,
);
const overIndex = variablesTableData.findIndex(
(i: { key: UniqueIdentifier | undefined }) => i.key === over?.id,
);
const updatedVariables: IDashboardVariable[] = arrayMove(
variablesTableData,
activeIndex,
overIndex,
);
const reArrangedVariables = {};
for (let index = 0; index < updatedVariables.length; index += 1) {
const variableName = updatedVariables[index].name;
if (variableName) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
reArrangedVariables[variableName] = {
...updatedVariables[index],
order: index,
};
}
}
updateVariables(reArrangedVariables);
setVariablesTableData(updatedVariables);
}
};
return ( return (
<> <>
{variableViewMode ? ( {variableViewMode ? (
@ -176,11 +344,17 @@ function VariablesSetting(): JSX.Element {
onSave={onVariableSaveHandler} onSave={onVariableSaveHandler}
onCancel={onDoneVariableViewMode} onCancel={onDoneVariableViewMode}
validateName={validateVariableName} validateName={validateVariableName}
variableViewMode={variableViewMode} mode={variableViewMode}
/> />
) : ( ) : (
<> <>
<Row style={{ flexDirection: 'row-reverse', padding: '0.5rem 0' }}> <Row
style={{
flexDirection: 'row',
justifyContent: 'flex-end',
padding: '0.5rem 0',
}}
>
<Button <Button
data-testid="add-new-variable" data-testid="add-new-variable"
type="primary" type="primary"
@ -192,7 +366,28 @@ function VariablesSetting(): JSX.Element {
</Button> </Button>
</Row> </Row>
<ResizeTable columns={columns} dataSource={variablesTableData} /> <DndContext
sensors={sensors}
modifiers={[restrictToVerticalAxis]}
onDragEnd={onDragEnd}
>
<SortableContext
// rowKey array
items={variablesTableData.map((variable: { key: any }) => variable.key)}
>
<Table
components={{
body: {
row: TableRow,
},
}}
rowKey="key"
columns={columns}
pagination={false}
dataSource={variablesTableData}
/>
</SortableContext>
</DndContext>
</> </>
)} )}
<Modal <Modal
@ -202,8 +397,13 @@ function VariablesSetting(): JSX.Element {
onOk={handleDeleteConfirm} onOk={handleDeleteConfirm}
onCancel={handleDeleteCancel} onCancel={handleDeleteCancel}
> >
<Typography.Text>
Are you sure you want to delete variable{' '} Are you sure you want to delete variable{' '}
<Tag>{variableToDelete.current}</Tag>? <span className="delete-variable-name">
{variableToDelete?.current?.name}
</span>
?
</Typography.Text>
</Modal> </Modal>
</> </>
); );

View File

@ -1 +1,7 @@
export type TVariableViewMode = 'EDIT' | 'ADD'; export type TVariableMode = 'VIEW' | 'EDIT' | 'ADD';
export const VariableModes = {
VIEW: 'VIEW',
EDIT: 'EDIT',
ADD: 'ADD',
};

View File

@ -1,14 +1,14 @@
import { Row } from 'antd'; import { Row } from 'antd';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import { map, sortBy } from 'lodash-es';
import { useDashboard } from 'providers/Dashboard/Dashboard'; import { useDashboard } from 'providers/Dashboard/Dashboard';
import { memo, useState } from 'react'; import { memo, useEffect, useState } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll'; import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
import AppReducer from 'types/reducer/app'; import AppReducer from 'types/reducer/app';
import { convertVariablesToDbFormat } from './util';
import VariableItem from './VariableItem'; import VariableItem from './VariableItem';
function DashboardVariableSelection(): JSX.Element | null { function DashboardVariableSelection(): JSX.Element | null {
@ -21,8 +21,32 @@ function DashboardVariableSelection(): JSX.Element | null {
const [update, setUpdate] = useState<boolean>(false); const [update, setUpdate] = useState<boolean>(false);
const [lastUpdatedVar, setLastUpdatedVar] = useState<string>(''); const [lastUpdatedVar, setLastUpdatedVar] = useState<string>('');
const [variablesTableData, setVariablesTableData] = useState<any>([]);
const { role } = useSelector<AppState, AppReducer>((state) => state.app); const { role } = useSelector<AppState, AppReducer>((state) => state.app);
useEffect(() => {
if (variables) {
const tableRowData = [];
// eslint-disable-next-line no-restricted-syntax
for (const [key, value] of Object.entries(variables)) {
const { id } = value;
tableRowData.push({
key,
name: key,
...variables[key],
id,
});
}
tableRowData.sort((a, b) => a.order - b.order);
setVariablesTableData(tableRowData);
}
}, [variables]);
const onVarChanged = (name: string): void => { const onVarChanged = (name: string): void => {
setLastUpdatedVar(name); setLastUpdatedVar(name);
setUpdate(!update); setUpdate(!update);
@ -64,40 +88,56 @@ function DashboardVariableSelection(): JSX.Element | null {
const onValueUpdate = ( const onValueUpdate = (
name: string, name: string,
id: string,
value: IDashboardVariable['selectedValue'], value: IDashboardVariable['selectedValue'],
allSelected: boolean, allSelected: boolean,
): void => { ): void => {
const updatedVariablesData = { ...variables }; if (id) {
updatedVariablesData[name].selectedValue = value; const newVariablesArr = variablesTableData.map(
updatedVariablesData[name].allSelected = allSelected; (variable: IDashboardVariable) => {
const variableCopy = { ...variable };
console.log('onValue Update', name); if (variableCopy.id === id) {
variableCopy.selectedValue = value;
variableCopy.allSelected = allSelected;
}
return variableCopy;
},
);
const variables = convertVariablesToDbFormat(newVariablesArr);
if (role !== 'VIEWER' && selectedDashboard) { if (role !== 'VIEWER' && selectedDashboard) {
updateVariables(name, updatedVariablesData); updateVariables(name, variables);
} }
onVarChanged(name); onVarChanged(name);
setUpdate(!update); setUpdate(!update);
}
}; };
if (!variables) { if (!variables) {
return null; return null;
} }
const variablesKeys = sortBy(Object.keys(variables)); const orderBasedSortedVariables = variablesTableData.sort(
(a: { order: number }, b: { order: number }) => a.order - b.order,
);
return ( return (
<Row> <Row>
{variablesKeys && {orderBasedSortedVariables &&
map(variablesKeys, (variableName) => ( Array.isArray(orderBasedSortedVariables) &&
orderBasedSortedVariables.length > 0 &&
orderBasedSortedVariables.map((variable) => (
<VariableItem <VariableItem
key={`${variableName}${variables[variableName].modificationUUID}`} key={`${variable.name}${variable.id}}${variable.order}`}
existingVariables={variables} existingVariables={variables}
lastUpdatedVar={lastUpdatedVar} lastUpdatedVar={lastUpdatedVar}
variableData={{ variableData={{
name: variableName, name: variable.name,
...variables[variableName], ...variable,
change: update, change: update,
}} }}
onValueUpdate={onValueUpdate} onValueUpdate={onValueUpdate}

View File

@ -14,6 +14,7 @@ import { IDashboardVariable } from 'types/api/dashboard/getAll';
import VariableItem from './VariableItem'; import VariableItem from './VariableItem';
const mockVariableData: IDashboardVariable = { const mockVariableData: IDashboardVariable = {
id: 'test_variable',
description: 'Test Variable', description: 'Test Variable',
type: 'TEXTBOX', type: 'TEXTBOX',
textboxValue: 'defaultValue', textboxValue: 'defaultValue',
@ -95,6 +96,7 @@ describe('VariableItem', () => {
// expect(mockOnValueUpdate).toHaveBeenCalledTimes(1); // expect(mockOnValueUpdate).toHaveBeenCalledTimes(1);
expect(mockOnValueUpdate).toHaveBeenCalledWith( expect(mockOnValueUpdate).toHaveBeenCalledWith(
'testVariable', 'testVariable',
'test_variable',
'newValue', 'newValue',
false, false,
); );

View File

@ -2,13 +2,14 @@ import './DashboardVariableSelection.styles.scss';
import { orange } from '@ant-design/colors'; import { orange } from '@ant-design/colors';
import { WarningOutlined } from '@ant-design/icons'; import { WarningOutlined } from '@ant-design/icons';
import { Input, Popover, Select, Typography } from 'antd'; import { Input, Popover, Select, Tooltip, Typography } from 'antd';
import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQuery'; import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQuery';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import useDebounce from 'hooks/useDebounce'; import useDebounce from 'hooks/useDebounce';
import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParser'; import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParser';
import sortValues from 'lib/dashbaordVariables/sortVariableValues'; import sortValues from 'lib/dashbaordVariables/sortVariableValues';
import map from 'lodash-es/map'; import map from 'lodash-es/map';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { memo, useEffect, useMemo, useState } from 'react'; import { memo, useEffect, useMemo, useState } from 'react';
import { useQuery } from 'react-query'; import { useQuery } from 'react-query';
import { IDashboardVariable } from 'types/api/dashboard/getAll'; import { IDashboardVariable } from 'types/api/dashboard/getAll';
@ -27,6 +28,7 @@ interface VariableItemProps {
existingVariables: Record<string, IDashboardVariable>; existingVariables: Record<string, IDashboardVariable>;
onValueUpdate: ( onValueUpdate: (
name: string, name: string,
id: string,
arg1: IDashboardVariable['selectedValue'], arg1: IDashboardVariable['selectedValue'],
allSelected: boolean, allSelected: boolean,
) => void; ) => void;
@ -48,6 +50,7 @@ function VariableItem({
onValueUpdate, onValueUpdate,
lastUpdatedVar, lastUpdatedVar,
}: VariableItemProps): JSX.Element { }: VariableItemProps): JSX.Element {
const { isDashboardLocked } = useDashboard();
const [optionsData, setOptionsData] = useState<(string | number | boolean)[]>( const [optionsData, setOptionsData] = useState<(string | number | boolean)[]>(
[], [],
); );
@ -137,8 +140,9 @@ function VariableItem({
} else { } else {
[value] = newOptionsData; [value] = newOptionsData;
} }
if (variableData.name) {
onValueUpdate(variableData.name, value, allSelected); if (variableData && variableData?.name && variableData?.id) {
onValueUpdate(variableData.name, variableData.id, value, allSelected);
} }
} }
@ -149,14 +153,13 @@ function VariableItem({
console.error(e); console.error(e);
} }
} else if (variableData.type === 'CUSTOM') { } else if (variableData.type === 'CUSTOM') {
setOptionsData( const optionsData = sortValues(
sortValues(
commaValuesParser(variableData.customValue || ''), commaValuesParser(variableData.customValue || ''),
variableData.sort, variableData.sort,
) as never, ) as never;
);
setOptionsData(optionsData);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}; };
const { isLoading } = useQuery(getQueryKey(variableData), { const { isLoading } = useQuery(getQueryKey(variableData), {
@ -195,9 +198,9 @@ function VariableItem({
(Array.isArray(value) && value.includes(ALL_SELECT_VALUE)) || (Array.isArray(value) && value.includes(ALL_SELECT_VALUE)) ||
(Array.isArray(value) && value.length === 0) (Array.isArray(value) && value.length === 0)
) { ) {
onValueUpdate(variableData.name, optionsData, true); onValueUpdate(variableData.name, variableData.id, optionsData, true);
} else { } else {
onValueUpdate(variableData.name, value, false); onValueUpdate(variableData.name, variableData.id, value, false);
} }
}; };
@ -230,9 +233,13 @@ function VariableItem({
getOptions(null); getOptions(null);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, [variableData.type, variableData.customValue]);
return ( return (
<Tooltip
placement="top"
title={isDashboardLocked ? 'Dashboard is locked' : ''}
>
<VariableContainer> <VariableContainer>
<Typography.Text className="variable-name" ellipsis> <Typography.Text className="variable-name" ellipsis>
${variableData.name} ${variableData.name}
@ -241,6 +248,7 @@ function VariableItem({
{variableData.type === 'TEXTBOX' ? ( {variableData.type === 'TEXTBOX' ? (
<Input <Input
placeholder="Enter value" placeholder="Enter value"
disabled={isDashboardLocked}
bordered={false} bordered={false}
value={variableValue} value={variableValue}
onChange={(e): void => { onChange={(e): void => {
@ -266,6 +274,7 @@ function VariableItem({
showArrow showArrow
showSearch showSearch
data-testid="variable-select" data-testid="variable-select"
disabled={isDashboardLocked}
> >
{enableSelectAll && ( {enableSelectAll && (
<Select.Option data-testid="option-ALL" value={ALL_SELECT_VALUE}> <Select.Option data-testid="option-ALL" value={ALL_SELECT_VALUE}>
@ -284,7 +293,7 @@ function VariableItem({
</Select> </Select>
) )
)} )}
{errorMessage && ( {variableData.type !== 'TEXTBOX' && errorMessage && (
<span style={{ margin: '0 0.5rem' }}> <span style={{ margin: '0 0.5rem' }}>
<Popover <Popover
placement="top" placement="top"
@ -296,6 +305,7 @@ function VariableItem({
)} )}
</VariableValue> </VariableValue>
</VariableContainer> </VariableContainer>
</Tooltip>
); );
} }

View File

@ -1,3 +1,5 @@
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
export function areArraysEqual( export function areArraysEqual(
a: (string | number | boolean)[], a: (string | number | boolean)[],
b: (string | number | boolean)[], b: (string | number | boolean)[],
@ -14,3 +16,16 @@ export function areArraysEqual(
return true; return true;
} }
export const convertVariablesToDbFormat = (
variblesArr: IDashboardVariable[],
): Dashboard['data']['variables'] =>
variblesArr.reduce((result, obj: IDashboardVariable) => {
const { id } = obj;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// eslint-disable-next-line no-param-reassign
result[id] = obj;
return result;
}, {});

View File

@ -6,8 +6,10 @@ export function variablePropsToPayloadVariables(
): PayloadVariables { ): PayloadVariables {
const payloadVariables: PayloadVariables = {}; const payloadVariables: PayloadVariables = {};
Object.entries(variables).forEach(([key, value]) => { Object.entries(variables).forEach(([, value]) => {
payloadVariables[key] = value?.selectedValue; if (value?.name) {
payloadVariables[value.name] = value?.selectedValue;
}
}); });
return payloadVariables; return payloadVariables;

View File

@ -6,13 +6,16 @@ import { useResizeObserver } from 'hooks/useDimensions';
import useUrlQuery from 'hooks/useUrlQuery'; import useUrlQuery from 'hooks/useUrlQuery';
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions'; import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData'; import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { useCallback, useMemo, useRef } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { UseQueryResult } from 'react-query'; import { UseQueryResult } from 'react-query';
import { useDispatch } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { UpdateTimeInterval } from 'store/actions'; import { UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers';
import { SuccessResponse } from 'types/api'; import { SuccessResponse } from 'types/api';
import { Widgets } from 'types/api/dashboard/getAll'; import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { GlobalReducer } from 'types/reducer/globalTime';
import { getTimeRange } from 'utils/getTimeRange';
function WidgetGraph({ function WidgetGraph({
getWidgetQueryRange, getWidgetQueryRange,
@ -23,6 +26,21 @@ function WidgetGraph({
}: WidgetGraphProps): JSX.Element { }: WidgetGraphProps): JSX.Element {
const { stagedQuery } = useQueryBuilder(); const { stagedQuery } = useQueryBuilder();
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const [minTimeScale, setMinTimeScale] = useState<number>();
const [maxTimeScale, setMaxTimeScale] = useState<number>();
useEffect((): void => {
const { startTime, endTime } = getTimeRange(getWidgetQueryRange);
setMinTimeScale(startTime);
setMaxTimeScale(endTime);
}, [getWidgetQueryRange, maxTime, minTime, globalSelectedInterval]);
const graphRef = useRef<HTMLDivElement>(null); const graphRef = useRef<HTMLDivElement>(null);
const containerDimensions = useResizeObserver(graphRef); const containerDimensions = useResizeObserver(graphRef);
@ -63,6 +81,8 @@ function WidgetGraph({
onDragSelect, onDragSelect,
thresholds, thresholds,
fillSpans, fillSpans,
minTimeScale,
maxTimeScale,
}), }),
[ [
widgetId, widgetId,
@ -73,6 +93,8 @@ function WidgetGraph({
onDragSelect, onDragSelect,
thresholds, thresholds,
fillSpans, fillSpans,
minTimeScale,
maxTimeScale,
], ],
); );

View File

@ -12,8 +12,7 @@ export const Container = styled(Card)<Props>`
} }
.ant-card-body { .ant-card-body {
padding: ${({ $panelType }): string => padding: 8px;
$panelType === PANEL_TYPES.TABLE ? '0 0' : '1.5rem 0'};
height: 57vh; height: 57vh;
overflow: auto; overflow: auto;
display: flex; display: flex;

View File

@ -10,11 +10,9 @@ import {
} from 'antd'; } from 'antd';
import InputComponent from 'components/Input'; import InputComponent from 'components/Input';
import TimePreference from 'components/TimePreferenceDropDown'; import TimePreference from 'components/TimePreferenceDropDown';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder'; import { PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import GraphTypes from 'container/NewDashboard/ComponentsSlider/menuItems'; import GraphTypes from 'container/NewDashboard/ComponentsSlider/menuItems';
import history from 'lib/history'; import useCreateAlerts from 'hooks/queryBuilder/useCreateAlerts';
import { Dispatch, SetStateAction, useCallback } from 'react'; import { Dispatch, SetStateAction, useCallback } from 'react';
import { Widgets } from 'types/api/dashboard/getAll'; import { Widgets } from 'types/api/dashboard/getAll';
@ -55,15 +53,7 @@ function RightContainer({
const selectedGraphType = const selectedGraphType =
GraphTypes.find((e) => e.name === selectedGraph)?.display || ''; GraphTypes.find((e) => e.name === selectedGraph)?.display || '';
const onCreateAlertsHandler = useCallback(() => { const onCreateAlertsHandler = useCreateAlerts(selectedWidget);
if (!selectedWidget) return;
history.push(
`${ROUTES.ALERTS_NEW}?${QueryParams.compositeQuery}=${encodeURIComponent(
JSON.stringify(selectedWidget?.query),
)}`,
);
}, [selectedWidget]);
const allowThreshold = panelTypeVsThreshold[selectedGraph]; const allowThreshold = panelTypeVsThreshold[selectedGraph];

View File

@ -1,5 +1,6 @@
import { LockFilled } from '@ant-design/icons'; /* eslint-disable sonarjs/cognitive-complexity */
import { Button, Modal, Tooltip, Typography } from 'antd'; import { LockFilled, WarningOutlined } from '@ant-design/icons';
import { Button, Modal, Space, Tooltip, Typography } from 'antd';
import { SOMETHING_WENT_WRONG } from 'constants/api'; import { SOMETHING_WENT_WRONG } from 'constants/api';
import { FeatureKeys } from 'constants/features'; import { FeatureKeys } from 'constants/features';
import { PANEL_TYPES } from 'constants/queryBuilder'; import { PANEL_TYPES } from 'constants/queryBuilder';
@ -18,6 +19,7 @@ import {
getSelectedWidgetIndex, getSelectedWidgetIndex,
} from 'providers/Dashboard/util'; } from 'providers/Dashboard/util';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { generatePath, useLocation, useParams } from 'react-router-dom'; import { generatePath, useLocation, useParams } from 'react-router-dom';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
@ -39,6 +41,7 @@ import {
RightContainerWrapper, RightContainerWrapper,
} from './styles'; } from './styles';
import { NewWidgetProps } from './types'; import { NewWidgetProps } from './types';
import { getIsQueryModified } from './utils';
function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element { function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
const { const {
@ -47,7 +50,14 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
setToScrollWidgetId, setToScrollWidgetId,
} = useDashboard(); } = useDashboard();
const { currentQuery } = useQueryBuilder(); const { t } = useTranslation(['dashboard']);
const { currentQuery, stagedQuery } = useQueryBuilder();
const isQueryModified = useMemo(
() => getIsQueryModified(currentQuery, stagedQuery),
[currentQuery, stagedQuery],
);
const { featureResponse } = useSelector<AppState, AppReducer>( const { featureResponse } = useSelector<AppState, AppReducer>(
(state) => state.app, (state) => state.app,
@ -92,6 +102,12 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
selectedWidget?.fillSpans || false, selectedWidget?.fillSpans || false,
); );
const [saveModal, setSaveModal] = useState(false); const [saveModal, setSaveModal] = useState(false);
const [discardModal, setDiscardModal] = useState(false);
const closeModal = (): void => {
setSaveModal(false);
setDiscardModal(false);
};
const [graphType, setGraphType] = useState(selectedGraph); const [graphType, setGraphType] = useState(selectedGraph);
@ -206,6 +222,14 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
]); ]);
const onClickDiscardHandler = useCallback(() => { const onClickDiscardHandler = useCallback(() => {
if (isQueryModified) {
setDiscardModal(true);
return;
}
history.push(generatePath(ROUTES.DASHBOARD, { dashboardId }));
}, [dashboardId, isQueryModified]);
const discardChanges = useCallback(() => {
history.push(generatePath(ROUTES.DASHBOARD, { dashboardId })); history.push(generatePath(ROUTES.DASHBOARD, { dashboardId }));
}, [dashboardId]); }, [dashboardId]);
@ -321,21 +345,54 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
</RightContainerWrapper> </RightContainerWrapper>
</PanelContainer> </PanelContainer>
<Modal <Modal
title="Save Changes" title={
isQueryModified ? (
<Space>
<WarningOutlined style={{ fontSize: '16px', color: '#fdd600' }} />
Unsaved Changes
</Space>
) : (
'Save Widget'
)
}
focusTriggerAfterClose focusTriggerAfterClose
forceRender forceRender
destroyOnClose destroyOnClose
closable closable
onCancel={(): void => setSaveModal(false)} onCancel={closeModal}
onOk={onClickSaveHandler} onOk={onClickSaveHandler}
centered centered
open={saveModal} open={saveModal}
width={600} width={600}
> >
{!isQueryModified ? (
<Typography> <Typography>
Your graph built with <QueryTypeTag queryType={currentQuery.queryType} />{' '} {t('your_graph_build_with')}{' '}
query will be saved. Press OK to confirm. <QueryTypeTag queryType={currentQuery.queryType} />
{t('dashboar_ok_confirm')}
</Typography> </Typography>
) : (
<Typography>{t('dashboard_unsave_changes')} </Typography>
)}
</Modal>
<Modal
title={
<Space>
<WarningOutlined style={{ fontSize: '16px', color: '#fdd600' }} />
Unsaved Changes
</Space>
}
focusTriggerAfterClose
forceRender
destroyOnClose
closable
onCancel={closeModal}
onOk={discardChanges}
centered
open={discardModal}
width={600}
>
<Typography>{t('dashboard_unsave_changes')}</Typography>
</Modal> </Modal>
</Container> </Container>
); );

View File

@ -0,0 +1,15 @@
import { omitIdFromQuery } from 'components/ExplorerCard/utils';
import { isEqual } from 'lodash-es';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
export const getIsQueryModified = (
currentQuery: Query,
stagedQuery: Query | null,
): boolean => {
if (!stagedQuery) {
return false;
}
const omitIdFromStageQuery = omitIdFromQuery(stagedQuery);
const omitIdFromCurrentQuery = omitIdFromQuery(currentQuery);
return !isEqual(omitIdFromStageQuery, omitIdFromCurrentQuery);
};

View File

@ -0,0 +1,24 @@
## Install otel-collector in your Kubernetes infra
&nbsp;
Add the SigNoz Helm Chart repository
```bash
helm repo add signoz https://charts.signoz.io
```
&nbsp;
If the chart is already present, update the chart to the latest using:
```bash
helm repo update
```
&nbsp;
Install the Kubernetes Infrastructure chart provided by SigNoz
```bash
helm install my-release signoz/k8s-infra \
--set otelCollectorEndpoint=ingest.{{REGION}}.signoz.cloud:443 \
--set otelInsecure=false \
--set signozApiKey={{SIGNOZ_INGESTION_KEY}} \
--set global.clusterName=<CLUSTER_NAME>
```
- Replace `<CLUSTER_NAME>` with the name of the Kubernetes cluster or a unique identifier of the cluster.

View File

@ -0,0 +1,65 @@
After setting up the Otel collector agent, follow the steps below to instrument your .NET Application
&nbsp;
&nbsp;
### Step 1: Install OpenTelemetry Dependencies
Install the following dependencies in your application.
```bash
dotnet add package OpenTelemetry
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Instrumentation.Runtime
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
dotnet add package OpenTelemetry.AutoInstrumentation
```
&nbsp;
### Step 2: Adding OpenTelemetry as a service and configuring exporter options
In your `Program.cs` file, add OpenTelemetry as a service. Here, we are configuring these variables:
`serviceName` - It is the name of your service.
`otlpOptions.Endpoint` - It is the endpoint for your OTel Collector agent.
&nbsp;
Heres a sample `Program.cs` file with the configured variables:
```bash
using System.Diagnostics;
using OpenTelemetry.Exporter;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
var builder = WebApplication.CreateBuilder(args);
// Configure OpenTelemetry with tracing and auto-start.
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource =>
resource.AddService(serviceName: "{{MYAPP}}"))
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddOtlpExporter(otlpOptions =>
{
otlpOptions.Endpoint = new Uri("http://localhost:4317");
otlpOptions.Protocol = OtlpExportProtocol.Grpc;
}));
var app = builder.Build();
//The index route ("/") is set up to write out the OpenTelemetry trace information on the response:
app.MapGet("/", () => $"Hello World! OpenTelemetry Trace: {Activity.Current?.Id}");
app.Run();
```
The OpenTelemetry.Exporter.Options get or set the target to which the exporter is going to send traces. Here, were configuring it to send traces to the OTel Collector agent. The target must be a valid Uri with the scheme (http or https) and host and may contain a port and a path.
This is done by configuring an OpenTelemetry [TracerProvider](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/docs/trace/customizing-the-sdk#readme) using extension methods and setting it to auto-start when the host is started.

View File

@ -0,0 +1,10 @@
&nbsp;
To run your .NET application, use the below command :
```bash
dotnet build
dotnet run
```
Once you run your .NET application, interact with your application to generate some load and see your application in the SigNoz UI.

View File

@ -0,0 +1,71 @@
### Step 1: Install OpenTelemetry Dependencies
Dependencies related to OpenTelemetry exporter and SDK have to be installed first.
Run the below commands after navigating to the application source folder:
```bash
dotnet add package OpenTelemetry
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Instrumentation.Runtime
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
dotnet add package OpenTelemetry.AutoInstrumentation
```
&nbsp;
### Step 2: Adding OpenTelemetry as a service and configuring exporter options
In your `Program.cs` file, add OpenTelemetry as a service. Here, we are configuring these variables:
`serviceName` - It is the name of your service.
`otlpOptions.Endpoint` - It is the endpoint for your OTel Collector agent.
&nbsp;
Heres a sample `Program.cs` file with the configured variables:
```bash
using System.Diagnostics;
using OpenTelemetry.Exporter;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
var builder = WebApplication.CreateBuilder(args);
// Configure OpenTelemetry with tracing and auto-start.
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource =>
resource.AddService(serviceName: "{{MYAPP}}"))
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddOtlpExporter(otlpOptions =>
{
//sigNoz Cloud Endpoint
otlpOptions.Endpoint = new Uri("https://ingest.{{REGION}}.signoz.cloud:443");
otlpOptions.Protocol = OtlpExportProtocol.Grpc;
//SigNoz Cloud account Ingestion key
string headerKey = "signoz-access-token";
string headerValue = "{{SIGNOZ_INGESTION_KEY}}";
string formattedHeader = $"{headerKey}={headerValue}";
otlpOptions.Headers = formattedHeader;
}));
var app = builder.Build();
//The index route ("/") is set up to write out the OpenTelemetry trace information on the response:
app.MapGet("/", () => $"Hello World! OpenTelemetry Trace: {Activity.Current?.Id}");
app.Run();
```
&nbsp;
The OpenTelemetry.Exporter.Options get or set the target to which the exporter is going to send traces. Here, were configuring it to send traces to the OTel Collector agent. The target must be a valid Uri with the scheme (http or https) and host and may contain a port and a path.
This is done by configuring an OpenTelemetry [TracerProvider](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/docs/trace/customizing-the-sdk#readme) using extension methods and setting it to auto-start when the host is started.

View File

@ -0,0 +1,10 @@
&nbsp;
To run your .NET application, use the below command :
```bash
dotnet build
dotnet run
```
Once you run your .NET application, interact with your application to generate some load and see your application in the SigNoz UI.

View File

@ -0,0 +1,98 @@
## Setup OpenTelemetry Binary as an agent
&nbsp;
### Step 1: Download otel-collector tar.gz
```bash
wget https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v0.79.0/otelcol-contrib_0.79.0_linux_amd64.tar.gz
```
&nbsp;
### Step 2: Extract otel-collector tar.gz to the `otelcol-contrib` folder
```bash
mkdir otelcol-contrib && tar xvzf otelcol-contrib_0.79.0_linux_amd64.tar.gz -C otelcol-contrib
```
&nbsp;
### Step 3: Create `config.yaml` in `otelcol-contrib` folder with the below content in it
```bash
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
hostmetrics:
collection_interval: 60s
scrapers:
cpu: {}
disk: {}
load: {}
filesystem: {}
memory: {}
network: {}
paging: {}
process:
mute_process_name_error: true
mute_process_exe_error: true
mute_process_io_error: true
processes: {}
prometheus:
config:
global:
scrape_interval: 60s
scrape_configs:
- job_name: otel-collector-binary
static_configs:
- targets:
# - localhost:8888
processors:
batch:
send_batch_size: 1000
timeout: 10s
# Ref: https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/processor/resourcedetectionprocessor/README.md
resourcedetection:
detectors: [env, system] # Before system detector, include ec2 for AWS, gcp for GCP and azure for Azure.
# Using OTEL_RESOURCE_ATTRIBUTES envvar, env detector adds custom labels.
timeout: 2s
system:
hostname_sources: [os] # alternatively, use [dns,os] for setting FQDN as host.name and os as fallback
extensions:
health_check: {}
zpages: {}
exporters:
otlp:
endpoint: "ingest.{{REGION}}.signoz.cloud:443"
tls:
insecure: false
headers:
"signoz-access-token": "{{SIGNOZ_INGESTION_KEY}}"
logging:
verbosity: normal
service:
telemetry:
metrics:
address: 0.0.0.0:8888
extensions: [health_check, zpages]
pipelines:
metrics:
receivers: [otlp]
processors: [batch]
exporters: [otlp]
metrics/internal:
receivers: [prometheus, hostmetrics]
processors: [resourcedetection, batch]
exporters: [otlp]
traces:
receivers: [otlp]
processors: [batch]
exporters: [otlp]
logs:
receivers: [otlp]
processors: [batch]
exporters: [otlp]
```

View File

@ -0,0 +1,67 @@
After setting up the Otel collector agent, follow the steps below to instrument your .NET Application
&nbsp;
&nbsp;
### Step 1: Install OpenTelemetry Dependencies
Install the following dependencies in your application.
```bash
dotnet add package OpenTelemetry
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Instrumentation.Runtime
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
dotnet add package OpenTelemetry.AutoInstrumentation
```
&nbsp;
### Step 2: Adding OpenTelemetry as a service and configuring exporter options
In your `Program.cs` file, add OpenTelemetry as a service. Here, we are configuring these variables:
`serviceName` - It is the name of your service.
`otlpOptions.Endpoint` - It is the endpoint for your OTel Collector agent.
&nbsp;
Heres a sample `Program.cs` file with the configured variables:
```bash
using System.Diagnostics;
using OpenTelemetry.Exporter;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
var builder = WebApplication.CreateBuilder(args);
// Configure OpenTelemetry with tracing and auto-start.
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource =>
resource.AddService(serviceName: "{{MYAPP}}"))
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddOtlpExporter(otlpOptions =>
{
otlpOptions.Endpoint = new Uri("http://localhost:4317");
otlpOptions.Protocol = OtlpExportProtocol.Grpc;
}));
var app = builder.Build();
//The index route ("/") is set up to write out the OpenTelemetry trace information on the response:
app.MapGet("/", () => $"Hello World! OpenTelemetry Trace: {Activity.Current?.Id}");
app.Run();
```
&nbsp;
The OpenTelemetry.Exporter.Options get or set the target to which the exporter is going to send traces. Here, were configuring it to send traces to the OTel Collector agent. The target must be a valid Uri with the scheme (http or https) and host and may contain a port and a path.
This is done by configuring an OpenTelemetry [TracerProvider](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/docs/trace/customizing-the-sdk#readme) using extension methods and setting it to auto-start when the host is started.

View File

@ -0,0 +1,18 @@
&nbsp;
Once you are done intrumenting your .NET application, you can run it using the below commands
&nbsp;
### Step 1: Run OTel Collector
Run this command inside the `otelcol-contrib` directory that you created in the install Otel Collector step
```bash
./otelcol-contrib --config ./config.yaml
```
&nbsp;
### Step 2: Run your .NET application
```bash
dotnet build
dotnet run
```

View File

@ -0,0 +1,70 @@
### Step 1: Install OpenTelemetry Dependencies
Dependencies related to OpenTelemetry exporter and SDK have to be installed first.
Run the below commands after navigating to the application source folder:
```bash
dotnet add package OpenTelemetry
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Instrumentation.Runtime
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
dotnet add package OpenTelemetry.AutoInstrumentation
```
&nbsp;
### Step 2: Adding OpenTelemetry as a service and configuring exporter options
In your `Program.cs` file, add OpenTelemetry as a service. Here, we are configuring these variables:
`serviceName` - It is the name of your service.
`otlpOptions.Endpoint` - It is the endpoint for your OTel Collector agent.
&nbsp;
Heres a sample `Program.cs` file with the configured variables:
```bash
using System.Diagnostics;
using OpenTelemetry.Exporter;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
var builder = WebApplication.CreateBuilder(args);
// Configure OpenTelemetry with tracing and auto-start.
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource =>
resource.AddService(serviceName: "{{MYAPP}}"))
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddOtlpExporter(otlpOptions =>
{
//sigNoz Cloud Endpoint
otlpOptions.Endpoint = new Uri("https://ingest.{{REGION}}.signoz.cloud:443");
otlpOptions.Protocol = OtlpExportProtocol.Grpc;
//SigNoz Cloud account Ingestion key
string headerKey = "signoz-access-token";
string headerValue = "{{SIGNOZ_INGESTION_KEY}}";
string formattedHeader = $"{headerKey}={headerValue}";
otlpOptions.Headers = formattedHeader;
}));
var app = builder.Build();
//The index route ("/") is set up to write out the OpenTelemetry trace information on the response:
app.MapGet("/", () => $"Hello World! OpenTelemetry Trace: {Activity.Current?.Id}");
app.Run();
```
&nbsp;
The OpenTelemetry.Exporter.Options get or set the target to which the exporter is going to send traces. Here, were configuring it to send traces to the OTel Collector agent. The target must be a valid Uri with the scheme (http or https) and host and may contain a port and a path.
This is done by configuring an OpenTelemetry [TracerProvider](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/docs/trace/customizing-the-sdk#readme) using extension methods and setting it to auto-start when the host is started.

View File

@ -0,0 +1,10 @@
&nbsp;
To run your .NET application, use the below command :
```bash
dotnet build
dotnet run
```
Once you run your .NET application, interact with your application to generate some load and see your application in the SigNoz UI.

View File

@ -0,0 +1,99 @@
## Setup OpenTelemetry Binary as an agent
&nbsp;
### Step 1: Download otel-collector tar.gz
```bash
wget https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v0.79.0/otelcol-contrib_0.79.0_linux_arm64.tar.gz
```
&nbsp;
### Step 2: Extract otel-collector tar.gz to the `otelcol-contrib` folder
```bash
mkdir otelcol-contrib && tar xvzf otelcol-contrib_0.79.0_linux_arm64.tar.gz -C otelcol-contrib
```
&nbsp;
### Step 3: Create `config.yaml` in `otelcol-contrib` folder with the below content in it
```bash
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
hostmetrics:
collection_interval: 60s
scrapers:
cpu: {}
disk: {}
load: {}
filesystem: {}
memory: {}
network: {}
paging: {}
process:
mute_process_name_error: true
mute_process_exe_error: true
mute_process_io_error: true
processes: {}
prometheus:
config:
global:
scrape_interval: 60s
scrape_configs:
- job_name: otel-collector-binary
static_configs:
- targets:
# - localhost:8888
processors:
batch:
send_batch_size: 1000
timeout: 10s
# Ref: https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/processor/resourcedetectionprocessor/README.md
resourcedetection:
detectors: [env, system] # Before system detector, include ec2 for AWS, gcp for GCP and azure for Azure.
# Using OTEL_RESOURCE_ATTRIBUTES envvar, env detector adds custom labels.
timeout: 2s
system:
hostname_sources: [os] # alternatively, use [dns,os] for setting FQDN as host.name and os as fallback
extensions:
health_check: {}
zpages: {}
exporters:
otlp:
endpoint: "ingest.{{REGION}}.signoz.cloud:443"
tls:
insecure: false
headers:
"signoz-access-token": "{{SIGNOZ_INGESTION_KEY}}"
logging:
verbosity: normal
service:
telemetry:
metrics:
address: 0.0.0.0:8888
extensions: [health_check, zpages]
pipelines:
metrics:
receivers: [otlp]
processors: [batch]
exporters: [otlp]
metrics/internal:
receivers: [prometheus, hostmetrics]
processors: [resourcedetection, batch]
exporters: [otlp]
traces:
receivers: [otlp]
processors: [batch]
exporters: [otlp]
logs:
receivers: [otlp]
processors: [batch]
exporters: [otlp]
```

View File

@ -0,0 +1,68 @@
After setting up the Otel collector agent, follow the steps below to instrument your .NET Application
&nbsp;
&nbsp;
### Step 1: Install OpenTelemetry Dependencies
Install the following dependencies in your application.
```bash
dotnet add package OpenTelemetry
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Instrumentation.Runtime
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
dotnet add package OpenTelemetry.AutoInstrumentation
```
&nbsp;
### Step 2: Adding OpenTelemetry as a service and configuring exporter options
In your `Program.cs` file, add OpenTelemetry as a service. Here, we are configuring these variables:
`serviceName` - It is the name of your service.
`otlpOptions.Endpoint` - It is the endpoint for your OTel Collector agent.
&nbsp;
Heres a sample `Program.cs` file with the configured variables:
```bash
using System.Diagnostics;
using OpenTelemetry.Exporter;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
var builder = WebApplication.CreateBuilder(args);
// Configure OpenTelemetry with tracing and auto-start.
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource =>
resource.AddService(serviceName: "{{MYAPP}}"))
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddOtlpExporter(otlpOptions =>
{
otlpOptions.Endpoint = new Uri("http://localhost:4317");
otlpOptions.Protocol = OtlpExportProtocol.Grpc;
}));
var app = builder.Build();
//The index route ("/") is set up to write out the OpenTelemetry trace information on the response:
app.MapGet("/", () => $"Hello World! OpenTelemetry Trace: {Activity.Current?.Id}");
app.Run();
```
&nbsp;
The OpenTelemetry.Exporter.Options get or set the target to which the exporter is going to send traces. Here, were configuring it to send traces to the OTel Collector agent. The target must be a valid Uri with the scheme (http or https) and host and may contain a port and a path.
This is done by configuring an OpenTelemetry [TracerProvider](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/docs/trace/customizing-the-sdk#readme) using extension methods and setting it to auto-start when the host is started.

View File

@ -0,0 +1,18 @@
&nbsp;
Once you are done intrumenting your .NET application, you can run it using the below commands
&nbsp;
### Step 1: Run OTel Collector
Run this command inside the `otelcol-contrib` directory that you created in the install Otel Collector step
```bash
./otelcol-contrib --config ./config.yaml
```
&nbsp;
### Step 2: Run your .NET application
```bash
dotnet build
dotnet run
```

View File

@ -0,0 +1,70 @@
### Step 1: Install OpenTelemetry Dependencies
Dependencies related to OpenTelemetry exporter and SDK have to be installed first.
Run the below commands after navigating to the application source folder:
```bash
dotnet add package OpenTelemetry
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Instrumentation.Runtime
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
dotnet add package OpenTelemetry.AutoInstrumentation
```
&nbsp;
### Step 2: Adding OpenTelemetry as a service and configuring exporter options
In your `Program.cs` file, add OpenTelemetry as a service. Here, we are configuring these variables:
`serviceName` - It is the name of your service.
`otlpOptions.Endpoint` - It is the endpoint for your OTel Collector agent.
&nbsp;
Heres a sample `Program.cs` file with the configured variables:
```bash
using System.Diagnostics;
using OpenTelemetry.Exporter;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
var builder = WebApplication.CreateBuilder(args);
// Configure OpenTelemetry with tracing and auto-start.
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource =>
resource.AddService(serviceName: "{{MYAPP}}"))
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddOtlpExporter(otlpOptions =>
{
//sigNoz Cloud Endpoint
otlpOptions.Endpoint = new Uri("https://ingest.{{REGION}}.signoz.cloud:443");
otlpOptions.Protocol = OtlpExportProtocol.Grpc;
//SigNoz Cloud account Ingestion key
string headerKey = "signoz-access-token";
string headerValue = "{{SIGNOZ_INGESTION_KEY}}";
string formattedHeader = $"{headerKey}={headerValue}";
otlpOptions.Headers = formattedHeader;
}));
var app = builder.Build();
//The index route ("/") is set up to write out the OpenTelemetry trace information on the response:
app.MapGet("/", () => $"Hello World! OpenTelemetry Trace: {Activity.Current?.Id}");
app.Run();
```
&nbsp;
The OpenTelemetry.Exporter.Options get or set the target to which the exporter is going to send traces. Here, were configuring it to send traces to the OTel Collector agent. The target must be a valid Uri with the scheme (http or https) and host and may contain a port and a path.
This is done by configuring an OpenTelemetry [TracerProvider](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/docs/trace/customizing-the-sdk#readme) using extension methods and setting it to auto-start when the host is started.

View File

@ -0,0 +1,10 @@
&nbsp;
To run your .NET application, use the below command :
```bash
dotnet build
dotnet run
```
Once you run your .NET application, interact with your application to generate some load and see your application in the SigNoz UI.

View File

@ -0,0 +1,97 @@
## Setup OpenTelemetry Binary as an agent
&nbsp;
### Step 1: Download otel-collector tar.gz
```bash
wget https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v0.79.0/otelcol-contrib_0.79.0_darwin_amd64.tar.gz
```
&nbsp;
### Step 2: Extract otel-collector tar.gz to the `otelcol-contrib` folder
```bash
mkdir otelcol-contrib && tar xvzf otelcol-contrib_0.79.0_darwin_amd64.tar.gz -C otelcol-contrib
```
&nbsp;
### Step 3: Create `config.yaml` in folder `otelcol-contrib` with the below content in it
```bash
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
hostmetrics:
collection_interval: 60s
scrapers:
cpu: {}
disk: {}
load: {}
filesystem: {}
memory: {}
network: {}
paging: {}
process:
mute_process_name_error: true
mute_process_exe_error: true
mute_process_io_error: true
processes: {}
prometheus:
config:
global:
scrape_interval: 60s
scrape_configs:
- job_name: otel-collector-binary
static_configs:
- targets:
# - localhost:8888
processors:
batch:
send_batch_size: 1000
timeout: 10s
# Ref: https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/processor/resourcedetectionprocessor/README.md
resourcedetection:
detectors: [env, system] # Before system detector, include ec2 for AWS, gcp for GCP and azure for Azure.
# Using OTEL_RESOURCE_ATTRIBUTES envvar, env detector adds custom labels.
timeout: 2s
system:
hostname_sources: [os] # alternatively, use [dns,os] for setting FQDN as host.name and os as fallback
extensions:
health_check: {}
zpages: {}
exporters:
otlp:
endpoint: "ingest.{{REGION}}.signoz.cloud:443"
tls:
insecure: false
headers:
"signoz-access-token": "{{SIGNOZ_INGESTION_KEY}}"
logging:
verbosity: normal
service:
telemetry:
metrics:
address: 0.0.0.0:8888
extensions: [health_check, zpages]
pipelines:
metrics:
receivers: [otlp]
processors: [batch]
exporters: [otlp]
metrics/internal:
receivers: [prometheus, hostmetrics]
processors: [resourcedetection, batch]
exporters: [otlp]
traces:
receivers: [otlp]
processors: [batch]
exporters: [otlp]
logs:
receivers: [otlp]
processors: [batch]
exporters: [otlp]
```

View File

@ -0,0 +1,67 @@
After setting up the Otel collector agent, follow the steps below to instrument your .NET Application
&nbsp;
&nbsp;
### Step 1: Install OpenTelemetry Dependencies
Install the following dependencies in your application.
```bash
dotnet add package OpenTelemetry
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Instrumentation.Runtime
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
dotnet add package OpenTelemetry.AutoInstrumentation
```
&nbsp;
### Step 2: Adding OpenTelemetry as a service and configuring exporter options
In your `Program.cs` file, add OpenTelemetry as a service. Here, we are configuring these variables:
`serviceName` - It is the name of your service.
`otlpOptions.Endpoint` - It is the endpoint for your OTel Collector agent.
&nbsp;
Heres a sample `Program.cs` file with the configured variables:
```bash
using System.Diagnostics;
using OpenTelemetry.Exporter;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
var builder = WebApplication.CreateBuilder(args);
// Configure OpenTelemetry with tracing and auto-start.
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource =>
resource.AddService(serviceName: "{{MYAPP}}"))
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddOtlpExporter(otlpOptions =>
{
otlpOptions.Endpoint = new Uri("http://localhost:4317");
otlpOptions.Protocol = OtlpExportProtocol.Grpc;
}));
var app = builder.Build();
//The index route ("/") is set up to write out the OpenTelemetry trace information on the response:
app.MapGet("/", () => $"Hello World! OpenTelemetry Trace: {Activity.Current?.Id}");
app.Run();
```
&nbsp;
The OpenTelemetry.Exporter.Options get or set the target to which the exporter is going to send traces. Here, were configuring it to send traces to the OTel Collector agent. The target must be a valid Uri with the scheme (http or https) and host and may contain a port and a path.
This is done by configuring an OpenTelemetry [TracerProvider](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/docs/trace/customizing-the-sdk#readme) using extension methods and setting it to auto-start when the host is started.

View File

@ -0,0 +1,18 @@
&nbsp;
Once you are done intrumenting your .NET application, you can run it using the below commands
&nbsp;
### Step 1: Run OTel Collector
Run this command inside the `otelcol-contrib` directory that you created in the install Otel Collector step
```bash
./otelcol-contrib --config ./config.yaml
```
&nbsp;
### Step 2: Run your .NET application
```bash
dotnet build
dotnet run
```

View File

@ -0,0 +1,70 @@
### Step 1: Install OpenTelemetry Dependencies
Dependencies related to OpenTelemetry exporter and SDK have to be installed first.
Run the below commands after navigating to the application source folder:
```bash
dotnet add package OpenTelemetry
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Instrumentation.Runtime
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
dotnet add package OpenTelemetry.AutoInstrumentation
```
&nbsp;
### Step 2: Adding OpenTelemetry as a service and configuring exporter options
In your `Program.cs` file, add OpenTelemetry as a service. Here, we are configuring these variables:
`serviceName` - It is the name of your service.
`otlpOptions.Endpoint` - It is the endpoint for your OTel Collector agent.
&nbsp;
Heres a sample `Program.cs` file with the configured variables:
```bash
using System.Diagnostics;
using OpenTelemetry.Exporter;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
var builder = WebApplication.CreateBuilder(args);
// Configure OpenTelemetry with tracing and auto-start.
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource =>
resource.AddService(serviceName: "{{MYAPP}}"))
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddOtlpExporter(otlpOptions =>
{
//sigNoz Cloud Endpoint
otlpOptions.Endpoint = new Uri("https://ingest.{{REGION}}.signoz.cloud:443");
otlpOptions.Protocol = OtlpExportProtocol.Grpc;
//SigNoz Cloud account Ingestion key
string headerKey = "signoz-access-token";
string headerValue = "{{SIGNOZ_INGESTION_KEY}}";
string formattedHeader = $"{headerKey}={headerValue}";
otlpOptions.Headers = formattedHeader;
}));
var app = builder.Build();
//The index route ("/") is set up to write out the OpenTelemetry trace information on the response:
app.MapGet("/", () => $"Hello World! OpenTelemetry Trace: {Activity.Current?.Id}");
app.Run();
```
&nbsp;
The OpenTelemetry.Exporter.Options get or set the target to which the exporter is going to send traces. Here, were configuring it to send traces to the OTel Collector agent. The target must be a valid Uri with the scheme (http or https) and host and may contain a port and a path.
This is done by configuring an OpenTelemetry [TracerProvider](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/docs/trace/customizing-the-sdk#readme) using extension methods and setting it to auto-start when the host is started.

View File

@ -0,0 +1,10 @@
&nbsp;
To run your .NET application, use the below command :
```bash
dotnet build
dotnet run
```
Once you run your .NET application, interact with your application to generate some load and see your application in the SigNoz UI.

View File

@ -0,0 +1,98 @@
## Setup OpenTelemetry Binary as an agent
&nbsp;
### Step 1: Download otel-collector tar.gz
```bash
wget https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v0.79.0/otelcol-contrib_0.79.0_darwin_arm64.tar.gz
```
&nbsp;
### Step 2: Extract otel-collector tar.gz to the `otelcol-contrib` folder
```bash
mkdir otelcol-contrib && tar xvzf otelcol-contrib_0.79.0_darwin_arm64.tar.gz -C otelcol-contrib
```
&nbsp;
### Step 3: Create `config.yaml` in folder `otelcol-contrib` with the below content in it
```bash
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
hostmetrics:
collection_interval: 60s
scrapers:
cpu: {}
disk: {}
load: {}
filesystem: {}
memory: {}
network: {}
paging: {}
process:
mute_process_name_error: true
mute_process_exe_error: true
mute_process_io_error: true
processes: {}
prometheus:
config:
global:
scrape_interval: 60s
scrape_configs:
- job_name: otel-collector-binary
static_configs:
- targets:
# - localhost:8888
processors:
batch:
send_batch_size: 1000
timeout: 10s
# Ref: https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/processor/resourcedetectionprocessor/README.md
resourcedetection:
detectors: [env, system] # Before system detector, include ec2 for AWS, gcp for GCP and azure for Azure.
# Using OTEL_RESOURCE_ATTRIBUTES envvar, env detector adds custom labels.
timeout: 2s
system:
hostname_sources: [os] # alternatively, use [dns,os] for setting FQDN as host.name and os as fallback
extensions:
health_check: {}
zpages: {}
exporters:
otlp:
endpoint: "ingest.{{REGION}}.signoz.cloud:443"
tls:
insecure: false
headers:
"signoz-access-token": "{{SIGNOZ_INGESTION_KEY}}"
logging:
verbosity: normal
service:
telemetry:
metrics:
address: 0.0.0.0:8888
extensions: [health_check, zpages]
pipelines:
metrics:
receivers: [otlp]
processors: [batch]
exporters: [otlp]
metrics/internal:
receivers: [prometheus, hostmetrics]
processors: [resourcedetection, batch]
exporters: [otlp]
traces:
receivers: [otlp]
processors: [batch]
exporters: [otlp]
logs:
receivers: [otlp]
processors: [batch]
exporters: [otlp]
```

View File

@ -0,0 +1,68 @@
After setting up the Otel collector agent, follow the steps below to instrument your .NET Application
&nbsp;
&nbsp;
### Step 1: Install OpenTelemetry Dependencies
Install the following dependencies in your application.
```bash
dotnet add package OpenTelemetry
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Instrumentation.Runtime
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
dotnet add package OpenTelemetry.AutoInstrumentation
```
&nbsp;
### Step 2: Adding OpenTelemetry as a service and configuring exporter options
In your `Program.cs` file, add OpenTelemetry as a service. Here, we are configuring these variables:
`serviceName` - It is the name of your service.
`otlpOptions.Endpoint` - It is the endpoint for your OTel Collector agent.
&nbsp;
Heres a sample `Program.cs` file with the configured variables:
```bash
using System.Diagnostics;
using OpenTelemetry.Exporter;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
var builder = WebApplication.CreateBuilder(args);
// Configure OpenTelemetry with tracing and auto-start.
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource =>
resource.AddService(serviceName: "{{MYAPP}}"))
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddOtlpExporter(otlpOptions =>
{
otlpOptions.Endpoint = new Uri("http://localhost:4317");
otlpOptions.Protocol = OtlpExportProtocol.Grpc;
}));
var app = builder.Build();
//The index route ("/") is set up to write out the OpenTelemetry trace information on the response:
app.MapGet("/", () => $"Hello World! OpenTelemetry Trace: {Activity.Current?.Id}");
app.Run();
```
&nbsp;
The OpenTelemetry.Exporter.Options get or set the target to which the exporter is going to send traces. Here, were configuring it to send traces to the OTel Collector agent. The target must be a valid Uri with the scheme (http or https) and host and may contain a port and a path.
This is done by configuring an OpenTelemetry [TracerProvider](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/docs/trace/customizing-the-sdk#readme) using extension methods and setting it to auto-start when the host is started.

View File

@ -0,0 +1,18 @@
&nbsp;
Once you are done intrumenting your .NET application, you can run it using the below commands
&nbsp;
### Step 1: Run OTel Collector
Run this command inside the `otelcol-contrib` directory that you created in the install Otel Collector step
```bash
./otelcol-contrib --config ./config.yaml
```
&nbsp;
### Step 2: Run your .NET application
```bash
dotnet build
dotnet run
```

View File

@ -38,6 +38,7 @@ To configure your application to send data we will need a function to initialize
import ( import (
..... .....
"google.golang.org/grpc/credentials"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/attribute"

View File

@ -38,6 +38,7 @@ To configure your application to send data we will need a function to initialize
import ( import (
..... .....
"google.golang.org/grpc/credentials"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/attribute"

View File

@ -38,6 +38,7 @@ To configure your application to send data we will need a function to initialize
import ( import (
..... .....
"google.golang.org/grpc/credentials"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/attribute"

View File

@ -38,6 +38,7 @@ To configure your application to send data we will need a function to initialize
import ( import (
..... .....
"google.golang.org/grpc/credentials"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/attribute"

View File

@ -38,6 +38,7 @@ To configure your application to send data we will need a function to initialize
import ( import (
..... .....
"google.golang.org/grpc/credentials"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/attribute"

View File

@ -38,6 +38,7 @@ To configure your application to send data we will need a function to initialize
import ( import (
..... .....
"google.golang.org/grpc/credentials"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/attribute"

View File

@ -38,6 +38,7 @@ To configure your application to send data we will need a function to initialize
import ( import (
..... .....
"google.golang.org/grpc/credentials"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/attribute"

View File

@ -38,6 +38,7 @@ To configure your application to send data we will need a function to initialize
import ( import (
..... .....
"google.golang.org/grpc/credentials"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/attribute"

View File

@ -38,6 +38,7 @@ To configure your application to send data we will need a function to initialize
import ( import (
..... .....
"google.golang.org/grpc/credentials"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/attribute"

View File

@ -0,0 +1,39 @@
### Configure AWS
Create a `~/.aws/credentials` file in the machine which should have `aws_access_key_id` and the `aws_secret_access_key` in the default section of credentials file.
An example credential file would look like this:
```bash
[default]
aws_access_key_id=AKIAIOSFODNN7EXAMPLE
aws_secret_access_key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
[user1]
aws_access_key_id=AKIAI44QH8DHBEXAMPLE
aws_secret_access_key=je7MtGbClwBF/2Zp9Utk/h3yCo8nvbEXAMPLEKEY
```
**Note:** Replace the `aws_access_key_id`, `aws_secret_access_key`, `aws_access_key_id` and `aws_secret_access_key` with your credential values.
&nbsp;
The account corresponding to these credentials should have the **below-mentioned AWS Identity and Access Management (IAM)** policy. This allows describing and filtering log events across all log groups of that particular AWS account, a crucial step for forwarding CloudWatch logs to SigNoz.
```bash
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"logs:DescribeLogGroups",
"logs:FilterLogEvents"
],
"Resource": "arn:aws:logs:*:090340947446:log-group:*"
}
]
}
```
**Important Note:** Make sure you have AWS configured on the machine where otel-collector is running.

View File

@ -0,0 +1,50 @@
### Configure awscloudwatch receiver
Add the `awscloudwatch` receiver in the receivers section of `config.yaml` file of the **`otecol-contrib`** directory that you created in the Setup Otel Collector Step.
&nbsp;
You can configure your receiver to collect logs with different conditions.
&nbsp;
Here are two sample configurations:
- This configuration below will do autodiscovery and collect 100 log groups starting with prefix application.
```bash
receivers:
...
awscloudwatch:
region: us-east-1
logs:
poll_interval: 1m
groups:
autodiscover:
limit: 100
prefix: application
...
```
- This configuration below will not do autodiscovery and specifies the names of the log groups to collect.
```bash
receivers:
...
awscloudwatch:
profile: 'my-profile'
region: us-west-1
logs:
poll_interval: 5m
groups:
named:
/aws/eks/dev-0/cluster:
...
```
&nbsp;
To know more about the different parameters of awscloudwatch receiver, and see more sample configuration, checkout this [GitHub link](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/receiver/awscloudwatchreceiver)

View File

@ -0,0 +1,94 @@
### Setup OpenTelemetry Binary as an agent
### Step 1: Download otel-collector tar.gz
```bash
wget https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v0.79.0/otelcol-contrib_0.79.0_linux_amd64.tar.gz
```
### Step 2: Extract otel-collector tar.gz to the `otelcol-contrib` folder
```bash
mkdir otelcol-contrib && tar xvzf otelcol-contrib_0.79.0_linux_amd64.tar.gz -C otelcol-contrib
```
### Step 3: Create config.yaml in folder otelcol-contrib with the below content in it
```bash
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
hostmetrics:
collection_interval: 60s
scrapers:
cpu: {}
disk: {}
load: {}
filesystem: {}
memory: {}
network: {}
paging: {}
process:
mute_process_name_error: true
mute_process_exe_error: true
mute_process_io_error: true
processes: {}
prometheus:
config:
global:
scrape_interval: 60s
scrape_configs:
- job_name: otel-collector-binary
static_configs:
- targets:
# - localhost:8888
processors:
batch:
send_batch_size: 1000
timeout: 10s
# Ref: https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/processor/resourcedetectionprocessor/README.md
resourcedetection:
detectors: [env, system] # Before system detector, include ec2 for AWS, gcp for GCP and azure for Azure.
# Using OTEL_RESOURCE_ATTRIBUTES envvar, env detector adds custom labels.
timeout: 2s
system:
hostname_sources: [os] # alternatively, use [dns,os] for setting FQDN as host.name and os as fallback
extensions:
health_check: {}
zpages: {}
exporters:
otlp:
endpoint: "ingest.{{REGION}}.signoz.cloud:443"
tls:
insecure: false
headers:
"signoz-access-token": "{{SIGNOZ_INGESTION_KEY}}"
logging:
verbosity: normal
service:
telemetry:
metrics:
address: 0.0.0.0:8888
extensions: [health_check, zpages]
pipelines:
metrics:
receivers: [otlp]
processors: [batch]
exporters: [otlp]
metrics/internal:
receivers: [prometheus, hostmetrics]
processors: [resourcedetection, batch]
exporters: [otlp]
traces:
receivers: [otlp]
processors: [batch]
exporters: [otlp]
logs:
receivers: [otlp]
processors: [batch]
exporters: [otlp]
```

View File

@ -0,0 +1,38 @@
### Send logs to SigNoz
To test out the receiver, create a pipeline in the pipeline section of the `config.yaml` of the **`otecol-contrib`** directory that you created in the Setup Otel Collector Step.
```bash
...
pipelines:
metrics:
receivers: [otlp]
processors: [batch]
exporters: [otlp]
metrics/internal:
receivers: [prometheus, hostmetrics]
processors: [resourcedetection, batch]
exporters: [otlp]
traces:
receivers: [otlp]
processors: [batch]
exporters: [otlp]
logs:
receivers: [otlp, awscloudwatch]
processors: [batch]
exporters: [otlp]
```
&nbsp;
### Run OTel Collector
Run this command inside the `otelcol-contrib` directory :
```bash
./otelcol-contrib --config ./config.yaml
```
You should be able to see your Cloudwatch logs in the logs tabs of SigNoz Cloud UI.

View File

@ -0,0 +1,39 @@
### Configure AWS
Create a `~/.aws/credentials` file in the machine which should have `aws_access_key_id` and the `aws_secret_access_key` in the default section of credentials file.
An example credential file would look like this:
```bash
[default]
aws_access_key_id=AKIAIOSFODNN7EXAMPLE
aws_secret_access_key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
[user1]
aws_access_key_id=AKIAI44QH8DHBEXAMPLE
aws_secret_access_key=je7MtGbClwBF/2Zp9Utk/h3yCo8nvbEXAMPLEKEY
```
**Note:** Replace the `aws_access_key_id`, `aws_secret_access_key`, `aws_access_key_id` and `aws_secret_access_key` with your credential values.
&nbsp;
The account corresponding to these credentials should have the **below-mentioned AWS Identity and Access Management (IAM)** policy. This allows describing and filtering log events across all log groups of that particular AWS account, a crucial step for forwarding CloudWatch logs to SigNoz.
```bash
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"logs:DescribeLogGroups",
"logs:FilterLogEvents"
],
"Resource": "arn:aws:logs:*:090340947446:log-group:*"
}
]
}
```
**Important Note:** Make sure you have AWS configured on the machine where otel-collector is running.

View File

@ -0,0 +1,50 @@
### Configure awscloudwatch receiver
Add the `awscloudwatch` receiver in the receivers section of `config.yaml` file of the **`otecol-contrib`** directory that you created in the Setup Otel Collector Step.
&nbsp;
You can configure your receiver to collect logs with different conditions.
&nbsp;
Here are two sample configurations:
- This configuration below will do autodiscovery and collect 100 log groups starting with prefix application.
```bash
receivers:
...
awscloudwatch:
region: us-east-1
logs:
poll_interval: 1m
groups:
autodiscover:
limit: 100
prefix: application
...
```
- This configuration below will not do autodiscovery and specifies the names of the log groups to collect.
```bash
receivers:
...
awscloudwatch:
profile: 'my-profile'
region: us-west-1
logs:
poll_interval: 5m
groups:
named:
/aws/eks/dev-0/cluster:
...
```
&nbsp;
To know more about the different parameters of awscloudwatch receiver, and see more sample configuration, checkout this [GitHub link](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/receiver/awscloudwatchreceiver)

View File

@ -0,0 +1,95 @@
### Setup OpenTelemetry Binary as an agent
### Step 1: Download otel-collector tar.gz
```bash
wget https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v0.79.0/otelcol-contrib_0.79.0_linux_arm64.tar.gz
```
### Step 2: Extract otel-collector tar.gz to the `otelcol-contrib` folder
```bash
mkdir otelcol-contrib && tar xvzf otelcol-contrib_0.79.0_linux_arm64.tar.gz -C otelcol-contrib
```
### Step 3: Create config.yaml in folder otelcol-contrib with the below content in it
```bash
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
hostmetrics:
collection_interval: 60s
scrapers:
cpu: {}
disk: {}
load: {}
filesystem: {}
memory: {}
network: {}
paging: {}
process:
mute_process_name_error: true
mute_process_exe_error: true
mute_process_io_error: true
processes: {}
prometheus:
config:
global:
scrape_interval: 60s
scrape_configs:
- job_name: otel-collector-binary
static_configs:
- targets:
# - localhost:8888
processors:
batch:
send_batch_size: 1000
timeout: 10s
# Ref: https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/processor/resourcedetectionprocessor/README.md
resourcedetection:
detectors: [env, system] # Before system detector, include ec2 for AWS, gcp for GCP and azure for Azure.
# Using OTEL_RESOURCE_ATTRIBUTES envvar, env detector adds custom labels.
timeout: 2s
system:
hostname_sources: [os] # alternatively, use [dns,os] for setting FQDN as host.name and os as fallback
extensions:
health_check: {}
zpages: {}
exporters:
otlp:
endpoint: "ingest.{{REGION}}.signoz.cloud:443"
tls:
insecure: false
headers:
"signoz-access-token": "{{SIGNOZ_INGESTION_KEY}}"
logging:
verbosity: normal
service:
telemetry:
metrics:
address: 0.0.0.0:8888
extensions: [health_check, zpages]
pipelines:
metrics:
receivers: [otlp]
processors: [batch]
exporters: [otlp]
metrics/internal:
receivers: [prometheus, hostmetrics]
processors: [resourcedetection, batch]
exporters: [otlp]
traces:
receivers: [otlp]
processors: [batch]
exporters: [otlp]
logs:
receivers: [otlp]
processors: [batch]
exporters: [otlp]
```

Some files were not shown because too many files have changed in this diff Show More