Merge branch 'develop' into SIG-5729

This commit is contained in:
rahulkeswani101 2024-09-08 22:01:48 +05:30 committed by GitHub
commit 71e24483dd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 1563 additions and 124 deletions

View File

@ -0,0 +1,22 @@
{
"trialPlanExpired": "Trial Plan Expired",
"gotQuestions": "Got Questions?",
"contactUs": "Contact Us",
"upgradeToContinue": "Upgrade to Continue",
"upgradeNow": "Upgrade now to keep enjoying all the great features youve been using.",
"yourDataIsSafe": "Your data is safe with us until",
"actNow": "Act now to avoid any disruptions and continue where you left off.",
"contactAdmin": "Contact your admin to proceed with the upgrade.",
"continueMyJourney": "Continue My Journey",
"needMoreTime": "Need More Time?",
"extendTrial": "Extend Trial",
"extendTrialMsgPart1": "If you have a specific reason why you were not able to finish your PoC in the trial period, please write to us on",
"extendTrialMsgPart2": "with the reason. Sometimes we can extend trial by a few days on a case by case basis",
"whyChooseSignoz": "Why choose Signoz",
"enterpriseGradeObservability": "Enterprise-grade Observability",
"observabilityDescription": "Get access to observability at any scale with advanced security and compliance.",
"continueToUpgrade": "Continue to Upgrade",
"youAreInGoodCompany": "You are in good company",
"faqs": "FAQs",
"somethingWentWrong": "Something went wrong"
}

View File

@ -0,0 +1,22 @@
{
"trialPlanExpired": "Trial Plan Expired",
"gotQuestions": "Got Questions?",
"contactUs": "Contact Us",
"upgradeToContinue": "Upgrade to Continue",
"upgradeNow": "Upgrade now to keep enjoying all the great features youve been using.",
"yourDataIsSafe": "Your data is safe with us until",
"actNow": "Act now to avoid any disruptions and continue where you left off.",
"contactAdmin": "Contact your admin to proceed with the upgrade.",
"continueMyJourney": "Continue My Journey",
"needMoreTime": "Need More Time?",
"extendTrial": "Extend Trial",
"extendTrialMsgPart1": "If you have a specific reason why you were not able to finish your PoC in the trial period, please write to us on",
"extendTrialMsgPart2": "with the reason. Sometimes we can extend trial by a few days on a case by case basis",
"whyChooseSignoz": "Why choose Signoz",
"enterpriseGradeObservability": "Enterprise-grade Observability",
"observabilityDescription": "Get access to observability at any scale with advanced security and compliance.",
"continueToUpgrade": "Continue to Upgrade",
"youAreInGoodCompany": "You are in good company",
"faqs": "FAQs",
"somethingWentWrong": "Something went wrong"
}

View File

