Compare commits
4 Commits
v0.88.0
...
fix/route-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aaaad8e0a2 | ||
|
|
5102cf2b7b | ||
|
|
9ec5594648 | ||
|
|
b6c2ebd6d7 |
@@ -74,13 +74,10 @@ exporters:
|
||||
dsn: tcp://clickhouse:9000/signoz_logs
|
||||
timeout: 10s
|
||||
use_new_schema: true
|
||||
# debug: {}
|
||||
service:
|
||||
telemetry:
|
||||
logs:
|
||||
encoding: json
|
||||
metrics:
|
||||
address: 0.0.0.0:8888
|
||||
extensions:
|
||||
- health_check
|
||||
- pprof
|
||||
|
||||
@@ -74,13 +74,10 @@ exporters:
|
||||
dsn: tcp://clickhouse:9000/signoz_logs
|
||||
timeout: 10s
|
||||
use_new_schema: true
|
||||
# debug: {}
|
||||
service:
|
||||
telemetry:
|
||||
logs:
|
||||
encoding: json
|
||||
metrics:
|
||||
address: 0.0.0.0:8888
|
||||
extensions:
|
||||
- health_check
|
||||
- pprof
|
||||
|
||||
@@ -29,13 +29,19 @@ function RouteTab({
|
||||
|
||||
// Find the matching route for the current pathname
|
||||
const currentRoute = routesWithParams.find((route) => {
|
||||
const pathnameOnly = route.route.split('?')[0];
|
||||
const routePattern = escapeRegExp(pathnameOnly).replace(
|
||||
/\\:([a-zA-Z0-9_]+)/g,
|
||||
'([^/]+)',
|
||||
);
|
||||
const regex = new RegExp(`^${routePattern}$`);
|
||||
return regex.test(location.pathname);
|
||||
try {
|
||||
const routePattern = route.route.replace(/:(\w+)/g, '([^/]+)');
|
||||
const regex = new RegExp(`^${routePattern}$`);
|
||||
return regex.test(location.pathname);
|
||||
} catch (error) {
|
||||
const pathnameOnly = route.route.split('?')[0];
|
||||
const routePattern = escapeRegExp(pathnameOnly).replace(
|
||||
/\\:([a-zA-Z0-9_]+)/g,
|
||||
'([^/]+)',
|
||||
);
|
||||
const regex = new RegExp(`^${routePattern}$`);
|
||||
return regex.test(location.pathname);
|
||||
}
|
||||
});
|
||||
|
||||
const onChange = (activeRoute: string): void => {
|
||||
|
||||
@@ -1,30 +1,173 @@
|
||||
.empty-logs-search-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 240px;
|
||||
|
||||
.empty-logs-search-container-content {
|
||||
.empty-logs-search {
|
||||
&__container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 240px;
|
||||
}
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
color: var(--text-vanilla-400);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 128.571% */
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.07px;
|
||||
|
||||
align-items: flex-start;
|
||||
.empty-state-svg {
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
}
|
||||
}
|
||||
&__sub-text {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.sub-text {
|
||||
font-weight: 600;
|
||||
&__container {
|
||||
&--custom-message {
|
||||
height: 445px;
|
||||
.empty-state-svg {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
}
|
||||
.empty-logs-search {
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
color: var(--bg-vanilla-100);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
&__description {
|
||||
font-size: 14px;
|
||||
color: var(--text-vanilla-400);
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
&__description-list {
|
||||
margin: 0;
|
||||
margin-top: 8px;
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
font-family: Inter;
|
||||
}
|
||||
|
||||
&__description-list li {
|
||||
position: relative;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
&__description-list li::before {
|
||||
content: '⎯';
|
||||
font-family: Inter;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: var(--bg-robin-400);
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
&__clear-filters-btn {
|
||||
display: flex;
|
||||
width: 468px;
|
||||
font-family: Inter;
|
||||
padding: 12px;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
border-radius: 3px;
|
||||
border: 1px dashed var(--bg-slate-500);
|
||||
background: transparent;
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.07px;
|
||||
cursor: pointer;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
&__clear-filters-btn-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
&__row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-end;
|
||||
max-width: 825px;
|
||||
gap: 25px;
|
||||
justify-content: center;
|
||||
margin-left: 21px;
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 260px;
|
||||
}
|
||||
|
||||
&__resources-card {
|
||||
background: var(--bg-ink-400);
|
||||
border: 1px solid var(--bg-slate-500);
|
||||
border-radius: 4px;
|
||||
width: 332px;
|
||||
}
|
||||
|
||||
&__resources-title {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
line-height: 18px;
|
||||
letter-spacing: 0.88px;
|
||||
text-transform: uppercase;
|
||||
padding: 16px 16px 12px;
|
||||
border-bottom: 1px solid var(--bg-slate-500);
|
||||
height: 46px;
|
||||
}
|
||||
|
||||
&__resources-links {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.learn-more {
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,24 @@ import './EmptyLogsSearch.styles.scss';
|
||||
|
||||
import { Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import cx from 'classnames';
|
||||
import LearnMore from 'components/LearnMore/LearnMore';
|
||||
import { EmptyLogsListConfig } from 'container/LogsExplorerList/utils';
|
||||
import { Delete } from 'lucide-react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { DataSource, PanelTypeKeys } from 'types/common/queryBuilder';
|
||||
|
||||
interface EmptyLogsSearchProps {
|
||||
dataSource: DataSource;
|
||||
panelType: PanelTypeKeys;
|
||||
customMessage?: EmptyLogsListConfig;
|
||||
}
|
||||
|
||||
export default function EmptyLogsSearch({
|
||||
dataSource,
|
||||
panelType,
|
||||
}: {
|
||||
dataSource: DataSource;
|
||||
panelType: PanelTypeKeys;
|
||||
}): JSX.Element {
|
||||
customMessage,
|
||||
}: EmptyLogsSearchProps): JSX.Element {
|
||||
const logEventCalledRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (!logEventCalledRef.current) {
|
||||
@@ -30,18 +38,80 @@ export default function EmptyLogsSearch({
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="empty-logs-search-container">
|
||||
<div className="empty-logs-search-container-content">
|
||||
<img
|
||||
src="/Icons/emptyState.svg"
|
||||
alt="thinking-emoji"
|
||||
className="empty-state-svg"
|
||||
/>
|
||||
<Typography.Text>
|
||||
<span className="sub-text">This query had no results. </span>
|
||||
Edit your query and try again!
|
||||
</Typography.Text>
|
||||
<div
|
||||
className={cx('empty-logs-search__container', {
|
||||
'empty-logs-search__container--custom-message': !!customMessage,
|
||||
})}
|
||||
>
|
||||
<div className="empty-logs-search__row">
|
||||
<div className="empty-logs-search__content">
|
||||
<img
|
||||
src="/Icons/emptyState.svg"
|
||||
alt="thinking-emoji"
|
||||
className="empty-state-svg"
|
||||
/>
|
||||
{customMessage ? (
|
||||
<>
|
||||
<div className="empty-logs-search__header">
|
||||
<Typography.Text className="empty-logs-search__title">
|
||||
{customMessage.title}
|
||||
</Typography.Text>
|
||||
{customMessage.subTitle && (
|
||||
<Typography.Text className="empty-logs-search__subtitle">
|
||||
{customMessage.subTitle}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
{Array.isArray(customMessage.description) ? (
|
||||
<ul className="empty-logs-search__description-list">
|
||||
{customMessage.description.map((desc) => (
|
||||
<li key={desc}>{desc}</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<Typography.Text className="empty-logs-search__description">
|
||||
{customMessage.description}
|
||||
</Typography.Text>
|
||||
)}
|
||||
{/* Clear filters button */}
|
||||
{customMessage.showClearFiltersButton && (
|
||||
<button
|
||||
type="button"
|
||||
className="empty-logs-search__clear-filters-btn"
|
||||
onClick={customMessage.onClearFilters}
|
||||
>
|
||||
{customMessage.clearFiltersButtonText}
|
||||
<span className="empty-logs-search__clear-filters-btn-icon">
|
||||
<Delete size={14} />
|
||||
Clear filters
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Typography.Text>
|
||||
<span className="empty-logs-search__sub-text">
|
||||
This query had no results.{' '}
|
||||
</span>
|
||||
Edit your query and try again!
|
||||
</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
{customMessage?.documentationLinks && (
|
||||
<div className="empty-logs-search__resources-card">
|
||||
<div className="empty-logs-search__resources-title">RESOURCES</div>
|
||||
<div className="empty-logs-search__resources-links">
|
||||
{customMessage.documentationLinks.map((link) => (
|
||||
<LearnMore key={link.text} text={link.text} url={link.url} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
EmptyLogsSearch.defaultProps = {
|
||||
customMessage: null,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import LogsExplorerViews from 'container/LogsExplorerViews';
|
||||
import { mockQueryBuilderContextValue } from 'container/LogsExplorerViews/tests/mock';
|
||||
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
|
||||
import { logsQueryRangeEmptyResponse } from 'mocks-server/__mockdata__/logs_query_range';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
|
||||
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
import { QueryBuilderContext } from 'providers/QueryBuilder';
|
||||
import { render, screen } from 'tests/test-utils';
|
||||
|
||||
const queryRangeURL = 'http://localhost/api/v3/query_range';
|
||||
|
||||
const logsQueryServerRequest = ({
|
||||
response = logsQueryRangeEmptyResponse,
|
||||
}: {
|
||||
response?: any;
|
||||
}): void =>
|
||||
server.use(
|
||||
rest.post(queryRangeURL, (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(response)),
|
||||
),
|
||||
);
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: (): { pathname: string } => ({
|
||||
pathname: `${ROUTES.LOGS_EXPLORER}`,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('providers/preferences/sync/usePreferenceSync', () => ({
|
||||
usePreferenceSync: (): any => ({
|
||||
preferences: {
|
||||
columns: [],
|
||||
formatting: {
|
||||
maxLines: 2,
|
||||
format: 'table',
|
||||
fontSize: 'small',
|
||||
version: 1,
|
||||
},
|
||||
},
|
||||
loading: false,
|
||||
error: null,
|
||||
updateColumns: jest.fn(),
|
||||
updateFormatting: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock(
|
||||
'container/TimeSeriesView/TimeSeriesView',
|
||||
() =>
|
||||
// eslint-disable-next-line func-names, @typescript-eslint/explicit-function-return-type, react/display-name
|
||||
function () {
|
||||
return <div>Time Series Chart</div>;
|
||||
},
|
||||
);
|
||||
|
||||
jest.mock(
|
||||
'container/LogsExplorerChart',
|
||||
() =>
|
||||
// eslint-disable-next-line func-names, @typescript-eslint/explicit-function-return-type, react/display-name
|
||||
function () {
|
||||
return <div>Histogram Chart</div>;
|
||||
},
|
||||
);
|
||||
|
||||
jest.mock('hooks/useSafeNavigate', () => ({
|
||||
useSafeNavigate: (): any => ({
|
||||
safeNavigate: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('uplot', () => {
|
||||
const paths = {
|
||||
spline: jest.fn(),
|
||||
bars: jest.fn(),
|
||||
};
|
||||
const uplotMock = jest.fn(() => ({
|
||||
paths,
|
||||
}));
|
||||
return {
|
||||
paths,
|
||||
default: uplotMock,
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('hooks/queryBuilder/useGetExplorerQueryRange', () => ({
|
||||
__esModule: true,
|
||||
useGetExplorerQueryRange: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('LogsExplorerList - empty states', () => {
|
||||
beforeEach(() => {
|
||||
(useGetExplorerQueryRange as jest.Mock).mockReturnValue({
|
||||
data: { payload: logsQueryRangeEmptyResponse },
|
||||
});
|
||||
logsQueryServerRequest({});
|
||||
});
|
||||
|
||||
it('should display custom empty state when navigating from trace to logs with no results', async () => {
|
||||
const mockTraceToLogsContextValue = {
|
||||
...mockQueryBuilderContextValue,
|
||||
panelType: PANEL_TYPES.LIST,
|
||||
stagedQuery: {
|
||||
...mockQueryBuilderContextValue.stagedQuery,
|
||||
builder: {
|
||||
...mockQueryBuilderContextValue.stagedQuery.builder,
|
||||
queryData: [
|
||||
{
|
||||
...mockQueryBuilderContextValue.stagedQuery.builder.queryData[0],
|
||||
filters: {
|
||||
items: [
|
||||
{
|
||||
id: 'trace-filter',
|
||||
key: {
|
||||
key: 'trace_id',
|
||||
type: '',
|
||||
dataType: 'string',
|
||||
isColumn: true,
|
||||
},
|
||||
op: '=',
|
||||
value: 'test-trace-id',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
render(
|
||||
<QueryBuilderContext.Provider value={mockTraceToLogsContextValue as any}>
|
||||
<PreferenceContextProvider>
|
||||
<LogsExplorerViews
|
||||
selectedView={SELECTED_VIEWS.SEARCH}
|
||||
showFrequencyChart
|
||||
setIsLoadingQueries={(): void => {}}
|
||||
listQueryKeyRef={{ current: {} }}
|
||||
chartQueryKeyRef={{ current: {} }}
|
||||
/>
|
||||
</PreferenceContextProvider>
|
||||
</QueryBuilderContext.Provider>,
|
||||
);
|
||||
|
||||
// Check for custom empty state message
|
||||
expect(screen.getByText('No logs found for this trace.')).toBeInTheDocument();
|
||||
expect(screen.getByText('This could be because :')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('Logs are not linked to Traces.'),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('Logs are not being sent to SigNoz.'),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('No logs are associated with this particular trace/span.'),
|
||||
).toBeInTheDocument();
|
||||
|
||||
// Check for documentation links
|
||||
expect(screen.getByText('Sending logs to SigNoz')).toBeInTheDocument();
|
||||
expect(screen.getByText('Correlate traces and logs')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display empty state when filters are applied and no results are found', async () => {
|
||||
const mockTraceToLogsContextValue = {
|
||||
...mockQueryBuilderContextValue,
|
||||
panelType: PANEL_TYPES.LIST,
|
||||
stagedQuery: {
|
||||
...mockQueryBuilderContextValue.stagedQuery,
|
||||
builder: {
|
||||
...mockQueryBuilderContextValue.stagedQuery.builder,
|
||||
queryData: [
|
||||
{
|
||||
...mockQueryBuilderContextValue.stagedQuery.builder.queryData[0],
|
||||
filters: {
|
||||
items: [
|
||||
{
|
||||
id: 'service-filter',
|
||||
key: {
|
||||
key: 'service.name',
|
||||
type: '',
|
||||
dataType: 'string',
|
||||
isColumn: true,
|
||||
},
|
||||
op: '=',
|
||||
value: 'test-service-name',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
render(
|
||||
<QueryBuilderContext.Provider value={mockTraceToLogsContextValue as any}>
|
||||
<PreferenceContextProvider>
|
||||
<LogsExplorerViews
|
||||
selectedView={SELECTED_VIEWS.SEARCH}
|
||||
showFrequencyChart
|
||||
setIsLoadingQueries={(): void => {}}
|
||||
listQueryKeyRef={{ current: {} }}
|
||||
chartQueryKeyRef={{ current: {} }}
|
||||
/>
|
||||
</PreferenceContextProvider>
|
||||
</QueryBuilderContext.Provider>,
|
||||
);
|
||||
|
||||
// Check for custom empty state message
|
||||
expect(screen.getByText(/This query had no results./i)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(/Edit your query and try again!/i),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -29,7 +29,11 @@ import NoLogs from '../NoLogs/NoLogs';
|
||||
import InfinityTableView from './InfinityTableView';
|
||||
import { LogsExplorerListProps } from './LogsExplorerList.interfaces';
|
||||
import { InfinityWrapperStyled } from './styles';
|
||||
import { convertKeysToColumnFields } from './utils';
|
||||
import {
|
||||
convertKeysToColumnFields,
|
||||
getEmptyLogsListConfig,
|
||||
isTraceToLogsQuery,
|
||||
} from './utils';
|
||||
|
||||
function Footer(): JSX.Element {
|
||||
return <Spinner height={20} tip="Getting Logs" />;
|
||||
@@ -63,6 +67,12 @@ function LogsExplorerList({
|
||||
currentStagedQueryData?.aggregateOperator || StringOperators.NOOP,
|
||||
});
|
||||
|
||||
const {
|
||||
currentQuery,
|
||||
lastUsedQuery,
|
||||
redirectWithQueryBuilderData,
|
||||
} = useQueryBuilder();
|
||||
|
||||
const activeLogIndex = useMemo(
|
||||
() => logs.findIndex(({ id }) => id === activeLogId),
|
||||
[logs, activeLogId],
|
||||
@@ -186,6 +196,44 @@ function LogsExplorerList({
|
||||
selectedFields,
|
||||
]);
|
||||
|
||||
const isTraceToLogsNavigation = useMemo(() => {
|
||||
if (!currentStagedQueryData) return false;
|
||||
return isTraceToLogsQuery(currentStagedQueryData);
|
||||
}, [currentStagedQueryData]);
|
||||
|
||||
const handleClearFilters = useCallback((): void => {
|
||||
const queryIndex = lastUsedQuery ?? 0;
|
||||
const updatedQuery = currentQuery?.builder.queryData?.[queryIndex];
|
||||
|
||||
if (!updatedQuery) return;
|
||||
|
||||
if (updatedQuery?.filters?.items) {
|
||||
updatedQuery.filters.items = [];
|
||||
}
|
||||
|
||||
const preparedQuery = {
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: currentQuery.builder.queryData.map((item, idx: number) => ({
|
||||
...item,
|
||||
filters: {
|
||||
...item.filters,
|
||||
items: idx === queryIndex ? [] : [...(item.filters?.items || [])],
|
||||
},
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
||||
redirectWithQueryBuilderData(preparedQuery);
|
||||
}, [currentQuery, lastUsedQuery, redirectWithQueryBuilderData]);
|
||||
|
||||
const getEmptyStateMessage = useMemo(() => {
|
||||
if (!isTraceToLogsNavigation) return;
|
||||
|
||||
return getEmptyLogsListConfig(handleClearFilters);
|
||||
}, [isTraceToLogsNavigation, handleClearFilters]);
|
||||
|
||||
return (
|
||||
<div className="logs-list-view-container">
|
||||
{(isLoading || (isFetching && logs.length === 0)) && <LogsLoading />}
|
||||
@@ -201,7 +249,11 @@ function LogsExplorerList({
|
||||
logs.length === 0 &&
|
||||
!isError &&
|
||||
isFilterApplied && (
|
||||
<EmptyLogsSearch dataSource={DataSource.LOGS} panelType="LIST" />
|
||||
<EmptyLogsSearch
|
||||
dataSource={DataSource.LOGS}
|
||||
panelType="LIST"
|
||||
customMessage={getEmptyStateMessage}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isError && !isLoading && !isFetching && <LogsError />}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { IField } from 'types/api/logs/fields';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
TagFilterItem,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
export const convertKeysToColumnFields = (
|
||||
keys: BaseAutocompleteData[],
|
||||
@@ -9,3 +13,56 @@ export const convertKeysToColumnFields = (
|
||||
name: item.key,
|
||||
type: item.type as string,
|
||||
}));
|
||||
/**
|
||||
* Determines if a query represents a trace-to-logs navigation
|
||||
* by checking for the presence of a trace_id filter.
|
||||
*/
|
||||
export const isTraceToLogsQuery = (queryData: IBuilderQuery): boolean => {
|
||||
// Check if this is a trace-to-logs query by looking for trace_id filter
|
||||
if (!queryData?.filters?.items) return false;
|
||||
|
||||
const traceIdFilter = queryData.filters.items.find(
|
||||
(item: TagFilterItem) => item.key?.key === 'trace_id',
|
||||
);
|
||||
|
||||
return !!traceIdFilter;
|
||||
};
|
||||
|
||||
export type EmptyLogsListConfig = {
|
||||
title: string;
|
||||
subTitle: string;
|
||||
description: string | string[];
|
||||
documentationLinks?: Array<{
|
||||
text: string;
|
||||
url: string;
|
||||
}>;
|
||||
showClearFiltersButton?: boolean;
|
||||
onClearFilters?: () => void;
|
||||
clearFiltersButtonText?: string;
|
||||
};
|
||||
|
||||
export const getEmptyLogsListConfig = (
|
||||
handleClearFilters: () => void,
|
||||
): EmptyLogsListConfig => ({
|
||||
title: 'No logs found for this trace.',
|
||||
subTitle: 'This could be because :',
|
||||
description: [
|
||||
'Logs are not linked to Traces.',
|
||||
'Logs are not being sent to SigNoz.',
|
||||
'No logs are associated with this particular trace/span.',
|
||||
],
|
||||
documentationLinks: [
|
||||
{
|
||||
text: 'Sending logs to SigNoz',
|
||||
url: 'https://signoz.io/docs/logs-management/send-logs-to-signoz/',
|
||||
},
|
||||
{
|
||||
text: 'Correlate traces and logs',
|
||||
url:
|
||||
'https://signoz.io/docs/traces-management/guides/correlate-traces-and-logs/',
|
||||
},
|
||||
],
|
||||
clearFiltersButtonText: 'Clear filters from Trace to view other logs',
|
||||
showClearFiltersButton: true,
|
||||
onClearFilters: handleClearFilters,
|
||||
});
|
||||
|
||||
@@ -551,19 +551,19 @@ function LogsExplorerViews({
|
||||
if (!stagedQuery) return [];
|
||||
|
||||
if (panelType === PANEL_TYPES.LIST) {
|
||||
if (listChartData && listChartData.payload.data.result.length > 0) {
|
||||
if (listChartData && listChartData.payload.data?.result.length > 0) {
|
||||
return listChartData.payload.data.result;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!data || data.payload.data.result.length === 0) return [];
|
||||
if (!data || data.payload.data?.result.length === 0) return [];
|
||||
|
||||
const isGroupByExist = stagedQuery.builder.queryData.some(
|
||||
(queryData) => queryData.groupBy.length > 0,
|
||||
);
|
||||
|
||||
const firstPayloadQuery = data.payload.data.result.find(
|
||||
const firstPayloadQuery = data.payload.data?.result.find(
|
||||
(item) => item.queryName === listQuery?.queryName,
|
||||
);
|
||||
|
||||
|
||||
@@ -46,6 +46,14 @@ export const logsQueryRangeSuccessResponse = {
|
||||
],
|
||||
},
|
||||
};
|
||||
export const logsQueryRangeEmptyResponse = {
|
||||
resultType: '',
|
||||
result: [
|
||||
{
|
||||
queryName: 'A',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const logsPaginationQueryRangeSuccessResponse = ({
|
||||
offset = 0,
|
||||
|
||||
@@ -4521,24 +4521,27 @@ func (aH *APIHandler) sendQueryResultEvents(r *http.Request, result []*v3.Result
|
||||
}
|
||||
|
||||
queryInfoResult := NewQueryInfoResult(queryRangeParams, version)
|
||||
if !(len(result) > 0 && (len(result[0].Series) > 0 || len(result[0].List) > 0)) {
|
||||
aH.Signoz.Analytics.TrackUser(r.Context(), claims.OrgID, claims.UserID, "Telemetry Query Returned Empty", queryInfoResult.ToMap())
|
||||
return
|
||||
}
|
||||
|
||||
aH.Signoz.Analytics.TrackUser(r.Context(), claims.OrgID, claims.UserID, "Telemetry Query Returned Results", queryInfoResult.ToMap())
|
||||
|
||||
if !(queryInfoResult.LogsUsed || queryInfoResult.MetricsUsed || queryInfoResult.TracesUsed) {
|
||||
return
|
||||
}
|
||||
|
||||
referrer := r.Header.Get("Referer")
|
||||
if referrer == "" {
|
||||
properties := queryInfoResult.ToMap()
|
||||
if !(len(result) > 0 && (len(result[0].Series) > 0 || len(result[0].List) > 0 || len(result[0].Table.Rows) > 0)) {
|
||||
aH.Signoz.Analytics.TrackUser(r.Context(), claims.OrgID, claims.UserID, "Telemetry Query Returned Empty", properties)
|
||||
return
|
||||
}
|
||||
|
||||
referrer := r.Header.Get("Referer")
|
||||
|
||||
if referrer == "" {
|
||||
aH.Signoz.Analytics.TrackUser(r.Context(), claims.OrgID, claims.UserID, "Telemetry Query Returned Results", properties)
|
||||
return
|
||||
}
|
||||
|
||||
properties["referrer"] = referrer
|
||||
|
||||
if matched, _ := regexp.MatchString(`/dashboard/[a-zA-Z0-9\-]+/(new|edit)(?:\?.*)?$`, referrer); matched {
|
||||
properties := queryInfoResult.ToMap()
|
||||
|
||||
if dashboardIDRegex, err := regexp.Compile(`/dashboard/([a-f0-9\-]+)/`); err == nil {
|
||||
if matches := dashboardIDRegex.FindStringSubmatch(referrer); len(matches) > 1 {
|
||||
@@ -4552,13 +4555,12 @@ func (aH *APIHandler) sendQueryResultEvents(r *http.Request, result []*v3.Result
|
||||
}
|
||||
}
|
||||
|
||||
properties["referrer"] = referrer
|
||||
properties["module_name"] = "dashboard"
|
||||
aH.Signoz.Analytics.TrackUser(r.Context(), claims.OrgID, claims.UserID, "Telemetry Query Returned Results", properties)
|
||||
return
|
||||
}
|
||||
|
||||
if matched, _ := regexp.MatchString(`/alerts/(new|edit)(?:\?.*)?$`, referrer); matched {
|
||||
properties := queryInfoResult.ToMap()
|
||||
|
||||
if alertIDRegex, err := regexp.Compile(`ruleId=(\d+)`); err == nil {
|
||||
if matches := alertIDRegex.FindStringSubmatch(referrer); len(matches) > 1 {
|
||||
@@ -4566,11 +4568,13 @@ func (aH *APIHandler) sendQueryResultEvents(r *http.Request, result []*v3.Result
|
||||
}
|
||||
}
|
||||
|
||||
properties["referrer"] = referrer
|
||||
properties["module_name"] = "rule"
|
||||
aH.Signoz.Analytics.TrackUser(r.Context(), claims.OrgID, claims.UserID, "Telemetry Query Returned Results", properties)
|
||||
return
|
||||
}
|
||||
|
||||
aH.Signoz.Analytics.TrackUser(r.Context(), claims.OrgID, claims.UserID, "Telemetry Query Returned Results", properties)
|
||||
|
||||
}
|
||||
|
||||
func (aH *APIHandler) QueryRangeV3(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
Reference in New Issue
Block a user