@ -1,3 +1,21 @@
.alert-popover {
.alert-popover-trigger-action {
cursor: pointer;
}
.alert-history-popover {
.ant-popover-inner {
border: 1px solid var(--bg-slate-400);
.lightMode & {
background: var(--bg-vanilla-100) !important;
border: 1px solid var(--bg-vanilla-300);
}
}
.ant-popover-arrow {
&::before {
.lightMode & {
background: var(--bg-vanilla-100);
}
}
}
}

View File

@ -64,12 +64,13 @@ function AlertPopover({
relatedLogsLink,
}: Props): JSX.Element {
return (
<div className="alert-popover">
<div className="alert-popover-trigger-action">
<Popover
showArrow={false}
placement="bottom"
color="linear-gradient(139deg, rgba(18, 19, 23, 1) 0%, rgba(18, 19, 23, 1) 98.68%)"
destroyTooltipOnHide
rootClassName="alert-history-popover"
content={
<PopoverContent
relatedTracesLink={relatedTracesLink}

View File

@ -120,7 +120,6 @@
.contributor-row-popover-buttons {
display: flex;
flex-direction: column;
border: 1px solid var(--bg-slate-400);
&__button {
display: flex;
@ -133,13 +132,36 @@
width: 160px;
cursor: pointer;
.text,
.icon {
color: var(--text-vanilla-100);
.lightMode & {
color: var(--text-ink-500);
}
}
&:hover {
background: var(--bg-slate-400);
.text,
.icon {
color: var(--text-vanilla-100);
.lightMode & {
color: var(--text-ink-500);
}
}
}
.icon {
display: flex;
}
.lightMode & {
background: var(--bg-vanilla-100);
border-color: var(--bg-vanilla-400);
}
}
}

View File

@ -19,20 +19,9 @@
font-size: 12px;
font-weight: 500;
padding: 12px 16px 8px !important;
&:last-of-type
// TODO(shaheer): uncomment when we display value column
// ,
// &:nth-last-of-type(2)
{
text-align: right;
}
}
&-tbody > tr > td {
border: none;
&:last-of-type,
&:nth-last-of-type(2) {
text-align: right;
}
}
}
@ -52,7 +41,7 @@
}
.alert-rule {
&-value,
&-created-at {
&__created-at {
font-size: 14px;
color: var(--text-vanilla-400);
}
@ -60,7 +49,7 @@
font-weight: 500;
line-height: 20px;
}
&-created-at {
&__created-at {
line-height: 18px;
letter-spacing: -0.07px;
}

View File

@ -1,3 +1,5 @@
import { EllipsisOutlined } from '@ant-design/icons';
import { Button } from 'antd';
import { ColumnsType } from 'antd/es/table';
import { ConditionalAlertPopover } from 'container/AlertHistory/AlertPopover/AlertPopover';
import AlertLabels from 'pages/AlertDetails/AlertHeader/AlertLabels/AlertLabels';
@ -10,43 +12,42 @@ export const timelineTableColumns = (): ColumnsType<AlertRuleTimelineTableRespon
title: 'STATE',
dataIndex: 'state',
sorter: true,
width: '12.5%',
render: (value, record): JSX.Element => (
<ConditionalAlertPopover
relatedTracesLink={record.relatedTracesLink}
relatedLogsLink={record.relatedLogsLink}
>
<div className="alert-rule-state">
<AlertState state={value} showLabel />
</div>
</ConditionalAlertPopover>
width: 140,
render: (value): JSX.Element => (
<div className="alert-rule-state">
<AlertState state={value} showLabel />
</div>
),
},
{
title: 'LABELS',
dataIndex: 'labels',
width: '54.5%',
render: (labels, record): JSX.Element => (
<ConditionalAlertPopover
relatedTracesLink={record.relatedTracesLink}
relatedLogsLink={record.relatedLogsLink}
>
<div className="alert-rule-labels">
<AlertLabels labels={labels} />
</div>
</ConditionalAlertPopover>
render: (labels): JSX.Element => (
<div className="alert-rule-labels">
<AlertLabels labels={labels} />
</div>
),
},
{
title: 'CREATED AT',
dataIndex: 'unixMilli',
width: '32.5%',
render: (value, record): JSX.Element => (
width: 200,
render: (value): JSX.Element => (
<div className="alert-rule__created-at">{formatEpochTimestamp(value)}</div>
),
},
{
title: 'ACTIONS',
width: 140,
align: 'right',
render: (record): JSX.Element => (
<ConditionalAlertPopover
relatedTracesLink={record.relatedTracesLink}
relatedLogsLink={record.relatedLogsLink}
>
<div className="alert-rule-created-at">{formatEpochTimestamp(value)}</div>
<Button type="text" ghost>
<EllipsisOutlined className="dropdown-icon" />
</Button>
</ConditionalAlertPopover>
),
},

View File

@ -214,7 +214,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const pageTitle = t(routeKey);
const renderFullScreen =
pathname === ROUTES.GET_STARTED ||
pathname === ROUTES.WORKSPACE_LOCKED ||
pathname === ROUTES.GET_STARTED_APPLICATION_MONITORING ||
pathname === ROUTES.GET_STARTED_INFRASTRUCTURE_MONITORING ||
pathname === ROUTES.GET_STARTED_LOGS_MANAGEMENT ||
@ -282,6 +281,14 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const isSideNavCollapsed = getLocalStorageKey(IS_SIDEBAR_COLLAPSED);
/**
* Note: Right now we don't have a page-level method to pass the sidebar collapse state.
* Since the use case for overriding is not widely needed, we are setting it here
* so that the workspace locked page will have an expanded sidebar regardless of how users
* have set it or what is stored in localStorage. This will not affect the localStorage config.
*/
const isWorkspaceLocked = pathname === ROUTES.WORKSPACE_LOCKED;
return (
<Layout
className={cx(
@ -326,7 +333,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
licenseData={licenseData}
isFetching={isFetching}
onCollapse={onCollapse}
collapsed={collapsed}
collapsed={isWorkspaceLocked ? false : collapsed}
/>
)}
<div

View File

@ -472,6 +472,15 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
if (currentWidget?.panelTypes === PANEL_GROUP_TYPES.ROW) {
const rowWidgetProperties = currentPanelMap[id] || {};
let { title } = currentWidget;
if (rowWidgetProperties.collapsed) {
const widgetCount = rowWidgetProperties.widgets?.length || 0;
const collapsedText = `(${widgetCount} widget${
widgetCount > 1 ? 's' : ''
})`;
title += ` ${collapsedText}`;
}
return (
<CardContainer
className="row-card"
@ -489,9 +498,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
cursor="move"
/>
)}
<Typography.Text className="section-title">
{currentWidget.title}
</Typography.Text>
<Typography.Text className="section-title">{title}</Typography.Text>
{rowWidgetProperties.collapsed ? (
<ChevronDown
size={14}

View File

@ -0,0 +1,35 @@
import './customerStoryCard.styles.scss';
import { Avatar, Card, Space } from 'antd';
interface CustomerStoryCardProps {
avatar: string;
personName: string;
role: string;
message: string;
link: string;
}
function CustomerStoryCard({
avatar,
personName,
role,
message,
link,
}: CustomerStoryCardProps): JSX.Element {
return (
<a href={link} target="_blank" rel="noopener noreferrer">
<Card className="customer-story-card">
<Space size="middle" direction="vertical">
<Card.Meta
avatar={<Avatar size={48} src={avatar} />}
title={personName}
description={role}
/>
{message}
</Space>
</Card>
</a>
);
}
export default CustomerStoryCard;

View File

@ -0,0 +1,30 @@
import { Col, Row, Space, Typography } from 'antd';
interface InfoItem {
title: string;
description: string;
id: string; // Add a unique identifier
}
interface InfoBlocksProps {
items: InfoItem[];
}
function InfoBlocks({ items }: InfoBlocksProps): JSX.Element {
return (
<Space direction="vertical" size="middle">
{items.map((item) => (
<Row gutter={8} key={item.id}>
<Col span={24}>
<Typography.Title level={5}>{item.title}</Typography.Title>
</Col>
<Col span={24}>
<Typography>{item.description}</Typography>
</Col>
</Row>
))}
</Space>
);
}
export default InfoBlocks;

View File

@ -1,16 +1,161 @@
.workspace-locked-container {
text-align: center;
padding: 48px;
margin: 24px;
$light-theme: 'lightMode';
@keyframes gradientFlow {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
.workpace-locked-details {
width: 50%;
margin: 0 auto;
.workspace-locked {
&__modal {
.ant-modal-mask {
backdrop-filter: blur(2px);
}
}
&__tabs {
margin-top: 148px;
.ant-tabs {
&-nav {
&::before {
border-color: var(--bg-slate-500);
.#{$light-theme} & {
border-color: var(--bg-vanilla-300);
}
}
}
&-nav-wrap {
justify-content: center;
}
}
}
&__modal {
&__header {
display: flex;
justify-content: space-between;
align-items: center;
&__actions {
display: flex;
align-items: center;
gap: 16px;
}
}
.ant-modal-content {
border-radius: 4px;
border: 1px solid var(--bg-slate-400);
background: linear-gradient(
139deg,
rgba(18, 19, 23, 0.8) 0%,
rgba(18, 19, 23, 0.9) 98.68%
);
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(20px);
.#{$light-theme} & {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
}
}
.ant-modal-header {
background: transparent;
}
.ant-list {
&-item {
border-color: var(--bg-slate-500);
.#{$light-theme} & {
border-color: var(--bg-vanilla-300);
}
&-meta {
align-items: center !important;
&-title {
margin-bottom: 0 !important;
}
&-avatar {
display: flex;
}
}
}
}
&__title {
font-weight: 400;
color: var(--text-vanilla-400);
.#{$light-theme} & {
color: var(--text-ink-200);
}
}
&__cta {
margin-top: 54px;
}
}
&__container {
padding-top: 64px;
}
&__details {
width: 80%;
margin: 0 auto;
color: var(--text-vanilla-400, #c0c1c3);
text-align: center;
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 24px; /* 150% */
.#{$light-theme} & {
color: var(--text-ink-200);
}
&__highlight {
color: var(--text-vanilla-100, #fff);
font-style: normal;
font-weight: 700;
line-height: 24px;
.#{$light-theme} & {
color: var(--text-ink-100);
}
}
}
&__title {
background: linear-gradient(
99deg,
#ead8fd 0%,
#7a97fa 33%,
#fd5ab2 66%,
#ead8fd 100%
);
background-size: 300% 300%;
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
animation: gradientFlow 24s ease infinite;
margin-bottom: 18px;
}
}
.contact-us {
margin-top: 48px;
color: var(--text-vanilla-400);
.#{$light-theme} & {
color: var(--text-ink-200);
}
}
.cta {

View File

@ -20,17 +20,17 @@ describe('WorkspaceLocked', () => {
});
const workspaceLocked = await screen.findByRole('heading', {
name: /workspace locked/i,
name: /upgrade to continue/i,
});
expect(workspaceLocked).toBeInTheDocument();
const gotQuestionText = await screen.findByText(/got question?/i);
expect(gotQuestionText).toBeInTheDocument();
const contactUsLink = await screen.findByRole('link', {
name: /contact us/i,
const contactUsBtn = await screen.findByRole('button', {
name: /Contact Us/i,
});
expect(contactUsLink).toBeInTheDocument();
expect(contactUsBtn).toBeInTheDocument();
});
test('Render for Admin', async () => {
@ -42,11 +42,11 @@ describe('WorkspaceLocked', () => {
render(<WorkspaceLocked />);
const contactAdminMessage = await screen.queryByText(
/please contact your administrator for further help/i,
/contact your admin to proceed with the upgrade./i,
);
expect(contactAdminMessage).not.toBeInTheDocument();
const updateCreditCardBtn = await screen.findByRole('button', {
name: /update credit card/i,
name: /continue my journey/i,
});
expect(updateCreditCardBtn).toBeInTheDocument();
});
@ -60,12 +60,12 @@ describe('WorkspaceLocked', () => {
render(<WorkspaceLocked />, {}, 'VIEWER');
const updateCreditCardBtn = await screen.queryByRole('button', {
name: /update credit card/i,
name: /Continue My Journey/i,
});
expect(updateCreditCardBtn).not.toBeInTheDocument();
const contactAdminMessage = await screen.findByText(
/please contact your administrator for further help/i,
/contact your admin to proceed with the upgrade./i,
);
expect(contactAdminMessage).toBeInTheDocument();
});

View File

@ -1,21 +1,30 @@
/* eslint-disable react/no-unescaped-entities */
import './WorkspaceLocked.styles.scss';
import type { TabsProps } from 'antd';
import {
CreditCardOutlined,
LockOutlined,
SendOutlined,
} from '@ant-design/icons';
import { Button, Card, Skeleton, Typography } from 'antd';
Alert,
Button,
Col,
Collapse,
Flex,
List,
Modal,
Row,
Skeleton,
Space,
Tabs,
Typography,
} from 'antd';
import updateCreditCardApi from 'api/billing/checkout';
import logEvent from 'api/common/logEvent';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import ROUTES from 'constants/routes';
import FullScreenHeader from 'container/FullScreenHeader/FullScreenHeader';
import useLicense from 'hooks/useLicense';
import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
import { CircleArrowRight } from 'lucide-react';
import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useMutation } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
@ -23,13 +32,22 @@ import { License } from 'types/api/licenses/def';
import AppReducer from 'types/reducer/app';
import { getFormattedDate } from 'utils/timeUtils';
import CustomerStoryCard from './CustomerStoryCard';
import InfoBlocks from './InfoBlocks';
import {
customerStoriesData,
enterpriseGradeValuesData,
faqData,
infoData,
} from './workspaceLocked.data';
export default function WorkspaceBlocked(): JSX.Element {
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
const isAdmin = role === 'ADMIN';
const [activeLicense, setActiveLicense] = useState<License | null>(null);
const { notifications } = useNotifications();
const { t } = useTranslation(['workspaceLocked']);
const {
isFetching: isFetchingLicenseData,
isLoading: isLoadingLicenseData,
@ -67,7 +85,7 @@ export default function WorkspaceBlocked(): JSX.Element {
},
onError: () =>
notifications.error({
message: SOMETHING_WENT_WRONG,
message: t('somethingWentWrong'),
}),
},
);
@ -87,73 +105,248 @@ export default function WorkspaceBlocked(): JSX.Element {
logEvent('Workspace Blocked: User Clicked Extend Trial', {});
notifications.info({
message: 'Extend Trial',
message: t('extendTrial'),
duration: 0,
description: (
<Typography>
If you have a specific reason why you were not able to finish your PoC in
the trial period, please write to us on
<a href="mailto:cloud-support@signoz.io"> cloud-support@signoz.io </a>
with the reason. Sometimes we can extend trial by a few days on a case by
case basis
{t('extendTrialMsgPart1')}{' '}
<a href="mailto:cloud-support@signoz.io">cloud-support@signoz.io</a>{' '}
{t('extendTrialMsgPart2')}
</Typography>
),
});
};
return (
<>
<FullScreenHeader overrideRoute={ROUTES.WORKSPACE_LOCKED} />
const renderCustomerStories = (
filterCondition: (index: number) => boolean,
): JSX.Element[] =>
customerStoriesData
.filter((_, index) => filterCondition(index))
.map((story) => (
<CustomerStoryCard
avatar={story.avatar}
personName={story.personName}
role={story.role}
message={story.message}
link={story.link}
key={story.key}
/>
));
<Card className="workspace-locked-container">
{isLoadingLicenseData || !licensesData?.payload?.workSpaceBlock ? (
<Skeleton />
) : (
<>
<LockOutlined style={{ fontSize: '36px', color: '#08c' }} />
<Typography.Title level={4}> Workspace Locked </Typography.Title>
<Typography.Paragraph className="workpace-locked-details">
You have been locked out of your workspace because your trial ended
without an upgrade to a paid plan. Your data will continue to be ingested
till{' '}
{getFormattedDate(licensesData?.payload?.gracePeriodEnd || Date.now())} ,
at which point we will drop all the ingested data and terminate the
account.
{!isAdmin && 'Please contact your administrator for further help'}
</Typography.Paragraph>
<div className="cta">
const tabItems: TabsProps['items'] = [
{
key: '1',
label: t('whyChooseSignoz'),
children: (
<Row align="middle" justify="center">
<Col span={12}>
<Row gutter={[24, 48]}>
<Col span={24}>
<InfoBlocks items={infoData} />
</Col>
<Col span={24}>
<Space size="large" direction="vertical">
<Flex vertical>
<Typography.Title level={3}>
{t('enterpriseGradeObservability')}
</Typography.Title>
<Typography>{t('observabilityDescription')}</Typography>
</Flex>
<List
itemLayout="horizontal"
dataSource={enterpriseGradeValuesData}
renderItem={(item, index): React.ReactNode => (
<List.Item key={index}>
<List.Item.Meta avatar={<CircleArrowRight />} title={item.title} />
</List.Item>
)}
/>
</Space>
</Col>
{isAdmin && (
<Col span={24}>
<Button
type="primary"
shape="round"
size="middle"
loading={isLoading}
onClick={handleUpdateCreditCard}
>
{t('continueToUpgrade')}
</Button>
</Col>
)}
</Row>
</Col>
</Row>
),
},
{
key: '2',
label: t('youAreInGoodCompany'),
children: (
<Row gutter={[24, 16]} justify="center">
{/* #FIXME: please suggest if there is any better way to loop in different columns to get the masonry layout */}
<Col span={10}>{renderCustomerStories((index) => index % 2 === 0)}</Col>
<Col span={10}>{renderCustomerStories((index) => index % 2 !== 0)}</Col>
{isAdmin && (
<Col span={24}>
<Flex justify="center">
<Button
className="update-credit-card-btn"
type="primary"
icon={<CreditCardOutlined />}
shape="round"
size="middle"
loading={isLoading}
onClick={handleUpdateCreditCard}
>
Update Credit Card
{t('continueToUpgrade')}
</Button>
</Flex>
</Col>
)}
</Row>
),
},
// #TODO: comming soon
// {
// key: '3',
// label: 'Our Pricing',
// children: 'Our Pricing',
// },
{
key: '4',
label: t('faqs'),
children: (
<Row align="middle" justify="center">
<Col span={18}>
<Space size="large" direction="vertical">
<Collapse items={faqData} defaultActiveKey={['1']} />
{isAdmin && (
<Button
type="primary"
shape="round"
size="middle"
loading={isLoading}
onClick={handleUpdateCreditCard}
>
{t('continueToUpgrade')}
</Button>
)}
</Space>
</Col>
</Row>
),
},
];
return (
<div>
<Modal
rootClassName="workspace-locked__modal"
title={
<div className="workspace-locked__modal__header">
<span className="workspace-locked__modal__title">
{t('trialPlanExpired')}
</span>
<span className="workspace-locked__modal__header__actions">
<Typography.Text className="workspace-locked__modal__title">
Got Questions?
</Typography.Text>
<Button
className="extend-trial-btn"
type="default"
icon={<SendOutlined />}
shape="round"
size="middle"
onClick={handleExtendTrial}
href="mailto:cloud-support@signoz.io"
role="button"
>
Extend Trial
Contact Us
</Button>
</div>
<div className="contact-us">
Got Questions?
<span>
<a href="mailto:cloud-support@signoz.io"> Contact Us </a>
</span>
</div>
</>
)}
</Card>
</>
</span>
</div>
}
open
closable={false}
footer={null}
width="65%"
>
<div className="workspace-locked__container">
{isLoadingLicenseData || !licensesData ? (
<Skeleton />
) : (
<>
<Row justify="center" align="middle">
<Col>
<Space direction="vertical" align="center">
<Typography.Title level={2}>
<div className="workspace-locked__title">Upgrade to Continue</div>
</Typography.Title>
<Typography.Paragraph className="workspace-locked__details">
{t('upgradeNow')}
<br />
{t('yourDataIsSafe')}{' '}
<span className="workspace-locked__details__highlight">
{getFormattedDate(
licensesData.payload?.gracePeriodEnd || Date.now(),
)}
</span>{' '}
{t('actNow')}
</Typography.Paragraph>
</Space>
</Col>
</Row>
{!isAdmin && (
<Row
justify="center"
align="middle"
className="workspace-locked__modal__cta"
gutter={[16, 16]}
>
<Col>
<Alert
message="Contact your admin to proceed with the upgrade."
type="info"
/>
</Col>
</Row>
)}
{isAdmin && (
<Row
justify="center"
align="middle"
className="workspace-locked__modal__cta"
gutter={[16, 16]}
>
<Col>
<Button
type="primary"
shape="round"
size="middle"
loading={isLoading}
onClick={handleUpdateCreditCard}
>
continue my journey
</Button>
</Col>
<Col>
<Button
type="default"
shape="round"
size="middle"
onClick={handleExtendTrial}
>
{t('needMoreTime')}
</Button>
</Col>
</Row>
)}
<Flex justify="center" className="workspace-locked__tabs">
<Tabs items={tabItems} defaultActiveKey="2" />
</Flex>
</>
)}
</div>
</Modal>
</div>
);
}

View File

@ -0,0 +1,33 @@
$component-name: 'customer-story-card';
$ant-card-override: 'ant-card';
$light-theme: 'lightMode';
.#{$component-name} {
max-width: 385px;
margin: 0 auto; // Center the card within the column
margin-bottom: 24px;
border-radius: 6px;
transition: transform 0.3s ease, box-shadow 0.3s ease;
background-color: var(--bg-ink-400);
border: 1px solid var(--bg-ink-300);
.#{$light-theme} & {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
}
.#{$ant-card-override}-meta-title {
margin-bottom: 2px !important;
}
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
background-color: var(--bg-ink-300);
.#{$light-theme} & {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
background-color: var(--bg-vanilla-100);
}
}
}

View File

@ -0,0 +1,156 @@
export const infoData = [
{
id: 'infoBlock-1',
title: 'Built for scale',
description:
'Our powerful ingestion engine has a proven track record of handling 10TB+ data ingestion per day.',
},
{
id: 'infoBlock-2',
title: 'Trusted across the globe',
description:
'Used by teams in all 5 continents ⎯ across the mountains, rivers, and the high seas.',
},
{
id: 'infoBlock-3',
title: 'Powering observability for teams of all sizes',
description:
'Hundreds of companies ⎯from early-stage start-ups to public enterprises use SigNoz to build more reliable products.',
},
];
export const enterpriseGradeValuesData = [
{
title: 'SSO and SAML support',
},
{
title: 'Query API keys',
},
{
title: 'Advanced security with SOC 2 Type I certification',
},
{
title: 'AWS Private Link',
},
{
title: 'VPC peering',
},
{
title: 'Custom integrations',
},
];
export const customerStoriesData = [
{
key: 'c-story-1',
avatar: 'https://signoz.io/img/users/subomi-oluwalana.webp',
personName: 'Subomi Oluwalana',
role: 'Founder & CEO at Convoy',
customerName: 'Convoy',
message:
"We use OTel with SigNoz to spot redundant database connect calls. For example, we found that our database driver wasn't using the connection pool even though the documentation claimed otherwise.",
link:
'https://www.linkedin.com/feed/update/urn:li:activity:7212117589068591105/',
},
{
key: 'c-story-2',
avatar: 'https://signoz.io/img/users/dhruv-garg.webp',
personName: 'Dhruv Garg',
role: 'Tech Lead at Nudge',
customerName: 'Nudge',
message:
'SigNoz is one of the best observability tools you can self-host hands down. And they are always there to help on their slack channel when needed.',
link:
'https://www.linkedin.com/posts/dhruv-garg79_signoz-docker-kubernetes-activity-7205163679028240384-Otlb/',
},
{
key: 'c-story-3',
avatar: 'https://signoz.io/img/users/vivek-bhakta.webp',
personName: 'Vivek Bhakta',
role: 'CTO at Wombo AI',
customerName: 'Wombo AI',
message:
'We use SigNoz and have been loving it - can definitely handle scale.',
link: 'https://x.com/notorious_VB/status/1701773119696904242',
},
{
key: 'c-story-4',
avatar: 'https://signoz.io/img/users/pranay-narang.webp',
personName: 'Pranay Narang',
role: 'Engineering at Azodha',
customerName: 'Azodha',
message:
'Recently moved metrics and logging to SigNoz. Gotta say, absolutely loving the tool.',
link: 'https://x.com/PranayNarang/status/1676247073396752387',
},
{
key: 'c-story-4',
avatar: 'https://signoz.io/img/users/shey.webp',
personName: 'Sheheryar Sewani',
role: 'Seasoned Rails Dev & Founder',
customerName: '',
message:
"But wow, I'm glad I tried SigNoz. Setting up SigNoz was easy—they provide super helpful instructions along with a docker-compose file.",
link:
'https://www.linkedin.com/feed/update/urn:li:activity:7181011853915926528/',
},
{
key: 'c-story-5',
avatar: 'https://signoz.io/img/users/daniel.webp',
personName: 'Daniel Schell',
role: 'Founder & CTO at Airlockdigital',
customerName: 'Airlockdigital',
message:
'Have been deep diving Signoz. Seems like the new hotness for an "all-in-one".',
link: 'https://x.com/danonit/status/1749256583157284919',
},
{
key: 'c-story-6',
avatar: 'https://signoz.io/img/users/go-frendi.webp',
personName: 'Go Frendi Gunawan',
role: 'Data Engineer at Ctlyst.id',
customerName: 'Ctlyst.id',
message:
'Monitoring done. Thanks to SigNoz, I dont have to deal with Grafana, Loki, Prometheus, and Jaeger separately.',
link: 'https://x.com/gofrendiasgard/status/1680139003658641408',
},
{
key: 'c-story-7',
avatar: 'https://signoz.io/img/users/anselm.jpg',
personName: 'Anselm Eickhoff',
role: 'Software Architect',
customerName: '',
message:
'NewRelic: receiving OpenTelemetry at all takes me 1/2 day to grok, docs are a mess. Traces show up after 5min. I burn the free 100GB/mo in 1 day of light testing. @SignozHQ: can run it locally (∞GB), has a special tutorial for OpenTelemetry + Rust! Traces show up immediately.',
link:
'https://twitter.com/ae_play/status/1572993932094472195?s=20&t=LWWrW5EP_k5q6_mwbFN4jQ',
},
];
export const faqData = [
{
key: '1',
label:
'What is the difference between SigNoz Cloud(Teams) and Community Edition?',
children:
'You can self-host and manage the community edition yourself. You should choose SigNoz Cloud if you dont want to worry about managing the SigNoz cluster. There are some exclusive features like SSO & SAML support, which come with SigNoz cloud offering. Our team also offers support on the initial configuration of dashboards & alerts and advises on best practices for setting up your observability stack in the SigNoz cloud offering.',
},
{
key: '2',
label: 'How are number of samples calculated for metrics pricing?',
children:
"If a timeseries sends data every 30s, then it will generate 2 samples per min. So, if you have 10,000 time series sending data every 30s then you will be sending 20,000 samples per min to SigNoz. This will be around 864 mn samples per month and would cost 86.4 USD/month. Here's an explainer video on how metrics pricing is calculated - Link: https://vimeo.com/973012522",
},
{
key: '3',
label: 'Do you offer enterprise support plans?',
children:
'Yes, feel free to reach out to us on hello@signoz.io if you need a dedicated support plan or paid support for setting up your initial SigNoz setup.',
},
{
key: '4',
label: 'Who should use Enterprise plans?',
children:
'Teams which need enterprise support or features like SSO, Audit logs, etc. may find our enterprise plans valuable.',
},
];

View File

@ -0,0 +1,31 @@
package v4
import (
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
)
var logOperators = map[v3.FilterOperator]string{
v3.FilterOperatorEqual: "=",
v3.FilterOperatorNotEqual: "!=",
v3.FilterOperatorLessThan: "<",
v3.FilterOperatorLessThanOrEq: "<=",
v3.FilterOperatorGreaterThan: ">",
v3.FilterOperatorGreaterThanOrEq: ">=",
v3.FilterOperatorLike: "LIKE",
v3.FilterOperatorNotLike: "NOT LIKE",
v3.FilterOperatorContains: "LIKE",
v3.FilterOperatorNotContains: "NOT LIKE",
v3.FilterOperatorRegex: "match(%s, %s)",
v3.FilterOperatorNotRegex: "NOT match(%s, %s)",
v3.FilterOperatorIn: "IN",
v3.FilterOperatorNotIn: "NOT IN",
v3.FilterOperatorExists: "mapContains(%s_%s, '%s')",
v3.FilterOperatorNotExists: "not mapContains(%s_%s, '%s')",
}
const (
BODY = "body"
DISTRIBUTED_LOGS_V2 = "distributed_logs_v2"
DISTRIBUTED_LOGS_V2_RESOURCE = "distributed_logs_v2_resource"
NANOSECOND = 1000000000
)

View File

@ -0,0 +1,201 @@
package v4
import (
"fmt"
"strings"
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
"go.signoz.io/signoz/pkg/query-service/utils"
)
// buildResourceFilter builds a clickhouse filter string for resource labels
func buildResourceFilter(logsOp string, key string, op v3.FilterOperator, value interface{}) string {
searchKey := fmt.Sprintf("simpleJSONExtractString(labels, '%s')", key)
chFmtVal := utils.ClickHouseFormattedValue(value)
switch op {
case v3.FilterOperatorExists:
return fmt.Sprintf("simpleJSONHas(labels, '%s')", key)
case v3.FilterOperatorNotExists:
return fmt.Sprintf("not simpleJSONHas(labels, '%s')", key)
case v3.FilterOperatorRegex, v3.FilterOperatorNotRegex:
return fmt.Sprintf(logsOp, searchKey, chFmtVal)
case v3.FilterOperatorContains, v3.FilterOperatorNotContains:
// this is required as clickhouseFormattedValue add's quotes to the string
escapedStringValue := utils.QuoteEscapedStringForContains(fmt.Sprintf("%s", value))
return fmt.Sprintf("%s %s '%%%s%%'", searchKey, logsOp, escapedStringValue)
default:
return fmt.Sprintf("%s %s %s", searchKey, logsOp, chFmtVal)
}
}
// buildIndexFilterForInOperator builds a clickhouse filter string for in operator
// example:= x in a,b,c = (labels like '%x%a%' or labels like '%"x":"b"%' or labels like '%"x"="c"%')
// example:= x nin a,b,c = (labels nlike '%x%a%' AND labels nlike '%"x"="b"' AND labels nlike '%"x"="c"%')
func buildIndexFilterForInOperator(key string, op v3.FilterOperator, value interface{}) string {
conditions := []string{}
separator := " OR "
sqlOp := "like"
if op == v3.FilterOperatorNotIn {
separator = " AND "
sqlOp = "not like"
}
// values is a slice of strings, we need to convert value to this type
// value can be string or []interface{}
values := []string{}
switch value.(type) {
case string:
values = append(values, value.(string))
case []interface{}:
for _, v := range (value).([]interface{}) {
// also resources attributes are always string values
strV, ok := v.(string)
if !ok {
continue
}
values = append(values, strV)
}
}
// if there are no values to filter on, return an empty string
if len(values) > 0 {
for _, v := range values {
value := utils.QuoteEscapedStringForContains(v)
conditions = append(conditions, fmt.Sprintf("labels %s '%%\"%s\":\"%s\"%%'", sqlOp, key, value))
}
return "(" + strings.Join(conditions, separator) + ")"
}
return ""
}
// buildResourceIndexFilter builds a clickhouse filter string for resource labels
// example:= x like '%john%' = labels like '%x%john%'
func buildResourceIndexFilter(key string, op v3.FilterOperator, value interface{}) string {
// not using clickhouseFormattedValue as we don't wan't the quotes
formattedValueEscaped := utils.QuoteEscapedStringForContains(fmt.Sprintf("%s", value))
// add index filters
switch op {
case v3.FilterOperatorContains, v3.FilterOperatorEqual, v3.FilterOperatorLike:
return fmt.Sprintf("labels like '%%%s%%%s%%'", key, formattedValueEscaped)
case v3.FilterOperatorNotContains, v3.FilterOperatorNotEqual, v3.FilterOperatorNotLike:
return fmt.Sprintf("labels not like '%%%s%%%s%%'", key, formattedValueEscaped)
case v3.FilterOperatorNotRegex:
return fmt.Sprintf("labels not like '%%%s%%'", key)
case v3.FilterOperatorIn, v3.FilterOperatorNotIn:
return buildIndexFilterForInOperator(key, op, value)
default:
return fmt.Sprintf("labels like '%%%s%%'", key)
}
}
// buildResourceFiltersFromFilterItems builds a list of clickhouse filter strings for resource labels from a FilterSet.
// It skips any filter items that are not resource attributes and checks that the operator is supported and the data type is correct.
func buildResourceFiltersFromFilterItems(fs *v3.FilterSet) ([]string, error) {
var conditions []string
if fs == nil || len(fs.Items) == 0 {
return nil, nil
}
for _, item := range fs.Items {
// skip anything other than resource attribute
if item.Key.Type != v3.AttributeKeyTypeResource {
continue
}
// since out map is in lower case we are converting it to lowercase
operatorLower := strings.ToLower(string(item.Operator))
op := v3.FilterOperator(operatorLower)
keyName := item.Key.Key
// resource filter value data type will always be string
// will be an interface if the operator is IN or NOT IN
if item.Key.DataType != v3.AttributeKeyDataTypeString &&
(op != v3.FilterOperatorIn && op != v3.FilterOperatorNotIn) {
return nil, fmt.Errorf("invalid data type for resource attribute: %s", item.Key.Key)
}
var value interface{}
var err error
if op != v3.FilterOperatorExists && op != v3.FilterOperatorNotExists {
// make sure to cast the value regardless of the actual type
value, err = utils.ValidateAndCastValue(item.Value, item.Key.DataType)
if err != nil {
return nil, fmt.Errorf("failed to validate and cast value for %s: %v", item.Key.Key, err)
}
}
if logsOp, ok := logOperators[op]; ok {
// the filter
if resourceFilter := buildResourceFilter(logsOp, keyName, op, value); resourceFilter != "" {
conditions = append(conditions, resourceFilter)
}
// the additional filter for better usage of the index
if resourceIndexFilter := buildResourceIndexFilter(keyName, op, value); resourceIndexFilter != "" {
conditions = append(conditions, resourceIndexFilter)
}
} else {
return nil, fmt.Errorf("unsupported operator: %s", op)
}
}
return conditions, nil
}
func buildResourceFiltersFromGroupBy(groupBy []v3.AttributeKey) []string {
var conditions []string
for _, attr := range groupBy {
if attr.Type != v3.AttributeKeyTypeResource {
continue
}
conditions = append(conditions, fmt.Sprintf("(simpleJSONHas(labels, '%s') AND labels like '%%%s%%')", attr.Key, attr.Key))
}
return conditions
}
func buildResourceFiltersFromAggregateAttribute(aggregateAttribute v3.AttributeKey) string {
if aggregateAttribute.Key != "" && aggregateAttribute.Type == v3.AttributeKeyTypeResource {
return fmt.Sprintf("(simpleJSONHas(labels, '%s') AND labels like '%%%s%%')", aggregateAttribute.Key, aggregateAttribute.Key)
}
return ""
}
func buildResourceSubQuery(bucketStart, bucketEnd int64, fs *v3.FilterSet, groupBy []v3.AttributeKey, aggregateAttribute v3.AttributeKey) (string, error) {
// BUILD THE WHERE CLAUSE
var conditions []string
// only add the resource attributes to the filters here
rs, err := buildResourceFiltersFromFilterItems(fs)
if err != nil {
return "", err
}
conditions = append(conditions, rs...)
// for aggregate attribute add exists check in resources
aggregateAttributeResourceFilter := buildResourceFiltersFromAggregateAttribute(aggregateAttribute)
if aggregateAttributeResourceFilter != "" {
conditions = append(conditions, aggregateAttributeResourceFilter)
}
groupByResourceFilters := buildResourceFiltersFromGroupBy(groupBy)
if len(groupByResourceFilters) > 0 {
// TODO: change AND to OR once we know how to solve for group by ( i.e show values if one is not present)
groupByStr := "( " + strings.Join(groupByResourceFilters, " AND ") + " )"
conditions = append(conditions, groupByStr)
}
if len(conditions) == 0 {
return "", nil
}
conditionStr := strings.Join(conditions, " AND ")
// BUILD THE FINAL QUERY
query := fmt.Sprintf("SELECT fingerprint FROM signoz_logs.%s WHERE (seen_at_ts_bucket_start >= %d) AND (seen_at_ts_bucket_start <= %d) AND ", DISTRIBUTED_LOGS_V2_RESOURCE, bucketStart, bucketEnd)
query = "(" + query + conditionStr + ")"
return query, nil
}

View File

@ -0,0 +1,482 @@
package v4
import (
"reflect"
"testing"
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
)
func Test_buildResourceFilter(t *testing.T) {
type args struct {
logsOp string
key string
op v3.FilterOperator
value interface{}
}
tests := []struct {
name string
args args
want string
}{
{
name: "test exists",
args: args{
key: "service.name",
op: v3.FilterOperatorExists,
},
want: `simpleJSONHas(labels, 'service.name')`,
},
{
name: "test nexists",
args: args{
key: "service.name",
op: v3.FilterOperatorNotExists,
},
want: `not simpleJSONHas(labels, 'service.name')`,
},
{
name: "test regex",
args: args{
logsOp: "match(%s, %s)",
key: "service.name",
op: v3.FilterOperatorRegex,
value: ".*",
},
want: `match(simpleJSONExtractString(labels, 'service.name'), '.*')`,
},
{
name: "test contains",
args: args{
logsOp: "LIKE",
key: "service.name",
op: v3.FilterOperatorContains,
value: "Application%_",
},
want: `simpleJSONExtractString(labels, 'service.name') LIKE '%Application\%\_%'`,
},
{
name: "test eq",
args: args{
logsOp: "=",
key: "service.name",
op: v3.FilterOperatorEqual,
value: "Application",
},
want: `simpleJSONExtractString(labels, 'service.name') = 'Application'`,
},
{
name: "test value with quotes",
args: args{
logsOp: "=",
key: "service.name",
op: v3.FilterOperatorEqual,
value: "Application's",
},
want: `simpleJSONExtractString(labels, 'service.name') = 'Application\'s'`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := buildResourceFilter(tt.args.logsOp, tt.args.key, tt.args.op, tt.args.value); got != tt.want {
t.Errorf("buildResourceFilter() = %v, want %v", got, tt.want)
}
})
}
}
func Test_buildIndexFilterForInOperator(t *testing.T) {
type args struct {
key string
op v3.FilterOperator
value interface{}
}
tests := []struct {
name string
args args
want string
}{
{
name: "test in array",
args: args{
key: "service.name",
op: v3.FilterOperatorIn,
value: []interface{}{"Application", "Test"},
},
want: `(labels like '%"service.name":"Application"%' OR labels like '%"service.name":"Test"%')`,
},
{
name: "test nin array",
args: args{
key: "service.name",
op: v3.FilterOperatorNotIn,
value: []interface{}{"Application", "Test"},
},
want: `(labels not like '%"service.name":"Application"%' AND labels not like '%"service.name":"Test"%')`,
},
{
name: "test in string",
args: args{
key: "service.name",
op: v3.FilterOperatorIn,
value: "application",
},
want: `(labels like '%"service.name":"application"%')`,
},
{
name: "test nin string",
args: args{
key: "service.name",
op: v3.FilterOperatorNotIn,
value: "application'\"_s",
},
want: `(labels not like '%"service.name":"application\'"\_s"%')`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := buildIndexFilterForInOperator(tt.args.key, tt.args.op, tt.args.value); got != tt.want {
t.Errorf("buildIndexFilterForInOperator() = %v, want %v", got, tt.want)
}
})
}
}
func Test_buildResourceIndexFilter(t *testing.T) {
type args struct {
key string
op v3.FilterOperator
value interface{}
}
tests := []struct {
name string
args args
want string
}{
{
name: "test contains",
args: args{
key: "service.name",
op: v3.FilterOperatorContains,
value: "application",
},
want: `labels like '%service.name%application%'`,
},
{
name: "test not contains",
args: args{
key: "service.name",
op: v3.FilterOperatorNotContains,
value: "application",
},
want: `labels not like '%service.name%application%'`,
},
{
name: "test contains with % and _",
args: args{
key: "service.name",
op: v3.FilterOperatorNotContains,
value: "application%_test",
},
want: `labels not like '%service.name%application\%\_test%'`,
},
{
name: "test not regex",
args: args{
key: "service.name",
op: v3.FilterOperatorNotRegex,
value: ".*",
},
want: `labels not like '%service.name%'`,
},
{
name: "test in",
args: args{
key: "service.name",
op: v3.FilterOperatorNotIn,
value: []interface{}{"Application", "Test"},
},
want: `(labels not like '%"service.name":"Application"%' AND labels not like '%"service.name":"Test"%')`,
},
{
name: "test eq",
args: args{
key: "service.name",
op: v3.FilterOperatorEqual,
value: "Application",
},
want: `labels like '%service.name%Application%'`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := buildResourceIndexFilter(tt.args.key, tt.args.op, tt.args.value); got != tt.want {
t.Errorf("buildResourceIndexFilter() = %v, want %v", got, tt.want)
}
})
}
}
func Test_buildResourceFiltersFromFilterItems(t *testing.T) {
type args struct {
fs *v3.FilterSet
}
tests := []struct {
name string
args args
want []string
wantErr bool
}{
{
name: "ignore attribute",
args: args{
fs: &v3.FilterSet{
Items: []v3.FilterItem{
{
Key: v3.AttributeKey{
Key: "service.name",
DataType: v3.AttributeKeyDataTypeString,
Type: v3.AttributeKeyTypeTag,
},
Operator: v3.FilterOperatorEqual,
Value: "test",
},
},
},
},
want: nil,
wantErr: false,
},
{
name: "build filter",
args: args{
fs: &v3.FilterSet{
Items: []v3.FilterItem{
{
Key: v3.AttributeKey{
Key: "service.name",
DataType: v3.AttributeKeyDataTypeString,
Type: v3.AttributeKeyTypeResource,
},
Operator: v3.FilterOperatorEqual,
Value: "test",
},
},
},
},
want: []string{
"simpleJSONExtractString(labels, 'service.name') = 'test'",
"labels like '%service.name%test%'",
},
wantErr: false,
},
{
name: "build filter with multiple items",
args: args{
fs: &v3.FilterSet{
Items: []v3.FilterItem{
{
Key: v3.AttributeKey{
Key: "service.name",
DataType: v3.AttributeKeyDataTypeString,
Type: v3.AttributeKeyTypeResource,
},
Operator: v3.FilterOperatorEqual,
Value: "test",
},
{
Key: v3.AttributeKey{
Key: "namespace",
DataType: v3.AttributeKeyDataTypeString,
Type: v3.AttributeKeyTypeResource,
},
Operator: v3.FilterOperatorContains,
Value: "test1",
},
},
},
},
want: []string{
"simpleJSONExtractString(labels, 'service.name') = 'test'",
"labels like '%service.name%test%'",
"simpleJSONExtractString(labels, 'namespace') LIKE '%test1%'",
"labels like '%namespace%test1%'",
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := buildResourceFiltersFromFilterItems(tt.args.fs)
if (err != nil) != tt.wantErr {
t.Errorf("buildResourceFiltersFromFilterItems() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("buildResourceFiltersFromFilterItems() = %v, want %v", got, tt.want)
}
})
}
}
func Test_buildResourceFiltersFromGroupBy(t *testing.T) {
type args struct {
groupBy []v3.AttributeKey
}
tests := []struct {
name string
args args
want []string
}{
{
name: "build filter",
args: args{
groupBy: []v3.AttributeKey{
{
Key: "service.name",
DataType: v3.AttributeKeyDataTypeString,
Type: v3.AttributeKeyTypeResource,
},
},
},
want: []string{
"(simpleJSONHas(labels, 'service.name') AND labels like '%service.name%')",
},
},
{
name: "build filter multiple group by",
args: args{
groupBy: []v3.AttributeKey{
{
Key: "service.name",
DataType: v3.AttributeKeyDataTypeString,
Type: v3.AttributeKeyTypeResource,
},
{
Key: "namespace",
DataType: v3.AttributeKeyDataTypeString,
Type: v3.AttributeKeyTypeResource,
},
},
},
want: []string{
"(simpleJSONHas(labels, 'service.name') AND labels like '%service.name%')",
"(simpleJSONHas(labels, 'namespace') AND labels like '%namespace%')",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := buildResourceFiltersFromGroupBy(tt.args.groupBy); !reflect.DeepEqual(got, tt.want) {
t.Errorf("buildResourceFiltersFromGroupBy() = %v, want %v", got, tt.want)
}
})
}
}
func Test_buildResourceFiltersFromAggregateAttribute(t *testing.T) {
type args struct {
aggregateAttribute v3.AttributeKey
}
tests := []struct {
name string
args args
want string
}{
{
name: "build filter",
args: args{
aggregateAttribute: v3.AttributeKey{
Key: "service.name",
DataType: v3.AttributeKeyDataTypeString,
Type: v3.AttributeKeyTypeResource,
},
},
want: "(simpleJSONHas(labels, 'service.name') AND labels like '%service.name%')",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := buildResourceFiltersFromAggregateAttribute(tt.args.aggregateAttribute); got != tt.want {
t.Errorf("buildResourceFiltersFromAggregateAttribute() = %v, want %v", got, tt.want)
}
})
}
}
func Test_buildResourceSubQuery(t *testing.T) {
type args struct {
bucketStart int64
bucketEnd int64
fs *v3.FilterSet
groupBy []v3.AttributeKey
aggregateAttribute v3.AttributeKey
}
tests := []struct {
name string
args args
want string
wantErr bool
}{
{
name: "build sub query",
args: args{
bucketStart: 1680064560,
bucketEnd: 1680066458,
fs: &v3.FilterSet{
Items: []v3.FilterItem{
{
Key: v3.AttributeKey{
Key: "service.name",
DataType: v3.AttributeKeyDataTypeString,
Type: v3.AttributeKeyTypeResource,
},
Operator: v3.FilterOperatorEqual,
Value: "test",
},
{
Key: v3.AttributeKey{
Key: "namespace",
DataType: v3.AttributeKeyDataTypeString,
Type: v3.AttributeKeyTypeResource,
},
Operator: v3.FilterOperatorContains,
Value: "test1",
},
},
},
groupBy: []v3.AttributeKey{
{
Key: "host.name",
DataType: v3.AttributeKeyDataTypeString,
Type: v3.AttributeKeyTypeResource,
},
},
aggregateAttribute: v3.AttributeKey{
Key: "cluster.name",
DataType: v3.AttributeKeyDataTypeString,
Type: v3.AttributeKeyTypeResource,
},
},
want: "(SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE " +
"(seen_at_ts_bucket_start >= 1680064560) AND (seen_at_ts_bucket_start <= 1680066458) AND " +
"simpleJSONExtractString(labels, 'service.name') = 'test' AND labels like '%service.name%test%' " +
"AND simpleJSONExtractString(labels, 'namespace') LIKE '%test1%' AND labels like '%namespace%test1%' " +
"AND (simpleJSONHas(labels, 'cluster.name') AND labels like '%cluster.name%') AND " +
"( (simpleJSONHas(labels, 'host.name') AND labels like '%host.name%') ))",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := buildResourceSubQuery(tt.args.bucketStart, tt.args.bucketEnd, tt.args.fs, tt.args.groupBy, tt.args.aggregateAttribute)
if (err != nil) != tt.wantErr {
t.Errorf("buildResourceSubQuery() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("buildResourceSubQuery() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -103,6 +103,9 @@ func InitDB(dataSourceName string) (*ModelDaoSqlite, error) {
return nil, err
}
telemetry.GetInstance().SetUserCountCallback(mds.GetUserCount)
telemetry.GetInstance().SetUserRoleCallback(mds.GetUserRole)
return mds, nil
}
@ -140,7 +143,6 @@ func (mds *ModelDaoSqlite) initializeOrgPreferences(ctx context.Context) error {
users, _ := mds.GetUsers(ctx)
countUsers := len(users)
telemetry.GetInstance().SetCountUsers(int8(countUsers))
if countUsers > 0 {
telemetry.GetInstance().SetCompanyDomain(users[countUsers-1].Email)
telemetry.GetInstance().SetUserEmail(users[countUsers-1].Email)

View File

@ -612,3 +612,19 @@ func (mds *ModelDaoSqlite) PrecheckLogin(ctx context.Context, email, sourceUrl s
return resp, nil
}
func (mds *ModelDaoSqlite) GetUserRole(ctx context.Context, groupId string) (string, error) {
role, err := mds.GetGroup(ctx, groupId)
if err != nil || role == nil {
return "", err
}
return role.Name, nil
}
func (mds *ModelDaoSqlite) GetUserCount(ctx context.Context) (int, error) {
users, err := mds.GetUsers(ctx)
if err != nil {
return 0, err
}
return len(users), nil
}

View File

@ -176,16 +176,25 @@ type Telemetry struct {
rateLimits map[string]int8
activeUser map[string]int8
patTokenUser bool
countUsers int8
mutex sync.RWMutex
alertsInfoCallback func(ctx context.Context) (*model.AlertsInfo, error)
userCountCallback func(ctx context.Context) (int, error)
userRoleCallback func(ctx context.Context, groupId string) (string, error)
}
func (a *Telemetry) SetAlertsInfoCallback(callback func(ctx context.Context) (*model.AlertsInfo, error)) {
a.alertsInfoCallback = callback
}
func (a *Telemetry) SetUserCountCallback(callback func(ctx context.Context) (int, error)) {
a.userCountCallback = callback
}
func (a *Telemetry) SetUserRoleCallback(callback func(ctx context.Context, groupId string) (string, error)) {
a.userRoleCallback = callback
}
func createTelemetry() {
// Do not do anything in CI (not even resolving the outbound IP address)
if testing.Testing() {
@ -259,6 +268,8 @@ func createTelemetry() {
metricsTTL, _ := telemetry.reader.GetTTL(ctx, &model.GetTTLParams{Type: constants.MetricsTTL})
logsTTL, _ := telemetry.reader.GetTTL(ctx, &model.GetTTLParams{Type: constants.LogsTTL})
userCount, _ := telemetry.userCountCallback(ctx)
data := map[string]interface{}{
"totalSpans": totalSpans,
"spansInLastHeartBeatInterval": spansInLastHeartBeatInterval,
@ -266,7 +277,7 @@ func createTelemetry() {
"getSamplesInfoInLastHeartBeatInterval": getSamplesInfoInLastHeartBeatInterval,
"totalLogs": totalLogs,
"getLogsInfoInLastHeartBeatInterval": getLogsInfoInLastHeartBeatInterval,
"countUsers": telemetry.countUsers,
"countUsers": userCount,
"metricsTTLStatus": metricsTTL.Status,
"tracesTTLStatus": traceTTL.Status,
"logsTTLStatus": logsTTL.Status,
@ -450,11 +461,22 @@ func (a *Telemetry) IdentifyUser(user *model.User) {
if !a.isTelemetryEnabled() || a.isTelemetryAnonymous() {
return
}
// extract user group from user.groupId
role, _ := a.userRoleCallback(context.Background(), user.GroupId)
if a.saasOperator != nil {
a.saasOperator.Enqueue(analytics.Identify{
UserId: a.userEmail,
Traits: analytics.NewTraits().SetName(user.Name).SetEmail(user.Email),
})
if role != "" {
a.saasOperator.Enqueue(analytics.Identify{
UserId: a.userEmail,
Traits: analytics.NewTraits().SetName(user.Name).SetEmail(user.Email).Set("role", role),
})
} else {
a.saasOperator.Enqueue(analytics.Identify{
UserId: a.userEmail,
Traits: analytics.NewTraits().SetName(user.Name).SetEmail(user.Email),
})
}
a.saasOperator.Enqueue(analytics.Group{
UserId: a.userEmail,
GroupId: a.getCompanyDomain(),
@ -474,10 +496,6 @@ func (a *Telemetry) IdentifyUser(user *model.User) {
})
}
func (a *Telemetry) SetCountUsers(countUsers int8) {
a.countUsers = countUsers
}
func (a *Telemetry) SetUserEmail(email string) {
a.userEmail = email
}

View File

@ -154,6 +154,14 @@ func QuoteEscapedString(str string) string {
return str
}
func QuoteEscapedStringForContains(str string) string {
// https: //clickhouse.com/docs/en/sql-reference/functions/string-search-functions#like
str = QuoteEscapedString(str)
str = strings.ReplaceAll(str, `%`, `\%`)
str = strings.ReplaceAll(str, `_`, `\_`)
return str
}
// ClickHouseFormattedValue formats the value to be used in clickhouse query
func ClickHouseFormattedValue(v interface{}) string {
// if it's pointer convert it to a value