mirror of
https://github.com/SigNoz/signoz.git
synced 2026-02-07 10:22:12 +00:00
Compare commits
47 Commits
pkg/config
...
config
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba749a12ad | ||
|
|
fdcdbf021a | ||
|
|
b59744a4e0 | ||
|
|
fbfc2d3626 | ||
|
|
fb1157515d | ||
|
|
c92ef53e9c | ||
|
|
8e7ede0642 | ||
|
|
4a068eb68c | ||
|
|
268f283785 | ||
|
|
0aba107436 | ||
|
|
e99d3427ec | ||
|
|
44cbe53705 | ||
|
|
1ccc0b3c48 | ||
|
|
e695f89c85 | ||
|
|
f080bcd3ee | ||
|
|
c574adc634 | ||
|
|
a5635b10e1 | ||
|
|
1ab9018641 | ||
|
|
c3153012a6 | ||
|
|
8ba479d3bb | ||
|
|
939ab5270e | ||
|
|
4d398b1bb1 | ||
|
|
8874da0cf6 | ||
|
|
756c9d7364 | ||
|
|
f48a919945 | ||
|
|
f0b58cd5ae | ||
|
|
42525b6067 | ||
|
|
c66cd3ce4e | ||
|
|
e9618d64bc | ||
|
|
8e11a988be | ||
|
|
3095db106b | ||
|
|
0f06ea1a0c | ||
|
|
188d8a4302 | ||
|
|
db95840260 | ||
|
|
c0bf5f5b0a | ||
|
|
35ecd38cef | ||
|
|
6bd1e1387c | ||
|
|
6680622762 | ||
|
|
f3f315726d | ||
|
|
513629e02d | ||
|
|
b180999a71 | ||
|
|
040c0d708b | ||
|
|
64c62896f8 | ||
|
|
a1160b990d | ||
|
|
79d99f21f8 | ||
|
|
92299e1b08 | ||
|
|
bab8c8274c |
@@ -351,7 +351,7 @@ func (s *Server) createPrivateServer(apiHandler *api.APIHandler) (*http.Server,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) createPublicServer(apiHandler *api.APIHandler, web *web.Web) (*http.Server, error) {
|
||||
func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*http.Server, error) {
|
||||
|
||||
r := baseapp.NewRouter()
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiBaseInstance } from 'api';
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
@@ -59,7 +59,7 @@ export const getHostLists = async (
|
||||
headers?: Record<string, string>,
|
||||
): Promise<SuccessResponse<HostListResponse> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await ApiBaseInstance.post('/hosts/list', props, {
|
||||
const response = await axios.post('/hosts/list', props, {
|
||||
signal,
|
||||
headers,
|
||||
});
|
||||
|
||||
@@ -58,7 +58,11 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useMutation } from 'react-query';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { ErrorResponse } from 'types/api';
|
||||
import { LimitProps } from 'types/api/ingestionKeys/limits/types';
|
||||
import {
|
||||
AddLimitProps,
|
||||
LimitProps,
|
||||
UpdateLimitProps,
|
||||
} from 'types/api/ingestionKeys/limits/types';
|
||||
import {
|
||||
IngestionKeyProps,
|
||||
PaginationProps,
|
||||
@@ -69,6 +73,18 @@ const { Option } = Select;
|
||||
|
||||
const BYTES = 1073741824;
|
||||
|
||||
const COUNT_MULTIPLIER = {
|
||||
thousand: 1000,
|
||||
million: 1000000,
|
||||
billion: 1000000000,
|
||||
};
|
||||
|
||||
const SIGNALS_CONFIG = [
|
||||
{ name: 'logs', usesSize: true, usesCount: false },
|
||||
{ name: 'traces', usesSize: true, usesCount: false },
|
||||
{ name: 'metrics', usesSize: false, usesCount: true },
|
||||
];
|
||||
|
||||
// Using any type here because antd's DatePicker expects its own internal Dayjs type
|
||||
// which conflicts with our project's Dayjs type that has additional plugins (tz, utc etc).
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
|
||||
@@ -76,8 +92,6 @@ export const disabledDate = (current: any): boolean =>
|
||||
// Disable all dates before today
|
||||
current && current < dayjs().endOf('day');
|
||||
|
||||
const SIGNALS = ['logs', 'traces', 'metrics'];
|
||||
|
||||
export const showErrorNotification = (
|
||||
notifications: NotificationInstance,
|
||||
err: Error,
|
||||
@@ -101,6 +115,31 @@ export const API_KEY_EXPIRY_OPTIONS: ExpiryOption[] = [
|
||||
{ value: '0', label: 'No Expiry' },
|
||||
];
|
||||
|
||||
const countToUnit = (count: number): { value: number; unit: string } => {
|
||||
if (
|
||||
count >= COUNT_MULTIPLIER.billion ||
|
||||
count / COUNT_MULTIPLIER.million >= 1000
|
||||
) {
|
||||
return { value: count / COUNT_MULTIPLIER.billion, unit: 'billion' };
|
||||
}
|
||||
if (
|
||||
count >= COUNT_MULTIPLIER.million ||
|
||||
count / COUNT_MULTIPLIER.thousand >= 1000
|
||||
) {
|
||||
return { value: count / COUNT_MULTIPLIER.million, unit: 'million' };
|
||||
}
|
||||
if (count >= COUNT_MULTIPLIER.thousand) {
|
||||
return { value: count / COUNT_MULTIPLIER.thousand, unit: 'thousand' };
|
||||
}
|
||||
// Default to million for small numbers
|
||||
return { value: count / COUNT_MULTIPLIER.million, unit: 'million' };
|
||||
};
|
||||
|
||||
const countFromUnit = (value: number, unit: string): number =>
|
||||
value *
|
||||
(COUNT_MULTIPLIER[unit as keyof typeof COUNT_MULTIPLIER] ||
|
||||
COUNT_MULTIPLIER.million);
|
||||
|
||||
function MultiIngestionSettings(): JSX.Element {
|
||||
const { user } = useAppContext();
|
||||
const { notifications } = useNotifications();
|
||||
@@ -181,7 +220,6 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
|
||||
const showEditModal = (apiKey: IngestionKeyProps): void => {
|
||||
setActiveAPIKey(apiKey);
|
||||
|
||||
handleFormReset();
|
||||
setUpdatedTags(apiKey.tags || []);
|
||||
|
||||
@@ -424,44 +462,90 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
addEditLimitForm.resetFields();
|
||||
};
|
||||
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
const handleAddLimit = (
|
||||
APIKey: IngestionKeyProps,
|
||||
signalName: string,
|
||||
): void => {
|
||||
const { dailyLimit, secondsLimit } = addEditLimitForm.getFieldsValue();
|
||||
const {
|
||||
dailyLimit,
|
||||
secondsLimit,
|
||||
dailyCount,
|
||||
dailyCountUnit,
|
||||
secondsCount,
|
||||
secondsCountUnit,
|
||||
} = addEditLimitForm.getFieldsValue();
|
||||
|
||||
const payload = {
|
||||
const payload: AddLimitProps = {
|
||||
keyID: APIKey.id,
|
||||
signal: signalName,
|
||||
config: {},
|
||||
};
|
||||
|
||||
if (!isUndefined(dailyLimit)) {
|
||||
payload.config = {
|
||||
day: {
|
||||
const signalCfg = SIGNALS_CONFIG.find((cfg) => cfg.name === signalName);
|
||||
if (!signalCfg) return;
|
||||
|
||||
// Only set size if usesSize is true
|
||||
if (signalCfg.usesSize) {
|
||||
if (!isUndefined(dailyLimit)) {
|
||||
payload.config.day = {
|
||||
...payload.config.day,
|
||||
size: gbToBytes(dailyLimit),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (!isUndefined(secondsLimit)) {
|
||||
payload.config = {
|
||||
...payload.config,
|
||||
second: {
|
||||
};
|
||||
}
|
||||
if (!isUndefined(secondsLimit)) {
|
||||
payload.config.second = {
|
||||
...payload.config.second,
|
||||
size: gbToBytes(secondsLimit),
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (isUndefined(dailyLimit) && isUndefined(secondsLimit)) {
|
||||
// No need to save as no limit is provided, close the edit view and reset active signal and api key
|
||||
// Only set count if usesCount is true
|
||||
if (signalCfg.usesCount) {
|
||||
if (!isUndefined(dailyCount)) {
|
||||
payload.config.day = {
|
||||
...payload.config.day,
|
||||
count: countFromUnit(dailyCount, dailyCountUnit || 'million'),
|
||||
};
|
||||
}
|
||||
if (!isUndefined(secondsCount)) {
|
||||
payload.config.second = {
|
||||
...payload.config.second,
|
||||
count: countFromUnit(secondsCount, secondsCountUnit || 'million'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// If neither size nor count was given, skip
|
||||
const noSizeProvided =
|
||||
isUndefined(dailyLimit) && isUndefined(secondsLimit) && signalCfg.usesSize;
|
||||
const noCountProvided =
|
||||
isUndefined(dailyCount) && isUndefined(secondsCount) && signalCfg.usesCount;
|
||||
|
||||
if (
|
||||
signalCfg.usesSize &&
|
||||
signalCfg.usesCount &&
|
||||
noSizeProvided &&
|
||||
noCountProvided
|
||||
) {
|
||||
// Both size and count are effectively empty
|
||||
setActiveSignal(null);
|
||||
setActiveAPIKey(null);
|
||||
setIsEditAddLimitOpen(false);
|
||||
setUpdatedTags([]);
|
||||
hideAddViewModal();
|
||||
setHasCreateLimitForIngestionKeyError(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!signalCfg.usesSize && !signalCfg.usesCount) {
|
||||
// Edge case: If there's no count or size usage at all
|
||||
setActiveSignal(null);
|
||||
setActiveAPIKey(null);
|
||||
setIsEditAddLimitOpen(false);
|
||||
setUpdatedTags([]);
|
||||
hideAddViewModal();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -472,44 +556,73 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
APIKey: IngestionKeyProps,
|
||||
signal: LimitProps,
|
||||
): void => {
|
||||
const { dailyLimit, secondsLimit } = addEditLimitForm.getFieldsValue();
|
||||
const payload = {
|
||||
const {
|
||||
dailyLimit,
|
||||
secondsLimit,
|
||||
dailyCount,
|
||||
dailyCountUnit,
|
||||
secondsCount,
|
||||
secondsCountUnit,
|
||||
} = addEditLimitForm.getFieldsValue();
|
||||
|
||||
const payload: UpdateLimitProps = {
|
||||
limitID: signal.id,
|
||||
signal: signal.signal,
|
||||
config: {},
|
||||
};
|
||||
|
||||
if (isUndefined(dailyLimit) && isUndefined(secondsLimit)) {
|
||||
showDeleteLimitModal(APIKey, signal);
|
||||
const signalCfg = SIGNALS_CONFIG.find((cfg) => cfg.name === signal.signal);
|
||||
if (!signalCfg) return;
|
||||
|
||||
const noSizeProvided =
|
||||
isUndefined(dailyLimit) && isUndefined(secondsLimit) && signalCfg.usesSize;
|
||||
const noCountProvided =
|
||||
isUndefined(dailyCount) && isUndefined(secondsCount) && signalCfg.usesCount;
|
||||
|
||||
// If the user cleared out all fields, remove the limit
|
||||
if (noSizeProvided && noCountProvided) {
|
||||
showDeleteLimitModal(APIKey, signal);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isUndefined(dailyLimit)) {
|
||||
payload.config = {
|
||||
day: {
|
||||
if (signalCfg.usesSize) {
|
||||
if (!isUndefined(dailyLimit)) {
|
||||
payload.config.day = {
|
||||
...payload.config.day,
|
||||
size: gbToBytes(dailyLimit),
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
if (!isUndefined(secondsLimit)) {
|
||||
payload.config.second = {
|
||||
...payload.config.second,
|
||||
size: gbToBytes(secondsLimit),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!isUndefined(secondsLimit)) {
|
||||
payload.config = {
|
||||
...payload.config,
|
||||
second: {
|
||||
size: gbToBytes(secondsLimit),
|
||||
},
|
||||
};
|
||||
if (signalCfg.usesCount) {
|
||||
if (!isUndefined(dailyCount)) {
|
||||
payload.config.day = {
|
||||
...payload.config.day,
|
||||
count: countFromUnit(dailyCount, dailyCountUnit || 'million'),
|
||||
};
|
||||
}
|
||||
if (!isUndefined(secondsCount)) {
|
||||
payload.config.second = {
|
||||
...payload.config.second,
|
||||
count: countFromUnit(secondsCount, secondsCountUnit || 'million'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
updateLimitForIngestionKey(payload);
|
||||
};
|
||||
/* eslint-enable sonarjs/cognitive-complexity */
|
||||
|
||||
const bytesToGb = (size: number | undefined): number => {
|
||||
if (!size) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return size / BYTES;
|
||||
};
|
||||
|
||||
@@ -517,6 +630,12 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
APIKey: IngestionKeyProps,
|
||||
signal: LimitProps,
|
||||
): void => {
|
||||
const dayCount = signal?.config?.day?.count;
|
||||
const secondCount = signal?.config?.second?.count;
|
||||
|
||||
const dayCountConverted = countToUnit(dayCount || 0);
|
||||
const secondCountConverted = countToUnit(secondCount || 0);
|
||||
|
||||
setActiveAPIKey(APIKey);
|
||||
setActiveSignal({
|
||||
...signal,
|
||||
@@ -524,11 +643,14 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
...signal.config,
|
||||
day: {
|
||||
...signal.config?.day,
|
||||
enabled: !isNil(signal?.config?.day?.size),
|
||||
enabled:
|
||||
!isNil(signal?.config?.day?.size) || !isNil(signal?.config?.day?.count),
|
||||
},
|
||||
second: {
|
||||
...signal.config?.second,
|
||||
enabled: !isNil(signal?.config?.second?.size),
|
||||
enabled:
|
||||
!isNil(signal?.config?.second?.size) ||
|
||||
!isNil(signal?.config?.second?.count),
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -536,15 +658,22 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
addEditLimitForm.setFieldsValue({
|
||||
dailyLimit: bytesToGb(signal?.config?.day?.size || 0),
|
||||
secondsLimit: bytesToGb(signal?.config?.second?.size || 0),
|
||||
enableDailyLimit: !isNil(signal?.config?.day?.size),
|
||||
enableSecondLimit: !isNil(signal?.config?.second?.size),
|
||||
enableDailyLimit:
|
||||
!isNil(signal?.config?.day?.size) || !isNil(signal?.config?.day?.count),
|
||||
enableSecondLimit:
|
||||
!isNil(signal?.config?.second?.size) ||
|
||||
!isNil(signal?.config?.second?.count),
|
||||
dailyCount: dayCountConverted.value,
|
||||
dailyCountUnit: dayCountConverted.unit,
|
||||
secondsCount: secondCountConverted.value,
|
||||
secondsCountUnit: secondCountConverted.unit,
|
||||
});
|
||||
|
||||
setIsEditAddLimitOpen(true);
|
||||
};
|
||||
|
||||
const onDeleteLimitHandler = (): void => {
|
||||
if (activeSignal && activeSignal?.id) {
|
||||
if (activeSignal && activeSignal.id) {
|
||||
deleteLimitForKey(activeSignal.id);
|
||||
}
|
||||
};
|
||||
@@ -572,13 +701,13 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
formatTimezoneAdjustedTimestamp,
|
||||
);
|
||||
|
||||
const limits: { [key: string]: LimitProps } = {};
|
||||
|
||||
APIKey.limits?.forEach((limit: LimitProps) => {
|
||||
limits[limit.signal] = limit;
|
||||
// Convert array of limits to a dictionary for quick access
|
||||
const limitsDict: Record<string, LimitProps> = {};
|
||||
APIKey.limits?.forEach((limitItem: LimitProps) => {
|
||||
limitsDict[limitItem.signal] = limitItem;
|
||||
});
|
||||
|
||||
const hasLimits = (signal: string): boolean => !!limits[signal];
|
||||
const hasLimits = (signalName: string): boolean => !!limitsDict[signalName];
|
||||
|
||||
const items: CollapseProps['items'] = [
|
||||
{
|
||||
@@ -614,11 +743,9 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
showEditModal(APIKey);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
icon={<Trash2 color={Color.BG_CHERRY_500} size={14} />}
|
||||
@@ -670,18 +797,23 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
|
||||
<div className="limits-data">
|
||||
<div className="signals">
|
||||
{SIGNALS.map((signal) => {
|
||||
const hasValidDayLimit = !isNil(limits[signal]?.config?.day?.size);
|
||||
const hasValidSecondLimit = !isNil(
|
||||
limits[signal]?.config?.second?.size,
|
||||
);
|
||||
{SIGNALS_CONFIG.map((signalCfg) => {
|
||||
const signalName = signalCfg.name;
|
||||
const limit = limitsDict[signalName];
|
||||
|
||||
const hasValidDayLimit =
|
||||
limit?.config?.day?.size !== undefined ||
|
||||
limit?.config?.day?.count !== undefined;
|
||||
const hasValidSecondLimit =
|
||||
limit?.config?.second?.size !== undefined ||
|
||||
limit?.config?.second?.count !== undefined;
|
||||
|
||||
return (
|
||||
<div className="signal" key={signal}>
|
||||
<div className="signal" key={signalName}>
|
||||
<div className="header">
|
||||
<div className="signal-name">{signal}</div>
|
||||
<div className="signal-name">{signalName}</div>
|
||||
<div className="actions">
|
||||
{hasLimits(signal) ? (
|
||||
{hasLimits(signalName) ? (
|
||||
<>
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
@@ -690,10 +822,9 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
enableEditLimitMode(APIKey, limits[signal]);
|
||||
enableEditLimitMode(APIKey, limit);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
icon={<Trash2 color={Color.BG_CHERRY_500} size={14} />}
|
||||
@@ -701,7 +832,7 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
showDeleteLimitModal(APIKey, limits[signal]);
|
||||
showDeleteLimitModal(APIKey, limit);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
@@ -712,14 +843,12 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
shape="round"
|
||||
icon={<PlusIcon size={14} />}
|
||||
disabled={!!(activeAPIKey?.id === APIKey.id && activeSignal)}
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
enableEditLimitMode(APIKey, {
|
||||
id: signal,
|
||||
signal,
|
||||
id: signalName,
|
||||
signal: signalName,
|
||||
config: {},
|
||||
});
|
||||
}}
|
||||
@@ -732,7 +861,7 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
|
||||
<div className="signal-limit-values">
|
||||
{activeAPIKey?.id === APIKey.id &&
|
||||
activeSignal?.signal === signal &&
|
||||
activeSignal?.signal === signalName &&
|
||||
isEditAddLimitOpen ? (
|
||||
<Form
|
||||
name="edit-ingestion-key-limit-form"
|
||||
@@ -740,8 +869,8 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
form={addEditLimitForm}
|
||||
autoComplete="off"
|
||||
initialValues={{
|
||||
dailyLimit: bytesToGb(limits[signal]?.config?.day?.size),
|
||||
secondsLimit: bytesToGb(limits[signal]?.config?.second?.size),
|
||||
dailyLimit: bytesToGb(limit?.config?.day?.size || 0),
|
||||
secondsLimit: bytesToGb(limit?.config?.second?.size || 0),
|
||||
}}
|
||||
className="edit-ingestion-key-limit-form"
|
||||
>
|
||||
@@ -756,16 +885,20 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
size="small"
|
||||
checked={activeSignal?.config?.day?.enabled}
|
||||
onChange={(value): void => {
|
||||
setActiveSignal({
|
||||
...activeSignal,
|
||||
config: {
|
||||
...activeSignal.config,
|
||||
day: {
|
||||
...activeSignal.config?.day,
|
||||
enabled: value,
|
||||
},
|
||||
},
|
||||
});
|
||||
setActiveSignal((prev) =>
|
||||
prev
|
||||
? {
|
||||
...prev,
|
||||
config: {
|
||||
...prev.config,
|
||||
day: {
|
||||
...prev.config?.day,
|
||||
enabled: value,
|
||||
},
|
||||
},
|
||||
}
|
||||
: null,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
@@ -775,50 +908,87 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
Add a limit for data ingested daily
|
||||
</div>
|
||||
</div>
|
||||
<div className="size">
|
||||
{activeSignal?.config?.day?.enabled ? (
|
||||
<Form.Item name="dailyLimit" key="dailyLimit">
|
||||
<InputNumber
|
||||
disabled={!activeSignal?.config?.day?.enabled}
|
||||
key="dailyLimit"
|
||||
addonAfter={
|
||||
<Select defaultValue="GiB" disabled>
|
||||
<Option value="TiB"> TiB</Option>
|
||||
<Option value="GiB"> GiB</Option>
|
||||
<Option value="MiB"> MiB </Option>
|
||||
<Option value="KiB"> KiB </Option>
|
||||
</Select>
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
) : (
|
||||
<div className="no-limit">
|
||||
<Infinity size={16} /> NO LIMIT
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{signalCfg.usesSize && (
|
||||
<div className="size">
|
||||
{activeSignal?.config?.day?.enabled ? (
|
||||
<Form.Item name="dailyLimit" key="dailyLimit">
|
||||
<InputNumber
|
||||
disabled={!activeSignal?.config?.day?.enabled}
|
||||
addonAfter={
|
||||
<Select defaultValue="GiB" disabled>
|
||||
<Option value="TiB">TiB</Option>
|
||||
<Option value="GiB">GiB</Option>
|
||||
<Option value="MiB">MiB</Option>
|
||||
<Option value="KiB">KiB</Option>
|
||||
</Select>
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
) : (
|
||||
<div className="no-limit">
|
||||
<Infinity size={16} /> NO LIMIT
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{signalCfg.usesCount && (
|
||||
<div className="count">
|
||||
{activeSignal?.config?.day?.enabled ? (
|
||||
<Form.Item name="dailyCount" key="dailyCount">
|
||||
<InputNumber
|
||||
placeholder="Enter max # of samples/day"
|
||||
addonAfter={
|
||||
<Form.Item
|
||||
name="dailyCountUnit"
|
||||
noStyle
|
||||
initialValue="million"
|
||||
>
|
||||
<Select
|
||||
style={{
|
||||
width: 90,
|
||||
}}
|
||||
>
|
||||
<Option value="thousand">Thousand</Option>
|
||||
<Option value="million">Million</Option>
|
||||
<Option value="billion">Billion</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
) : (
|
||||
<div className="no-limit">
|
||||
<Infinity size={16} /> NO LIMIT
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="second-limit">
|
||||
<div className="heading">
|
||||
<div className="title">
|
||||
Per Second limit{' '}
|
||||
Per Second limit
|
||||
<div className="limit-enable-disable-toggle">
|
||||
<Form.Item name="enableSecondLimit">
|
||||
<Switch
|
||||
size="small"
|
||||
checked={activeSignal?.config?.second?.enabled}
|
||||
onChange={(value): void => {
|
||||
setActiveSignal({
|
||||
...activeSignal,
|
||||
config: {
|
||||
...activeSignal.config,
|
||||
second: {
|
||||
...activeSignal.config?.second,
|
||||
enabled: value,
|
||||
},
|
||||
},
|
||||
});
|
||||
setActiveSignal((prev) =>
|
||||
prev
|
||||
? {
|
||||
...prev,
|
||||
config: {
|
||||
...prev.config,
|
||||
second: {
|
||||
...prev.config?.second,
|
||||
enabled: value,
|
||||
},
|
||||
},
|
||||
}
|
||||
: null,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
@@ -828,37 +998,68 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
Add a limit for data ingested every second
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="size">
|
||||
{activeSignal?.config?.second?.enabled ? (
|
||||
<Form.Item name="secondsLimit" key="secondsLimit">
|
||||
<InputNumber
|
||||
key="secondsLimit"
|
||||
disabled={!activeSignal?.config?.second?.enabled}
|
||||
addonAfter={
|
||||
<Select defaultValue="GiB" disabled>
|
||||
<Option value="TiB"> TiB</Option>
|
||||
<Option value="GiB"> GiB</Option>
|
||||
<Option value="MiB"> MiB </Option>
|
||||
<Option value="KiB"> KiB </Option>
|
||||
</Select>
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
) : (
|
||||
<div className="no-limit">
|
||||
<Infinity size={16} /> NO LIMIT
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{signalCfg.usesSize && (
|
||||
<div className="size">
|
||||
{activeSignal?.config?.second?.enabled ? (
|
||||
<Form.Item name="secondsLimit" key="secondsLimit">
|
||||
<InputNumber
|
||||
disabled={!activeSignal?.config?.second?.enabled}
|
||||
addonAfter={
|
||||
<Select defaultValue="GiB" disabled>
|
||||
<Option value="TiB">TiB</Option>
|
||||
<Option value="GiB">GiB</Option>
|
||||
<Option value="MiB">MiB</Option>
|
||||
<Option value="KiB">KiB</Option>
|
||||
</Select>
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
) : (
|
||||
<div className="no-limit">
|
||||
<Infinity size={16} /> NO LIMIT
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{signalCfg.usesCount && (
|
||||
<div className="count">
|
||||
{activeSignal?.config?.second?.enabled ? (
|
||||
<Form.Item name="secondsCount" key="secondsCount">
|
||||
<InputNumber
|
||||
placeholder="Enter max # of samples/s"
|
||||
addonAfter={
|
||||
<Form.Item
|
||||
name="secondsCountUnit"
|
||||
noStyle
|
||||
initialValue="million"
|
||||
>
|
||||
<Select
|
||||
style={{
|
||||
width: 90,
|
||||
}}
|
||||
>
|
||||
<Option value="thousand">Thousand</Option>
|
||||
<Option value="million">Million</Option>
|
||||
<Option value="billion">Billion</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
) : (
|
||||
<div className="no-limit">
|
||||
<Infinity size={16} /> NO LIMIT
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{activeAPIKey?.id === APIKey.id &&
|
||||
activeSignal.signal === signal &&
|
||||
activeSignal.signal === signalName &&
|
||||
!isLoadingLimitForKey &&
|
||||
hasCreateLimitForIngestionKeyError &&
|
||||
createLimitForIngestionKeyError &&
|
||||
createLimitForIngestionKeyError?.error && (
|
||||
<div className="error">
|
||||
{createLimitForIngestionKeyError?.error}
|
||||
@@ -866,17 +1067,17 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
)}
|
||||
|
||||
{activeAPIKey?.id === APIKey.id &&
|
||||
activeSignal.signal === signal &&
|
||||
activeSignal.signal === signalName &&
|
||||
!isLoadingLimitForKey &&
|
||||
hasUpdateLimitForIngestionKeyError &&
|
||||
updateLimitForIngestionKeyError && (
|
||||
updateLimitForIngestionKeyError?.error && (
|
||||
<div className="error">
|
||||
{updateLimitForIngestionKeyError?.error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeAPIKey?.id === APIKey.id &&
|
||||
activeSignal.signal === signal &&
|
||||
activeSignal.signal === signalName &&
|
||||
isEditAddLimitOpen && (
|
||||
<div className="signal-limit-save-discard">
|
||||
<Button
|
||||
@@ -890,10 +1091,10 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
isLoadingLimitForKey || isLoadingUpdatedLimitForKey
|
||||
}
|
||||
onClick={(): void => {
|
||||
if (!hasLimits(signal)) {
|
||||
handleAddLimit(APIKey, signal);
|
||||
if (!hasLimits(signalName)) {
|
||||
handleAddLimit(APIKey, signalName);
|
||||
} else {
|
||||
handleUpdateLimit(APIKey, limits[signal]);
|
||||
handleUpdateLimit(APIKey, limitsDict[signalName]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -915,55 +1116,99 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
</Form>
|
||||
) : (
|
||||
<div className="signal-limit-view-mode">
|
||||
{/* DAILY limit usage/limit */}
|
||||
<div className="signal-limit-value">
|
||||
<div className="limit-type">
|
||||
Daily <Minus size={16} />{' '}
|
||||
Daily <Minus size={16} />
|
||||
</div>
|
||||
|
||||
<div className="limit-value">
|
||||
{hasValidDayLimit ? (
|
||||
<>
|
||||
{getYAxisFormattedValue(
|
||||
(limits[signal]?.metric?.day?.size || 0).toString(),
|
||||
'bytes',
|
||||
)}{' '}
|
||||
/{' '}
|
||||
{getYAxisFormattedValue(
|
||||
(limits[signal]?.config?.day?.size || 0).toString(),
|
||||
'bytes',
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Infinity size={16} /> NO LIMIT
|
||||
</>
|
||||
)}
|
||||
{/* Size (if usesSize) */}
|
||||
{signalCfg.usesSize &&
|
||||
(hasValidDayLimit &&
|
||||
limit?.config?.day?.size !== undefined ? (
|
||||
<>
|
||||
{getYAxisFormattedValue(
|
||||
(limit?.metric?.day?.size || 0).toString(),
|
||||
'bytes',
|
||||
)}{' '}
|
||||
/{' '}
|
||||
{getYAxisFormattedValue(
|
||||
(limit?.config?.day?.size || 0).toString(),
|
||||
'bytes',
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Infinity size={16} /> NO LIMIT
|
||||
</>
|
||||
))}
|
||||
|
||||
{/* Count (if usesCount) */}
|
||||
{signalCfg.usesCount &&
|
||||
(limit?.config?.day?.count !== undefined ? (
|
||||
<div style={{ marginTop: 4 }}>
|
||||
{countToUnit(
|
||||
limit?.metric?.day?.count || 0,
|
||||
).value.toFixed(2)}{' '}
|
||||
{countToUnit(limit?.metric?.day?.count || 0).unit} /{' '}
|
||||
{countToUnit(
|
||||
limit?.config?.day?.count || 0,
|
||||
).value.toFixed(2)}{' '}
|
||||
{countToUnit(limit?.config?.day?.count || 0).unit}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Infinity size={16} /> NO LIMIT
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* SECOND limit usage/limit */}
|
||||
<div className="signal-limit-value">
|
||||
<div className="limit-type">
|
||||
Seconds <Minus size={16} />
|
||||
</div>
|
||||
|
||||
<div className="limit-value">
|
||||
{hasValidSecondLimit ? (
|
||||
<>
|
||||
{getYAxisFormattedValue(
|
||||
(limits[signal]?.metric?.second?.size || 0).toString(),
|
||||
'bytes',
|
||||
)}{' '}
|
||||
/{' '}
|
||||
{getYAxisFormattedValue(
|
||||
(limits[signal]?.config?.second?.size || 0).toString(),
|
||||
'bytes',
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Infinity size={16} /> NO LIMIT
|
||||
</>
|
||||
)}
|
||||
{/* Size (if usesSize) */}
|
||||
{signalCfg.usesSize &&
|
||||
(hasValidSecondLimit &&
|
||||
limit?.config?.second?.size !== undefined ? (
|
||||
<>
|
||||
{getYAxisFormattedValue(
|
||||
(limit?.metric?.second?.size || 0).toString(),
|
||||
'bytes',
|
||||
)}{' '}
|
||||
/{' '}
|
||||
{getYAxisFormattedValue(
|
||||
(limit?.config?.second?.size || 0).toString(),
|
||||
'bytes',
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Infinity size={16} /> NO LIMIT
|
||||
</>
|
||||
))}
|
||||
|
||||
{/* Count (if usesCount) */}
|
||||
{signalCfg.usesCount &&
|
||||
(limit?.config?.second?.count !== undefined ? (
|
||||
<div style={{ marginTop: 4 }}>
|
||||
{countToUnit(
|
||||
limit?.metric?.second?.count || 0,
|
||||
).value.toFixed(2)}{' '}
|
||||
{countToUnit(limit?.metric?.second?.count || 0).unit} /{' '}
|
||||
{countToUnit(
|
||||
limit?.config?.second?.count || 0,
|
||||
).value.toFixed(2)}{' '}
|
||||
{countToUnit(limit?.config?.second?.count || 0).unit}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Infinity size={16} /> NO LIMIT
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1033,7 +1278,6 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
className="learn-more"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{' '}
|
||||
Learn more <ArrowUpRight size={14} />
|
||||
</a>
|
||||
</Typography.Text>
|
||||
|
||||
@@ -8,7 +8,7 @@ import RouteTab from 'components/RouteTab';
|
||||
import Spinner from 'components/Spinner';
|
||||
import ROUTES from 'constants/routes';
|
||||
import history from 'lib/history';
|
||||
import { useMemo } from 'react';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
@@ -80,6 +80,11 @@ function AlertDetails(): JSX.Element {
|
||||
alertDetailsResponse,
|
||||
} = useGetAlertRuleDetails();
|
||||
|
||||
useEffect(() => {
|
||||
const alertTitle = alertDetailsResponse?.payload?.data.alert;
|
||||
document.title = alertTitle || document.title;
|
||||
}, [alertDetailsResponse?.payload?.data.alert, isRefetching]);
|
||||
|
||||
if (
|
||||
isError ||
|
||||
!isValidRuleId ||
|
||||
|
||||
@@ -4,6 +4,7 @@ import NotFound from 'components/NotFound';
|
||||
import Spinner from 'components/Spinner';
|
||||
import NewDashboard from 'container/NewDashboard';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useEffect } from 'react';
|
||||
import { ErrorType } from 'types/common';
|
||||
|
||||
function DashboardPage(): JSX.Element {
|
||||
@@ -17,6 +18,11 @@ function DashboardPage(): JSX.Element {
|
||||
(dashboardResponse?.error as AxiosError)?.response?.data?.errorType
|
||||
: 'Something went wrong';
|
||||
|
||||
useEffect(() => {
|
||||
const dashboardTitle = dashboardResponse.data?.data.title;
|
||||
document.title = dashboardTitle || document.title;
|
||||
}, [dashboardResponse.data?.data.title, isFetching]);
|
||||
|
||||
if (isError && !isFetching && errorMessage === ErrorType.NotFound) {
|
||||
return <NotFound />;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
export interface LimitConfig {
|
||||
size?: number;
|
||||
count?: number; // mainly used for metrics
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface LimitSettings {
|
||||
day?: LimitConfig;
|
||||
second?: LimitConfig;
|
||||
}
|
||||
|
||||
export interface LimitProps {
|
||||
id: string;
|
||||
signal: string;
|
||||
@@ -5,56 +16,20 @@ export interface LimitProps {
|
||||
key_id?: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
config?: {
|
||||
day?: {
|
||||
size?: number;
|
||||
enabled?: boolean;
|
||||
};
|
||||
second?: {
|
||||
size?: number;
|
||||
enabled?: boolean;
|
||||
};
|
||||
};
|
||||
metric?: {
|
||||
day?: {
|
||||
size?: number;
|
||||
enabled?: boolean;
|
||||
};
|
||||
second?: {
|
||||
size?: number;
|
||||
enabled?: boolean;
|
||||
};
|
||||
};
|
||||
config?: LimitSettings;
|
||||
metric?: LimitSettings;
|
||||
}
|
||||
|
||||
export interface AddLimitProps {
|
||||
keyID: string;
|
||||
signal: string;
|
||||
config: {
|
||||
day?: {
|
||||
size?: number;
|
||||
enabled?: boolean;
|
||||
};
|
||||
second?: {
|
||||
size?: number;
|
||||
enabled?: boolean;
|
||||
};
|
||||
};
|
||||
config: LimitSettings;
|
||||
}
|
||||
|
||||
export interface UpdateLimitProps {
|
||||
limitID: string;
|
||||
signal: string;
|
||||
config: {
|
||||
day?: {
|
||||
size?: number;
|
||||
enabled?: boolean;
|
||||
};
|
||||
second?: {
|
||||
size?: number;
|
||||
enabled?: boolean;
|
||||
};
|
||||
};
|
||||
config: LimitSettings;
|
||||
}
|
||||
|
||||
export interface LimitSuccessProps {
|
||||
|
||||
14
go.mod
14
go.mod
@@ -20,6 +20,7 @@ require (
|
||||
github.com/go-kit/log v0.2.1
|
||||
github.com/go-redis/redis/v8 v8.11.5
|
||||
github.com/go-redis/redismock/v8 v8.11.5
|
||||
github.com/go-viper/mapstructure/v2 v2.1.0
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/handlers v1.5.1
|
||||
@@ -29,6 +30,7 @@ require (
|
||||
github.com/jmoiron/sqlx v1.3.4
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/knadh/koanf v1.5.0
|
||||
github.com/knadh/koanf/v2 v2.1.1
|
||||
github.com/mailru/easyjson v0.7.7
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible
|
||||
github.com/oklog/oklog v0.3.2
|
||||
@@ -48,6 +50,8 @@ require (
|
||||
github.com/soheilhy/cmux v0.1.5
|
||||
github.com/srikanthccv/ClickHouse-go-mock v0.9.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/uptrace/bun v1.2.8
|
||||
github.com/uptrace/bun/dialect/sqlitedialect v1.2.8
|
||||
go.opentelemetry.io/collector/confmap v1.17.0
|
||||
go.opentelemetry.io/collector/pdata v1.17.0
|
||||
go.opentelemetry.io/collector/processor v0.111.0
|
||||
@@ -99,6 +103,7 @@ require (
|
||||
github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/form3tech-oss/jwt-go v3.2.5+incompatible // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/go-faster/city v1.0.1 // indirect
|
||||
github.com/go-faster/errors v0.7.1 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.2 // indirect
|
||||
@@ -106,7 +111,6 @@ require (
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.1.0 // indirect
|
||||
github.com/goccy/go-json v0.10.3 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
|
||||
@@ -120,13 +124,13 @@ require (
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect
|
||||
github.com/hashicorp/go-version v1.7.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/jonboulle/clockwork v0.4.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/jpillora/backoff v1.0.0 // indirect
|
||||
github.com/jtolds/gls v4.20.0+incompatible // indirect
|
||||
github.com/klauspost/compress v1.17.10 // indirect
|
||||
github.com/knadh/koanf/v2 v2.1.1 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/leodido/go-syslog/v4 v4.2.0 // indirect
|
||||
github.com/leodido/ragel-machinery v0.0.0-20190525184631-5f46317e436b // indirect
|
||||
@@ -151,6 +155,7 @@ require (
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common/sigv4 v0.1.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
github.com/segmentio/asm v1.2.0 // indirect
|
||||
github.com/segmentio/backo-go v1.0.1 // indirect
|
||||
@@ -162,8 +167,11 @@ require (
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.13 // indirect
|
||||
github.com/tklauser/numcpus v0.7.0 // indirect
|
||||
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
|
||||
github.com/valyala/fastjson v1.6.4 // indirect
|
||||
github.com/vjeantet/grok v1.0.1 // indirect
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
@@ -212,7 +220,7 @@ require (
|
||||
go.opentelemetry.io/otel/sdk/metric v1.30.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
golang.org/x/time v0.6.0 // indirect
|
||||
gonum.org/v1/gonum v0.15.1 // indirect
|
||||
google.golang.org/api v0.199.0 // indirect
|
||||
|
||||
18
go.sum
18
go.sum
@@ -436,6 +436,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/ionos-cloud/sdk-go/v6 v6.2.1 h1:mxxN+frNVmbFrmmFfXnBC3g2USYJrl6mc1LW2iNYbFY=
|
||||
github.com/ionos-cloud/sdk-go/v6 v6.2.1/go.mod h1:SXrO9OGyWjd2rZhAhEpdYN6VUAODzzqRdqA9BCviQtI=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||
@@ -661,6 +663,8 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O
|
||||
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
@@ -740,12 +744,22 @@ github.com/tklauser/go-sysconf v0.3.13 h1:GBUpcahXSpR2xN01jhkNAbTLRk2Yzgggk8IM08
|
||||
github.com/tklauser/go-sysconf v0.3.13/go.mod h1:zwleP4Q4OehZHGn4CYZDipCgg9usW5IJePewFCGVEa0=
|
||||
github.com/tklauser/numcpus v0.7.0 h1:yjuerZP127QG9m5Zh/mSO4wqurYil27tHrqwRoRjpr4=
|
||||
github.com/tklauser/numcpus v0.7.0/go.mod h1:bb6dMVcj8A42tSE7i32fsIUCbQNllK5iDguyOZRUzAY=
|
||||
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
|
||||
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
|
||||
github.com/uptrace/bun v1.2.8 h1:HEiLvy9wc7ehU5S02+O6NdV5BLz48lL4REPhTkMX3Dg=
|
||||
github.com/uptrace/bun v1.2.8/go.mod h1:JBq0uBKsKqNT0Ccce1IAFZY337Wkf08c6F6qlmfOHE8=
|
||||
github.com/uptrace/bun/dialect/sqlitedialect v1.2.8 h1:Huqw7YhLFTbocbSv8NETYYXqKtwLa6XsciCWtjzWSWU=
|
||||
github.com/uptrace/bun/dialect/sqlitedialect v1.2.8/go.mod h1:ni7h2uwIc5zPhxgmCMTEbefONc4XsVr/ATfz1Q7d3CE=
|
||||
github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc=
|
||||
github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
|
||||
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
|
||||
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
|
||||
github.com/vjeantet/grok v1.0.1 h1:2rhIR7J4gThTgcZ1m2JY4TrJZNgjn985U28kT2wQrJ4=
|
||||
github.com/vjeantet/grok v1.0.1/go.mod h1:ax1aAchzC6/QMXMcyzHQGZWaW1l195+uMYIkCWPCNIo=
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
||||
github.com/vultr/govultr/v2 v2.17.2 h1:gej/rwr91Puc/tgh+j33p/BLR16UrIPnSr+AIwYWZQs=
|
||||
github.com/vultr/govultr/v2 v2.17.2/go.mod h1:ZFOKGWmgjytfyjeyAdhQlSWwTjh2ig+X49cAp50dzXI=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
@@ -1086,8 +1100,8 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package memory
|
||||
package memorycache
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -10,21 +10,21 @@ import (
|
||||
_cache "go.signoz.io/signoz/pkg/cache"
|
||||
)
|
||||
|
||||
type cache struct {
|
||||
type provider struct {
|
||||
cc *go_cache.Cache
|
||||
}
|
||||
|
||||
func New(opts *_cache.Memory) *cache {
|
||||
return &cache{cc: go_cache.New(opts.TTL, opts.CleanupInterval)}
|
||||
func New(opts *_cache.Memory) *provider {
|
||||
return &provider{cc: go_cache.New(opts.TTL, opts.CleanupInterval)}
|
||||
}
|
||||
|
||||
// Connect does nothing
|
||||
func (c *cache) Connect(_ context.Context) error {
|
||||
func (c *provider) Connect(_ context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Store stores the data in the cache
|
||||
func (c *cache) Store(_ context.Context, cacheKey string, data _cache.CacheableEntity, ttl time.Duration) error {
|
||||
func (c *provider) Store(_ context.Context, cacheKey string, data _cache.CacheableEntity, ttl time.Duration) error {
|
||||
// check if the data being passed is a pointer and is not nil
|
||||
rv := reflect.ValueOf(data)
|
||||
if rv.Kind() != reflect.Pointer || rv.IsNil() {
|
||||
@@ -36,7 +36,7 @@ func (c *cache) Store(_ context.Context, cacheKey string, data _cache.CacheableE
|
||||
}
|
||||
|
||||
// Retrieve retrieves the data from the cache
|
||||
func (c *cache) Retrieve(_ context.Context, cacheKey string, dest _cache.CacheableEntity, allowExpired bool) (_cache.RetrieveStatus, error) {
|
||||
func (c *provider) Retrieve(_ context.Context, cacheKey string, dest _cache.CacheableEntity, allowExpired bool) (_cache.RetrieveStatus, error) {
|
||||
// check if the destination being passed is a pointer and is not nil
|
||||
dstv := reflect.ValueOf(dest)
|
||||
if dstv.Kind() != reflect.Pointer || dstv.IsNil() {
|
||||
@@ -65,7 +65,7 @@ func (c *cache) Retrieve(_ context.Context, cacheKey string, dest _cache.Cacheab
|
||||
}
|
||||
|
||||
// SetTTL sets the TTL for the cache entry
|
||||
func (c *cache) SetTTL(_ context.Context, cacheKey string, ttl time.Duration) {
|
||||
func (c *provider) SetTTL(_ context.Context, cacheKey string, ttl time.Duration) {
|
||||
item, found := c.cc.Get(cacheKey)
|
||||
if !found {
|
||||
return
|
||||
@@ -74,23 +74,23 @@ func (c *cache) SetTTL(_ context.Context, cacheKey string, ttl time.Duration) {
|
||||
}
|
||||
|
||||
// Remove removes the cache entry
|
||||
func (c *cache) Remove(_ context.Context, cacheKey string) {
|
||||
func (c *provider) Remove(_ context.Context, cacheKey string) {
|
||||
c.cc.Delete(cacheKey)
|
||||
}
|
||||
|
||||
// BulkRemove removes the cache entries
|
||||
func (c *cache) BulkRemove(_ context.Context, cacheKeys []string) {
|
||||
func (c *provider) BulkRemove(_ context.Context, cacheKeys []string) {
|
||||
for _, cacheKey := range cacheKeys {
|
||||
c.cc.Delete(cacheKey)
|
||||
}
|
||||
}
|
||||
|
||||
// Close does nothing
|
||||
func (c *cache) Close(_ context.Context) error {
|
||||
func (c *provider) Close(_ context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Configuration returns the cache configuration
|
||||
func (c *cache) Configuration() *_cache.Memory {
|
||||
func (c *provider) Configuration() *_cache.Memory {
|
||||
return nil
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package memory
|
||||
package memorycache
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package redis
|
||||
package rediscache
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -11,22 +11,22 @@ import (
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type cache struct {
|
||||
type provider struct {
|
||||
client *redis.Client
|
||||
opts *_cache.Redis
|
||||
}
|
||||
|
||||
func New(opts *_cache.Redis) *cache {
|
||||
return &cache{opts: opts}
|
||||
func New(opts *_cache.Redis) *provider {
|
||||
return &provider{opts: opts}
|
||||
}
|
||||
|
||||
// WithClient creates a new cache with the given client
|
||||
func WithClient(client *redis.Client) *cache {
|
||||
return &cache{client: client}
|
||||
func WithClient(client *redis.Client) *provider {
|
||||
return &provider{client: client}
|
||||
}
|
||||
|
||||
// Connect connects to the redis server
|
||||
func (c *cache) Connect(_ context.Context) error {
|
||||
func (c *provider) Connect(_ context.Context) error {
|
||||
c.client = redis.NewClient(&redis.Options{
|
||||
Addr: fmt.Sprintf("%s:%d", c.opts.Host, c.opts.Port),
|
||||
Password: c.opts.Password,
|
||||
@@ -36,12 +36,12 @@ func (c *cache) Connect(_ context.Context) error {
|
||||
}
|
||||
|
||||
// Store stores the data in the cache
|
||||
func (c *cache) Store(ctx context.Context, cacheKey string, data _cache.CacheableEntity, ttl time.Duration) error {
|
||||
func (c *provider) Store(ctx context.Context, cacheKey string, data _cache.CacheableEntity, ttl time.Duration) error {
|
||||
return c.client.Set(ctx, cacheKey, data, ttl).Err()
|
||||
}
|
||||
|
||||
// Retrieve retrieves the data from the cache
|
||||
func (c *cache) Retrieve(ctx context.Context, cacheKey string, dest _cache.CacheableEntity, allowExpired bool) (_cache.RetrieveStatus, error) {
|
||||
func (c *provider) Retrieve(ctx context.Context, cacheKey string, dest _cache.CacheableEntity, allowExpired bool) (_cache.RetrieveStatus, error) {
|
||||
err := c.client.Get(ctx, cacheKey).Scan(dest)
|
||||
if err != nil {
|
||||
if errors.Is(err, redis.Nil) {
|
||||
@@ -53,7 +53,7 @@ func (c *cache) Retrieve(ctx context.Context, cacheKey string, dest _cache.Cache
|
||||
}
|
||||
|
||||
// SetTTL sets the TTL for the cache entry
|
||||
func (c *cache) SetTTL(ctx context.Context, cacheKey string, ttl time.Duration) {
|
||||
func (c *provider) SetTTL(ctx context.Context, cacheKey string, ttl time.Duration) {
|
||||
err := c.client.Expire(ctx, cacheKey, ttl).Err()
|
||||
if err != nil {
|
||||
zap.L().Error("error setting TTL for cache key", zap.String("cacheKey", cacheKey), zap.Duration("ttl", ttl), zap.Error(err))
|
||||
@@ -61,39 +61,39 @@ func (c *cache) SetTTL(ctx context.Context, cacheKey string, ttl time.Duration)
|
||||
}
|
||||
|
||||
// Remove removes the cache entry
|
||||
func (c *cache) Remove(ctx context.Context, cacheKey string) {
|
||||
func (c *provider) Remove(ctx context.Context, cacheKey string) {
|
||||
c.BulkRemove(ctx, []string{cacheKey})
|
||||
}
|
||||
|
||||
// BulkRemove removes the cache entries
|
||||
func (c *cache) BulkRemove(ctx context.Context, cacheKeys []string) {
|
||||
func (c *provider) BulkRemove(ctx context.Context, cacheKeys []string) {
|
||||
if err := c.client.Del(ctx, cacheKeys...).Err(); err != nil {
|
||||
zap.L().Error("error deleting cache keys", zap.Strings("cacheKeys", cacheKeys), zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes the connection to the redis server
|
||||
func (c *cache) Close(_ context.Context) error {
|
||||
func (c *provider) Close(_ context.Context) error {
|
||||
return c.client.Close()
|
||||
}
|
||||
|
||||
// Ping pings the redis server
|
||||
func (c *cache) Ping(ctx context.Context) error {
|
||||
func (c *provider) Ping(ctx context.Context) error {
|
||||
return c.client.Ping(ctx).Err()
|
||||
}
|
||||
|
||||
// GetClient returns the redis client
|
||||
func (c *cache) GetClient() *redis.Client {
|
||||
func (c *provider) GetClient() *redis.Client {
|
||||
return c.client
|
||||
}
|
||||
|
||||
// GetOptions returns the options
|
||||
func (c *cache) GetOptions() *_cache.Redis {
|
||||
func (c *provider) GetOptions() *_cache.Redis {
|
||||
return c.opts
|
||||
}
|
||||
|
||||
// GetTTL returns the TTL for the cache entry
|
||||
func (c *cache) GetTTL(ctx context.Context, cacheKey string) time.Duration {
|
||||
func (c *provider) GetTTL(ctx context.Context, cacheKey string) time.Duration {
|
||||
ttl, err := c.client.TTL(ctx, cacheKey).Result()
|
||||
if err != nil {
|
||||
zap.L().Error("error getting TTL for cache key", zap.String("cacheKey", cacheKey), zap.Error(err))
|
||||
@@ -102,12 +102,12 @@ func (c *cache) GetTTL(ctx context.Context, cacheKey string) time.Duration {
|
||||
}
|
||||
|
||||
// GetKeys returns the keys matching the pattern
|
||||
func (c *cache) GetKeys(ctx context.Context, pattern string) ([]string, error) {
|
||||
func (c *provider) GetKeys(ctx context.Context, pattern string) ([]string, error) {
|
||||
return c.client.Keys(ctx, pattern).Result()
|
||||
}
|
||||
|
||||
// GetKeysWithTTL returns the keys matching the pattern with their TTL
|
||||
func (c *cache) GetKeysWithTTL(ctx context.Context, pattern string) (map[string]time.Duration, error) {
|
||||
func (c *provider) GetKeysWithTTL(ctx context.Context, pattern string) (map[string]time.Duration, error) {
|
||||
keys, err := c.GetKeys(ctx, pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1,4 +1,4 @@
|
||||
package redis
|
||||
package rediscache
|
||||
|
||||
import (
|
||||
"context"
|
||||
90
pkg/config/conf.go
Normal file
90
pkg/config/conf.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/go-viper/mapstructure/v2"
|
||||
"github.com/knadh/koanf/providers/confmap"
|
||||
"github.com/knadh/koanf/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
KoanfDelimiter string = "::"
|
||||
)
|
||||
|
||||
// Conf is a wrapper around the koanf library.
|
||||
type Conf struct {
|
||||
*koanf.Koanf
|
||||
}
|
||||
|
||||
// NewConf creates a new Conf instance.
|
||||
func NewConf() *Conf {
|
||||
return &Conf{koanf.New(KoanfDelimiter)}
|
||||
}
|
||||
|
||||
// NewConfFromMap creates a new Conf instance from a map.
|
||||
func NewConfFromMap(m map[string]any) (*Conf, error) {
|
||||
conf := NewConf()
|
||||
if err := conf.Koanf.Load(confmap.Provider(m, KoanfDelimiter), nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
// MustNewConfFromMap creates a new Conf instance from a map.
|
||||
// It panics if the conf cannot be created.
|
||||
func MustNewConfFromMap(m map[string]any) *Conf {
|
||||
conf, err := NewConfFromMap(m)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return conf
|
||||
}
|
||||
|
||||
// Merge merges the current configuration with the input configuration.
|
||||
func (conf *Conf) Merge(input *Conf) error {
|
||||
return conf.Koanf.Merge(input.Koanf)
|
||||
}
|
||||
|
||||
// Merge merges the current configuration with the input configuration.
|
||||
func (conf *Conf) MergeAt(input *Conf, path string) error {
|
||||
return conf.Koanf.MergeAt(input.Koanf, path)
|
||||
}
|
||||
|
||||
// Unmarshal unmarshals the configuration at the given path into the input.
|
||||
// It uses a WeaklyTypedInput to allow for more flexible unmarshalling.
|
||||
func (conf *Conf) Unmarshal(path string, input any) error {
|
||||
dc := &mapstructure.DecoderConfig{
|
||||
TagName: "mapstructure",
|
||||
WeaklyTypedInput: true,
|
||||
DecodeHook: mapstructure.ComposeDecodeHookFunc(
|
||||
mapstructure.StringToSliceHookFunc(","),
|
||||
mapstructure.StringToTimeDurationHookFunc(),
|
||||
mapstructure.TextUnmarshallerHookFunc(),
|
||||
),
|
||||
Result: input,
|
||||
}
|
||||
|
||||
return conf.Koanf.UnmarshalWithConf(path, input, koanf.UnmarshalConf{Tag: "mapstructure", DecoderConfig: dc})
|
||||
}
|
||||
|
||||
// Set sets the configuration at the given key.
|
||||
// It decodes the input into a map as per mapstructure.Decode and then merges it into the configuration.
|
||||
func (conf *Conf) Set(key string, input any) error {
|
||||
m := map[string]any{}
|
||||
err := mapstructure.Decode(input, &m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newConf := NewConf()
|
||||
if err := newConf.Koanf.Load(confmap.Provider(m, KoanfDelimiter), nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := conf.Koanf.MergeAt(newConf.Koanf, key); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
38
pkg/config/conf_test.go
Normal file
38
pkg/config/conf_test.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestConfMerge(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
conf *Conf
|
||||
input *Conf
|
||||
expected *Conf
|
||||
pass bool
|
||||
}{
|
||||
{name: "Empty", conf: NewConf(), input: NewConf(), expected: NewConf(), pass: true},
|
||||
{name: "Merge", conf: MustNewConfFromMap(map[string]any{"a": "b"}), input: MustNewConfFromMap(map[string]any{"c": "d"}), expected: MustNewConfFromMap(map[string]any{"a": "b", "c": "d"}), pass: true},
|
||||
{name: "NestedMerge", conf: MustNewConfFromMap(map[string]any{"a": map[string]any{"b": "v1", "c": "v2"}}), input: MustNewConfFromMap(map[string]any{"a": map[string]any{"d": "v1", "e": "v2"}}), expected: MustNewConfFromMap(map[string]any{"a": map[string]any{"b": "v1", "c": "v2", "d": "v1", "e": "v2"}}), pass: true},
|
||||
{name: "Override", conf: MustNewConfFromMap(map[string]any{"a": "b"}), input: MustNewConfFromMap(map[string]any{"a": "c"}), expected: MustNewConfFromMap(map[string]any{"a": "c"}), pass: true},
|
||||
}
|
||||
|
||||
t.Parallel()
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := tc.conf.Merge(tc.input)
|
||||
if !tc.pass {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.expected.Raw(), tc.conf.Raw())
|
||||
assert.Equal(t, tc.expected.Raw(), tc.conf.Raw())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3,33 +3,34 @@ package config
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.signoz.io/signoz/pkg/cache"
|
||||
signozconfmap "go.signoz.io/signoz/pkg/confmap"
|
||||
"go.signoz.io/signoz/pkg/instrumentation"
|
||||
"go.signoz.io/signoz/pkg/web"
|
||||
"go.signoz.io/signoz/pkg/factory"
|
||||
)
|
||||
|
||||
// This map contains the default values of all config structs
|
||||
var (
|
||||
defaults = map[string]signozconfmap.Config{
|
||||
"instrumentation": &instrumentation.Config{},
|
||||
"web": &web.Config{},
|
||||
"cache": &cache.Config{},
|
||||
}
|
||||
)
|
||||
|
||||
// Config defines the entire configuration of signoz.
|
||||
type Config struct {
|
||||
Instrumentation instrumentation.Config `mapstructure:"instrumentation"`
|
||||
Web web.Config `mapstructure:"web"`
|
||||
Cache cache.Config `mapstructure:"cache"`
|
||||
}
|
||||
|
||||
func New(ctx context.Context, settings ProviderSettings) (*Config, error) {
|
||||
provider, err := NewProvider(settings)
|
||||
func New(ctx context.Context, resolverConfig ResolverConfig, configFactories []factory.ConfigFactory) (*Conf, error) {
|
||||
// Get the config from the resolver
|
||||
resolver, err := NewResolver(resolverConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return provider.Get(ctx)
|
||||
resolvedConf, err := resolver.Do(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conf := NewConf()
|
||||
// Set the default configs
|
||||
for _, factory := range configFactories {
|
||||
c := factory.New()
|
||||
if err := conf.Set(factory.Name().String(), c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
err = conf.Merge(resolvedConf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.opentelemetry.io/collector/confmap"
|
||||
"go.signoz.io/signoz/pkg/cache"
|
||||
"go.signoz.io/signoz/pkg/confmap/provider/signozenvprovider"
|
||||
"go.signoz.io/signoz/pkg/web"
|
||||
)
|
||||
|
||||
func TestNewWithSignozEnvProvider(t *testing.T) {
|
||||
|
||||
t.Setenv("SIGNOZ__WEB__PREFIX", "/web")
|
||||
t.Setenv("SIGNOZ__WEB__DIRECTORY", "/build")
|
||||
t.Setenv("SIGNOZ__CACHE__PROVIDER", "redis")
|
||||
t.Setenv("SIGNOZ__CACHE__REDIS__HOST", "127.0.0.1")
|
||||
|
||||
config, err := New(context.Background(), ProviderSettings{
|
||||
ResolverSettings: confmap.ResolverSettings{
|
||||
URIs: []string{"signozenv:"},
|
||||
ProviderFactories: []confmap.ProviderFactory{
|
||||
signozenvprovider.NewFactory(),
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := &Config{
|
||||
Web: web.Config{
|
||||
Prefix: "/web",
|
||||
Directory: "/build",
|
||||
},
|
||||
Cache: cache.Config{
|
||||
Provider: "redis",
|
||||
Memory: cache.Memory{
|
||||
TTL: time.Duration(-1),
|
||||
CleanupInterval: 1 * time.Minute,
|
||||
},
|
||||
Redis: cache.Redis{
|
||||
Host: "127.0.0.1",
|
||||
Port: 6379,
|
||||
Password: "",
|
||||
DB: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, config)
|
||||
}
|
||||
71
pkg/config/envprovider/provider.go
Normal file
71
pkg/config/envprovider/provider.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package envprovider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
koanfenv "github.com/knadh/koanf/providers/env"
|
||||
"go.signoz.io/signoz/pkg/config"
|
||||
)
|
||||
|
||||
const (
|
||||
prefix string = "SIGNOZ_"
|
||||
scheme string = "env"
|
||||
)
|
||||
|
||||
type provider struct{}
|
||||
|
||||
func NewFactory() config.ProviderFactory {
|
||||
return config.NewProviderFactory(New)
|
||||
}
|
||||
|
||||
func New(config config.ProviderConfig) config.Provider {
|
||||
return &provider{}
|
||||
}
|
||||
|
||||
func (provider *provider) Scheme() string {
|
||||
return scheme
|
||||
}
|
||||
|
||||
func (provider *provider) Get(ctx context.Context, uri config.Uri) (*config.Conf, error) {
|
||||
conf := config.NewConf()
|
||||
err := conf.Load(
|
||||
koanfenv.Provider(
|
||||
prefix,
|
||||
// Do not set this to `_`. The correct delimiter is being set by the custom callback provided below.
|
||||
// Since this had to be passed, using `config.KoanfDelimiter` eliminates any possible side effect.
|
||||
config.KoanfDelimiter,
|
||||
func(s string) string {
|
||||
s = strings.ToLower(strings.TrimPrefix(s, prefix))
|
||||
return provider.cb(s, config.KoanfDelimiter)
|
||||
},
|
||||
),
|
||||
nil,
|
||||
)
|
||||
|
||||
return conf, err
|
||||
}
|
||||
|
||||
func (provider *provider) cb(s string, delim string) string {
|
||||
delims := []rune(delim)
|
||||
runes := []rune(s)
|
||||
result := make([]rune, 0, len(runes))
|
||||
|
||||
for i := 0; i < len(runes); i++ {
|
||||
// Check for double underscore pattern
|
||||
if i < len(runes)-1 && runes[i] == '_' && runes[i+1] == '_' {
|
||||
result = append(result, '_')
|
||||
i++ // Skip next underscore
|
||||
continue
|
||||
}
|
||||
|
||||
if runes[i] == '_' {
|
||||
result = append(result, delims...)
|
||||
continue
|
||||
}
|
||||
|
||||
result = append(result, runes[i])
|
||||
}
|
||||
|
||||
return string(result)
|
||||
}
|
||||
78
pkg/config/envprovider/provider_test.go
Normal file
78
pkg/config/envprovider/provider_test.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package envprovider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.signoz.io/signoz/pkg/config"
|
||||
)
|
||||
|
||||
func TestGetWithStrings(t *testing.T) {
|
||||
t.Setenv("SIGNOZ_K1_K2", "string")
|
||||
t.Setenv("SIGNOZ_K3__K4", "string")
|
||||
t.Setenv("SIGNOZ_K5__K6_K7__K8", "string")
|
||||
t.Setenv("SIGNOZ_K9___K10", "string")
|
||||
t.Setenv("SIGNOZ_K11____K12", "string")
|
||||
expected := map[string]any{
|
||||
"k1::k2": "string",
|
||||
"k3_k4": "string",
|
||||
"k5_k6::k7_k8": "string",
|
||||
"k9_::k10": "string",
|
||||
"k11__k12": "string",
|
||||
}
|
||||
|
||||
provider := New(config.ProviderConfig{})
|
||||
actual, err := provider.Get(context.Background(), config.MustNewUri("env:"))
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, expected, actual.All())
|
||||
}
|
||||
|
||||
func TestGetWithGoTypes(t *testing.T) {
|
||||
t.Setenv("SIGNOZ_BOOL", "true")
|
||||
t.Setenv("SIGNOZ_STRING", "string")
|
||||
t.Setenv("SIGNOZ_INT", "1")
|
||||
t.Setenv("SIGNOZ_SLICE", "[1,2]")
|
||||
expected := map[string]any{
|
||||
"bool": "true",
|
||||
"int": "1",
|
||||
"slice": "[1,2]",
|
||||
"string": "string",
|
||||
}
|
||||
|
||||
provider := New(config.ProviderConfig{})
|
||||
actual, err := provider.Get(context.Background(), config.MustNewUri("env:"))
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, expected, actual.All())
|
||||
}
|
||||
|
||||
func TestGetWithGoTypesWithUnmarshal(t *testing.T) {
|
||||
t.Setenv("SIGNOZ_BOOL", "true")
|
||||
t.Setenv("SIGNOZ_STRING", "string")
|
||||
t.Setenv("SIGNOZ_INT", "1")
|
||||
|
||||
type test struct {
|
||||
Bool bool `mapstructure:"bool"`
|
||||
String string `mapstructure:"string"`
|
||||
Int int `mapstructure:"int"`
|
||||
}
|
||||
|
||||
expected := test{
|
||||
Bool: true,
|
||||
String: "string",
|
||||
Int: 1,
|
||||
}
|
||||
|
||||
provider := New(config.ProviderConfig{})
|
||||
conf, err := provider.Get(context.Background(), config.MustNewUri("env:"))
|
||||
require.NoError(t, err)
|
||||
|
||||
actual := test{}
|
||||
err = conf.Unmarshal("", &actual)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, expected, actual)
|
||||
}
|
||||
34
pkg/config/fileprovider/provider.go
Normal file
34
pkg/config/fileprovider/provider.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package fileprovider
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
koanfyaml "github.com/knadh/koanf/parsers/yaml"
|
||||
koanffile "github.com/knadh/koanf/providers/file"
|
||||
"go.signoz.io/signoz/pkg/config"
|
||||
)
|
||||
|
||||
const (
|
||||
scheme string = "file"
|
||||
)
|
||||
|
||||
type provider struct{}
|
||||
|
||||
func NewFactory() config.ProviderFactory {
|
||||
return config.NewProviderFactory(New)
|
||||
}
|
||||
|
||||
func New(config config.ProviderConfig) config.Provider {
|
||||
return &provider{}
|
||||
}
|
||||
|
||||
func (provider *provider) Scheme() string {
|
||||
return scheme
|
||||
}
|
||||
|
||||
func (provider *provider) Get(ctx context.Context, uri config.Uri) (*config.Conf, error) {
|
||||
conf := config.NewConf()
|
||||
err := conf.Load(koanffile.Provider(uri.Value()), koanfyaml.Parser())
|
||||
|
||||
return conf, err
|
||||
}
|
||||
68
pkg/config/fileprovider/provider_test.go
Normal file
68
pkg/config/fileprovider/provider_test.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package fileprovider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.signoz.io/signoz/pkg/config"
|
||||
)
|
||||
|
||||
func TestGetWithStrings(t *testing.T) {
|
||||
expected := map[string]any{
|
||||
"k1::k2": "string",
|
||||
"k3_k4": "string",
|
||||
"k5_k6::k7_k8": "string",
|
||||
"k9_::k10": "string",
|
||||
"k11__k12": "string",
|
||||
}
|
||||
|
||||
provider := New(config.ProviderConfig{})
|
||||
actual, err := provider.Get(context.Background(), config.MustNewUri("file:"+filepath.Join("testdata", "strings.yaml")))
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, expected, actual.All())
|
||||
}
|
||||
|
||||
func TestGetWithGoTypes(t *testing.T) {
|
||||
expected := map[string]any{
|
||||
"bool": true,
|
||||
"int": 1,
|
||||
"slice": []any{1, 2},
|
||||
"string": "string",
|
||||
}
|
||||
|
||||
provider := New(config.ProviderConfig{})
|
||||
actual, err := provider.Get(context.Background(), config.MustNewUri("file:"+filepath.Join("testdata", "gotypes.yaml")))
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, expected, actual.All())
|
||||
}
|
||||
|
||||
func TestGetWithGoTypesWithUnmarshal(t *testing.T) {
|
||||
type test struct {
|
||||
Bool bool `mapstructure:"bool"`
|
||||
String string `mapstructure:"string"`
|
||||
Int int `mapstructure:"int"`
|
||||
Slice []any `mapstructure:"slice"`
|
||||
}
|
||||
|
||||
expected := test{
|
||||
Bool: true,
|
||||
String: "string",
|
||||
Int: 1,
|
||||
Slice: []any{1, 2},
|
||||
}
|
||||
|
||||
provider := New(config.ProviderConfig{})
|
||||
conf, err := provider.Get(context.Background(), config.MustNewUri("file:"+filepath.Join("testdata", "gotypes.yaml")))
|
||||
require.NoError(t, err)
|
||||
|
||||
actual := test{}
|
||||
err = conf.Unmarshal("", &actual)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, expected, actual)
|
||||
}
|
||||
6
pkg/config/fileprovider/testdata/gotypes.yaml
vendored
Normal file
6
pkg/config/fileprovider/testdata/gotypes.yaml
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
bool: true
|
||||
string: string
|
||||
int: 1
|
||||
slice:
|
||||
- 1
|
||||
- 2
|
||||
8
pkg/config/fileprovider/testdata/strings.yaml
vendored
Normal file
8
pkg/config/fileprovider/testdata/strings.yaml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
k1:
|
||||
k2: string
|
||||
k3_k4: string
|
||||
k5_k6:
|
||||
k7_k8: string
|
||||
k9_:
|
||||
k10: string
|
||||
k11__k12: string
|
||||
@@ -2,51 +2,38 @@ package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"go.opentelemetry.io/collector/confmap"
|
||||
)
|
||||
|
||||
// Provides the configuration for signoz.
|
||||
// NewProviderFunc is a function that creates a new provider.
|
||||
type NewProviderFunc = func(ProviderConfig) Provider
|
||||
|
||||
// ProviderFactory is a factory that creates a new provider.
|
||||
type ProviderFactory interface {
|
||||
New(ProviderConfig) Provider
|
||||
}
|
||||
|
||||
// NewProviderFactory creates a new provider factory.
|
||||
func NewProviderFactory(f NewProviderFunc) ProviderFactory {
|
||||
return &providerFactory{f: f}
|
||||
}
|
||||
|
||||
// providerFactory is a factory that implements the ProviderFactory interface.
|
||||
type providerFactory struct {
|
||||
f NewProviderFunc
|
||||
}
|
||||
|
||||
// New creates a new provider.
|
||||
func (factory *providerFactory) New(config ProviderConfig) Provider {
|
||||
return factory.f(config)
|
||||
}
|
||||
|
||||
// ProviderConfig is the configuration for a provider.
|
||||
type ProviderConfig struct{}
|
||||
|
||||
// Provider is an interface that represents a configuration provider.
|
||||
type Provider interface {
|
||||
// Get returns the configuration, or error otherwise.
|
||||
Get(ctx context.Context) (*Config, error)
|
||||
}
|
||||
|
||||
type provider struct {
|
||||
resolver *confmap.Resolver
|
||||
}
|
||||
|
||||
// ProviderSettings are the settings to configure the behavior of the Provider.
|
||||
type ProviderSettings struct {
|
||||
// ResolverSettings are the settings to configure the behavior of the confmap.Resolver.
|
||||
ResolverSettings confmap.ResolverSettings
|
||||
}
|
||||
|
||||
// NewProvider returns a new Provider that provides the entire configuration.
|
||||
// See https://github.com/open-telemetry/opentelemetry-collector/blob/main/otelcol/configprovider.go for
|
||||
// more details
|
||||
func NewProvider(settings ProviderSettings) (Provider, error) {
|
||||
resolver, err := confmap.NewResolver(settings.ResolverSettings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &provider{
|
||||
resolver: resolver,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (provider *provider) Get(ctx context.Context) (*Config, error) {
|
||||
conf, err := provider.resolver.Resolve(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot resolve configuration: %w", err)
|
||||
}
|
||||
|
||||
config, err := unmarshal(conf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot unmarshal configuration: %w", err)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
// Get returns the configuration for the given URI.
|
||||
Get(context.Context, Uri) (*Conf, error)
|
||||
// Scheme returns the scheme of the provider.
|
||||
Scheme() string
|
||||
}
|
||||
|
||||
87
pkg/config/resolver.go
Normal file
87
pkg/config/resolver.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type ResolverConfig struct {
|
||||
// Each string or `uri` must follow "<scheme>:<value>" format. This format is compatible with the URI definition
|
||||
// defined at https://datatracker.ietf.org/doc/html/rfc3986".
|
||||
// It is required to have at least one uri.
|
||||
Uris []string
|
||||
|
||||
// ProviderFactories is a slice of Provider factories.
|
||||
// It is required to have at least one factory.
|
||||
ProviderFactories []ProviderFactory
|
||||
}
|
||||
|
||||
type Resolver struct {
|
||||
uris []Uri
|
||||
providers map[string]Provider
|
||||
}
|
||||
|
||||
func NewResolver(config ResolverConfig) (*Resolver, error) {
|
||||
if len(config.Uris) == 0 {
|
||||
return nil, errors.New("cannot build resolver, no uris have been provided")
|
||||
}
|
||||
|
||||
if len(config.ProviderFactories) == 0 {
|
||||
return nil, errors.New("cannot build resolver, no providers have been provided")
|
||||
}
|
||||
|
||||
uris := make([]Uri, len(config.Uris))
|
||||
for i, inputUri := range config.Uris {
|
||||
uri, err := NewUri(inputUri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uris[i] = uri
|
||||
}
|
||||
|
||||
providers := make(map[string]Provider, len(config.ProviderFactories))
|
||||
for _, factory := range config.ProviderFactories {
|
||||
provider := factory.New(ProviderConfig{})
|
||||
|
||||
scheme := provider.Scheme()
|
||||
// Check that the scheme is unique.
|
||||
if _, ok := providers[scheme]; ok {
|
||||
return nil, fmt.Errorf("cannot build resolver, duplicate scheme %q found", scheme)
|
||||
}
|
||||
|
||||
providers[provider.Scheme()] = provider
|
||||
}
|
||||
|
||||
return &Resolver{
|
||||
uris: uris,
|
||||
providers: providers,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (resolver *Resolver) Do(ctx context.Context) (*Conf, error) {
|
||||
conf := NewConf()
|
||||
|
||||
for _, uri := range resolver.uris {
|
||||
currentConf, err := resolver.get(ctx, uri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = conf.Merge(currentConf); err != nil {
|
||||
return nil, fmt.Errorf("cannot merge config: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
func (resolver *Resolver) get(ctx context.Context, uri Uri) (*Conf, error) {
|
||||
provider, ok := resolver.providers[uri.scheme]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("cannot find provider with schema %q", uri.scheme)
|
||||
}
|
||||
|
||||
return provider.Get(ctx, uri)
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"go.opentelemetry.io/collector/confmap"
|
||||
)
|
||||
|
||||
// unmarshal converts a confmap.Conf into a Config struct.
|
||||
// It splits the input confmap into a map of key-value pairs, fetches the corresponding
|
||||
// signozconfmap.Config interface by name, merges it with the default config, validates it,
|
||||
// and then creates a new confmap from the parsed map to unmarshal into the Config struct.
|
||||
func unmarshal(conf *confmap.Conf) (*Config, error) {
|
||||
raw := make(map[string]any)
|
||||
if err := conf.Unmarshal(&raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parsed := make(map[string]any)
|
||||
|
||||
// To help the defaults kick in, we need iterate over the default map instead of the raw values
|
||||
for k, v := range defaults {
|
||||
sub, err := conf.Sub(k)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot read config for %q: %w", k, err)
|
||||
}
|
||||
|
||||
d := v.NewWithDefaults()
|
||||
if err := sub.Unmarshal(&d); err != nil {
|
||||
return nil, fmt.Errorf("cannot merge config for %q: %w", k, err)
|
||||
}
|
||||
|
||||
err = d.Validate()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to validate config for for %q: %w", k, err)
|
||||
}
|
||||
|
||||
parsed[k] = d
|
||||
}
|
||||
|
||||
parsedConf := confmap.NewFromStringMap(parsed)
|
||||
config := new(Config)
|
||||
err := parsedConf.Unmarshal(config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot unmarshal config: %w", err)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.opentelemetry.io/collector/confmap"
|
||||
"go.signoz.io/signoz/pkg/instrumentation"
|
||||
)
|
||||
|
||||
func TestUnmarshalForInstrumentation(t *testing.T) {
|
||||
input := confmap.NewFromStringMap(
|
||||
map[string]any{
|
||||
"instrumentation": map[string]any{
|
||||
"logs": map[string]bool{
|
||||
"enabled": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
expected := &Config{
|
||||
Instrumentation: instrumentation.Config{
|
||||
Logs: instrumentation.LogsConfig{
|
||||
Enabled: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
cfg, err := unmarshal(input)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, expected.Instrumentation, cfg.Instrumentation)
|
||||
}
|
||||
46
pkg/config/uri.go
Normal file
46
pkg/config/uri.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var (
|
||||
// uriRegex is a regex that matches the URI format. It complies with the URI definition defined at https://datatracker.ietf.org/doc/html/rfc3986.
|
||||
// The format is "<scheme>:<value>".
|
||||
uriRegex = regexp.MustCompile(`(?s:^(?P<Scheme>[A-Za-z][A-Za-z0-9+.-]+):(?P<Value>.*)$)`)
|
||||
)
|
||||
|
||||
type Uri struct {
|
||||
scheme string
|
||||
value string
|
||||
}
|
||||
|
||||
func NewUri(input string) (Uri, error) {
|
||||
submatches := uriRegex.FindStringSubmatch(input)
|
||||
|
||||
if len(submatches) != 3 {
|
||||
return Uri{}, fmt.Errorf("invalid uri: %q", input)
|
||||
}
|
||||
return Uri{
|
||||
scheme: submatches[1],
|
||||
value: submatches[2],
|
||||
}, nil
|
||||
}
|
||||
|
||||
func MustNewUri(input string) Uri {
|
||||
uri, err := NewUri(input)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return uri
|
||||
}
|
||||
|
||||
func (uri Uri) Scheme() string {
|
||||
return uri.scheme
|
||||
}
|
||||
|
||||
func (uri Uri) Value() string {
|
||||
return uri.value
|
||||
}
|
||||
35
pkg/config/uri_test.go
Normal file
35
pkg/config/uri_test.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewUri(t *testing.T) {
|
||||
testCases := []struct {
|
||||
input string
|
||||
expected Uri
|
||||
pass bool
|
||||
}{
|
||||
{input: "file:/path/1", expected: Uri{scheme: "file", value: "/path/1"}, pass: true},
|
||||
{input: "file:", expected: Uri{scheme: "file", value: ""}, pass: true},
|
||||
{input: "env:", expected: Uri{scheme: "env", value: ""}, pass: true},
|
||||
{input: "scheme", expected: Uri{}, pass: false},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
uri, err := NewUri(tc.input)
|
||||
if !tc.pass {
|
||||
assert.Error(t, err)
|
||||
continue
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotPanics(t, func() { MustNewUri(tc.input) })
|
||||
assert.Equal(t, tc.expected, uri)
|
||||
assert.Equal(t, tc.expected.Scheme(), uri.scheme)
|
||||
assert.Equal(t, tc.expected.Value(), uri.value)
|
||||
}
|
||||
}
|
||||
37
pkg/factory/config.go
Normal file
37
pkg/factory/config.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package factory
|
||||
|
||||
// Config is an interface that defines methods for creating and validating configurations.
|
||||
type Config interface {
|
||||
// Validate the configuration and returns an error if invalid.
|
||||
Validate() error
|
||||
}
|
||||
|
||||
// NewConfigFunc is a function that creates a new config.
|
||||
type NewConfigFunc func() Config
|
||||
|
||||
// ConfigFactory is a factory that creates a new config.
|
||||
type ConfigFactory interface {
|
||||
Named
|
||||
New() Config
|
||||
}
|
||||
|
||||
// configFactory is a factory that implements the ConfigFactory interface.
|
||||
type configFactory struct {
|
||||
name Name
|
||||
newConfigFunc NewConfigFunc
|
||||
}
|
||||
|
||||
// Name returns the name of the factory.
|
||||
func (factory *configFactory) Name() Name {
|
||||
return factory.name
|
||||
}
|
||||
|
||||
// New creates a new config.
|
||||
func (factory *configFactory) New() Config {
|
||||
return factory.newConfigFunc()
|
||||
}
|
||||
|
||||
// Creates a new config factory.
|
||||
func NewConfigFactory(name Name, f NewConfigFunc) ConfigFactory {
|
||||
return &configFactory{name: name, newConfigFunc: f}
|
||||
}
|
||||
29
pkg/factory/config_test.go
Normal file
29
pkg/factory/config_test.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package factory
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type c1 struct{}
|
||||
|
||||
func (c1) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestNewConfigFactory(t *testing.T) {
|
||||
cf := NewConfigFactory(MustNewName("c1"), func() Config {
|
||||
return c1{}
|
||||
})
|
||||
assert.Equal(t, MustNewName("c1"), cf.Name())
|
||||
assert.IsType(t, c1{}, cf.New())
|
||||
}
|
||||
|
||||
func TestNewConfigFactoryWithPointer(t *testing.T) {
|
||||
cfp := NewConfigFactory(MustNewName("c1"), func() Config {
|
||||
return &c1{}
|
||||
})
|
||||
assert.Equal(t, MustNewName("c1"), cfp.Name())
|
||||
assert.IsType(t, &c1{}, cfp.New())
|
||||
}
|
||||
38
pkg/factory/name.go
Normal file
38
pkg/factory/name.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package factory
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var (
|
||||
// nameRegex is a regex that matches a valid name.
|
||||
// It must start with a alphabet, and can only contain alphabets, numbers, underscores or hyphens.
|
||||
nameRegex = regexp.MustCompile(`^[a-z][a-z0-9_-]{0,30}$`)
|
||||
)
|
||||
|
||||
type Name struct {
|
||||
name string
|
||||
}
|
||||
|
||||
func (n Name) String() string {
|
||||
return n.name
|
||||
}
|
||||
|
||||
// NewName creates a new name.
|
||||
func NewName(name string) (Name, error) {
|
||||
if !nameRegex.MatchString(name) {
|
||||
return Name{}, fmt.Errorf("invalid factory name %q", name)
|
||||
}
|
||||
return Name{name: name}, nil
|
||||
}
|
||||
|
||||
// MustNewName creates a new name.
|
||||
// It panics if the name is invalid.
|
||||
func MustNewName(name string) Name {
|
||||
n, err := NewName(name)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return n
|
||||
}
|
||||
20
pkg/factory/name_test.go
Normal file
20
pkg/factory/name_test.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package factory
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestName(t *testing.T) {
|
||||
assert.Equal(t, Name{name: "c1"}, MustNewName("c1"))
|
||||
}
|
||||
|
||||
func TestNameWithInvalidCharacters(t *testing.T) {
|
||||
_, err := NewName("c1%")
|
||||
assert.Error(t, err)
|
||||
|
||||
assert.Panics(t, func() {
|
||||
MustNewName("c1%")
|
||||
})
|
||||
}
|
||||
74
pkg/factory/named.go
Normal file
74
pkg/factory/named.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package factory
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Named is implemented by all types of factories.
|
||||
type Named interface {
|
||||
Name() Name
|
||||
}
|
||||
|
||||
type NamedMap[T Named] struct {
|
||||
factories map[Name]T
|
||||
factoriesInOrder []T
|
||||
}
|
||||
|
||||
// NewNamedMap creates a new NamedMap from a list of factories.
|
||||
// It returns an error if the factories have duplicate names.
|
||||
func NewNamedMap[T Named](factories ...T) (NamedMap[T], error) {
|
||||
fmap := make(map[Name]T)
|
||||
for _, factory := range factories {
|
||||
if _, ok := fmap[factory.Name()]; ok {
|
||||
return NamedMap[T]{}, fmt.Errorf("cannot build factory map, duplicate name %q found", factory.Name())
|
||||
}
|
||||
|
||||
fmap[factory.Name()] = factory
|
||||
}
|
||||
|
||||
return NamedMap[T]{factories: fmap, factoriesInOrder: factories}, nil
|
||||
}
|
||||
|
||||
// MustNewNamedMap creates a new NamedMap from a list of factories.
|
||||
// It panics if the factories have duplicate names.
|
||||
func MustNewNamedMap[T Named](factories ...T) NamedMap[T] {
|
||||
nm, err := NewNamedMap(factories...)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return nm
|
||||
}
|
||||
|
||||
// Get returns the factory for the given name by string.
|
||||
// It returns an error if the factory is not found or the name is invalid.
|
||||
func (n *NamedMap[T]) Get(namestr string) (t T, err error) {
|
||||
name, err := NewName(namestr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
factory, ok := n.factories[name]
|
||||
if !ok {
|
||||
err = fmt.Errorf("factory %q not found or not registered", name)
|
||||
return
|
||||
}
|
||||
|
||||
t = factory
|
||||
return
|
||||
}
|
||||
|
||||
// Add adds a factory to the NamedMap.
|
||||
// It returns an error if the factory already exists.
|
||||
func (n *NamedMap[T]) Add(factory T) (err error) {
|
||||
name := factory.Name()
|
||||
if _, ok := n.factories[name]; ok {
|
||||
return fmt.Errorf("factory %q already exists", name)
|
||||
}
|
||||
|
||||
n.factories[name] = factory
|
||||
n.factoriesInOrder = append(n.factoriesInOrder, factory)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetInOrder returns the factories in the order they were added.
|
||||
func (n *NamedMap[T]) GetInOrder() []T {
|
||||
return n.factoriesInOrder
|
||||
}
|
||||
72
pkg/factory/named_test.go
Normal file
72
pkg/factory/named_test.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package factory
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type f1 struct{}
|
||||
|
||||
func (*f1) Name() Name {
|
||||
return MustNewName("f1")
|
||||
}
|
||||
|
||||
type f2 struct{}
|
||||
|
||||
func (*f2) Name() Name {
|
||||
return MustNewName("f2")
|
||||
}
|
||||
|
||||
func TestNewNamedMap(t *testing.T) {
|
||||
nm, err := NewNamedMap[Named](&f1{}, &f2{})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, map[Name]Named{
|
||||
MustNewName("f1"): &f1{},
|
||||
MustNewName("f2"): &f2{},
|
||||
}, nm.factories)
|
||||
assert.Equal(t, []Named{&f1{}, &f2{}}, nm.GetInOrder())
|
||||
}
|
||||
|
||||
func TestNewNamedMapWithDuplicateNames(t *testing.T) {
|
||||
_, err := NewNamedMap[Named](&f1{}, &f1{})
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestMustNewNamedMap(t *testing.T) {
|
||||
nm := MustNewNamedMap[Named](&f1{}, &f2{})
|
||||
assert.Equal(t, map[Name]Named{
|
||||
MustNewName("f1"): &f1{},
|
||||
MustNewName("f2"): &f2{},
|
||||
}, nm.factories)
|
||||
assert.Equal(t, []Named{&f1{}, &f2{}}, nm.GetInOrder())
|
||||
}
|
||||
|
||||
func TestMustNewNamedMapDuplicateNames(t *testing.T) {
|
||||
assert.Panics(t, func() {
|
||||
MustNewNamedMap[Named](&f1{}, &f1{})
|
||||
})
|
||||
}
|
||||
|
||||
func TestNamedMapGet(t *testing.T) {
|
||||
nm := MustNewNamedMap[Named](&f1{}, &f2{})
|
||||
|
||||
nf1, err := nm.Get("f1")
|
||||
assert.NoError(t, err)
|
||||
assert.IsType(t, &f1{}, nf1)
|
||||
|
||||
_, err = nm.Get("f3")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestNamedMapAdd(t *testing.T) {
|
||||
nm := MustNewNamedMap[Named](&f1{})
|
||||
|
||||
err := nm.Add(&f2{})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, map[Name]Named{
|
||||
MustNewName("f1"): &f1{},
|
||||
MustNewName("f2"): &f2{},
|
||||
}, nm.factories)
|
||||
assert.Equal(t, []Named{&f1{}, &f2{}}, nm.GetInOrder())
|
||||
}
|
||||
48
pkg/factory/provider.go
Normal file
48
pkg/factory/provider.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package factory
|
||||
|
||||
import "context"
|
||||
|
||||
type Provider = any
|
||||
|
||||
// NewProviderFunc is a function that creates a new Provider.
|
||||
type NewProviderFunc[P Provider, C Config] func(context.Context, ProviderSettings, C) (P, error)
|
||||
|
||||
type ProviderFactory[P Provider, C Config] interface {
|
||||
Named
|
||||
New(context.Context, ProviderSettings, C) (P, error)
|
||||
}
|
||||
|
||||
type providerFactory[P Provider, C Config] struct {
|
||||
name Name
|
||||
newProviderFunc NewProviderFunc[P, C]
|
||||
}
|
||||
|
||||
func (factory *providerFactory[P, C]) Name() Name {
|
||||
return factory.name
|
||||
}
|
||||
|
||||
func (factory *providerFactory[P, C]) New(ctx context.Context, settings ProviderSettings, config C) (P, error) {
|
||||
return factory.newProviderFunc(ctx, settings, config)
|
||||
}
|
||||
|
||||
func NewProviderFactory[P Provider, C Config](name Name, newProviderFunc NewProviderFunc[P, C]) ProviderFactory[P, C] {
|
||||
return &providerFactory[P, C]{
|
||||
name: name,
|
||||
newProviderFunc: newProviderFunc,
|
||||
}
|
||||
}
|
||||
|
||||
func NewFromFactory[P Provider, C Config](ctx context.Context, settings ProviderSettings, config C, factories NamedMap[ProviderFactory[P, C]], key string) (p P, err error) {
|
||||
providerFactory, err := factories.Get(key)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
provider, err := providerFactory.New(ctx, settings, config)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
p = provider
|
||||
return
|
||||
}
|
||||
41
pkg/factory/provider_test.go
Normal file
41
pkg/factory/provider_test.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package factory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type p1 struct{}
|
||||
|
||||
type pc1 struct{}
|
||||
|
||||
func (pc1) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestNewProviderFactory(t *testing.T) {
|
||||
pf := NewProviderFactory(MustNewName("p1"), func(ctx context.Context, settings ProviderSettings, config pc1) (p1, error) {
|
||||
return p1{}, nil
|
||||
})
|
||||
assert.Equal(t, MustNewName("p1"), pf.Name())
|
||||
p, err := pf.New(context.Background(), ProviderSettings{}, pc1{})
|
||||
assert.NoError(t, err)
|
||||
assert.IsType(t, p1{}, p)
|
||||
}
|
||||
|
||||
func TestNewProviderFactoryFromFactory(t *testing.T) {
|
||||
pf := NewProviderFactory(MustNewName("p1"), func(ctx context.Context, settings ProviderSettings, config pc1) (p1, error) {
|
||||
return p1{}, nil
|
||||
})
|
||||
|
||||
m := MustNewNamedMap(pf)
|
||||
assert.Equal(t, MustNewName("p1"), pf.Name())
|
||||
p, err := NewFromFactory(context.Background(), ProviderSettings{}, pc1{}, m, "p1")
|
||||
assert.NoError(t, err)
|
||||
assert.IsType(t, p1{}, p)
|
||||
|
||||
_, err = NewFromFactory(context.Background(), ProviderSettings{}, pc1{}, m, "p2")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
10
pkg/factory/service.go
Normal file
10
pkg/factory/service.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package factory
|
||||
|
||||
import "context"
|
||||
|
||||
type Service interface {
|
||||
// Starts a service. The service should return an error if it cannot be started.
|
||||
Start(context.Context) error
|
||||
// Stops a service.
|
||||
Stop(context.Context) error
|
||||
}
|
||||
51
pkg/factory/servicetest/http.go
Normal file
51
pkg/factory/servicetest/http.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package servicetest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"go.signoz.io/signoz/pkg/factory"
|
||||
)
|
||||
|
||||
var _ factory.Service = (*httpService)(nil)
|
||||
|
||||
type httpService struct {
|
||||
Listener net.Listener
|
||||
Server *http.Server
|
||||
name string
|
||||
}
|
||||
|
||||
func NewHttpService(name string) (*httpService, error) {
|
||||
return &httpService{
|
||||
name: name,
|
||||
Server: &http.Server{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (service *httpService) Name() factory.Name {
|
||||
return factory.MustNewName(service.name)
|
||||
}
|
||||
|
||||
func (service *httpService) Start(ctx context.Context) error {
|
||||
listener, err := net.Listen("tcp", "localhost:0")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
service.Listener = listener
|
||||
|
||||
if err := service.Server.Serve(service.Listener); err != nil {
|
||||
if err != http.ErrServerClosed {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *httpService) Stop(ctx context.Context) error {
|
||||
if err := service.Server.Shutdown(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
58
pkg/factory/setting.go
Normal file
58
pkg/factory/setting.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package factory
|
||||
|
||||
import (
|
||||
sdklog "go.opentelemetry.io/otel/log"
|
||||
sdkmetric "go.opentelemetry.io/otel/metric"
|
||||
sdktrace "go.opentelemetry.io/otel/trace"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type ProviderSettings struct {
|
||||
// LoggerProvider is the otel logger.
|
||||
LoggerProvider sdklog.LoggerProvider
|
||||
// ZapLogger is the zap logger.
|
||||
ZapLogger *zap.Logger
|
||||
// MeterProvider is the meter provider.
|
||||
MeterProvider sdkmetric.MeterProvider
|
||||
// TracerProvider is the tracer provider.
|
||||
TracerProvider sdktrace.TracerProvider
|
||||
}
|
||||
|
||||
type ScopedProviderSettings interface {
|
||||
Logger() sdklog.Logger
|
||||
ZapLogger() *zap.Logger
|
||||
Meter() sdkmetric.Meter
|
||||
Tracer() sdktrace.Tracer
|
||||
}
|
||||
|
||||
type scoped struct {
|
||||
logger sdklog.Logger
|
||||
zapLogger *zap.Logger
|
||||
meter sdkmetric.Meter
|
||||
tracer sdktrace.Tracer
|
||||
}
|
||||
|
||||
func NewScopedProviderSettings(settings ProviderSettings, pkgName string) *scoped {
|
||||
return &scoped{
|
||||
logger: settings.LoggerProvider.Logger(pkgName),
|
||||
zapLogger: settings.ZapLogger.Named(pkgName),
|
||||
meter: settings.MeterProvider.Meter(pkgName),
|
||||
tracer: settings.TracerProvider.Tracer(pkgName),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *scoped) Logger() sdklog.Logger {
|
||||
return s.logger
|
||||
}
|
||||
|
||||
func (s *scoped) ZapLogger() *zap.Logger {
|
||||
return s.zapLogger
|
||||
}
|
||||
|
||||
func (s *scoped) Meter() sdkmetric.Meter {
|
||||
return s.meter
|
||||
}
|
||||
|
||||
func (s *scoped) Tracer() sdktrace.Tracer {
|
||||
return s.tracer
|
||||
}
|
||||
@@ -2,13 +2,10 @@ package instrumentation
|
||||
|
||||
import (
|
||||
contribsdkconfig "go.opentelemetry.io/contrib/config"
|
||||
"go.signoz.io/signoz/pkg/confmap"
|
||||
"go.signoz.io/signoz/pkg/factory"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
// Config satisfies the confmap.Config interface
|
||||
var _ confmap.Config = (*Config)(nil)
|
||||
|
||||
// Config holds the configuration for all instrumentation components.
|
||||
type Config struct {
|
||||
Logs LogsConfig `mapstructure:"logs"`
|
||||
@@ -24,39 +21,69 @@ type Resource struct {
|
||||
|
||||
// LogsConfig holds the configuration for the logging component.
|
||||
type LogsConfig struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
Level zapcore.Level `mapstructure:"level"`
|
||||
contribsdkconfig.LoggerProvider `mapstructure:",squash"`
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
Level zapcore.Level `mapstructure:"level"`
|
||||
Processors LogsProcessors `mapstructure:"processors"`
|
||||
}
|
||||
|
||||
type LogsProcessors struct {
|
||||
Batch contribsdkconfig.BatchLogRecordProcessor `mapstructure:"batch"`
|
||||
}
|
||||
|
||||
// TracesConfig holds the configuration for the tracing component.
|
||||
type TracesConfig struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
contribsdkconfig.TracerProvider `mapstructure:",squash"`
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
Processors TracesProcessors `mapstructure:"processors"`
|
||||
Sampler contribsdkconfig.Sampler `mapstructure:"sampler"`
|
||||
}
|
||||
|
||||
type TracesProcessors struct {
|
||||
Batch contribsdkconfig.BatchSpanProcessor `mapstructure:"batch"`
|
||||
}
|
||||
|
||||
// MetricsConfig holds the configuration for the metrics component.
|
||||
type MetricsConfig struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
contribsdkconfig.MeterProvider `mapstructure:",squash"`
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
Readers MetricsReaders `mapstructure:"readers"`
|
||||
}
|
||||
|
||||
func (c *Config) NewWithDefaults() confmap.Config {
|
||||
return &Config{
|
||||
type MetricsReaders struct {
|
||||
Pull contribsdkconfig.PullMetricReader `mapstructure:"pull"`
|
||||
}
|
||||
|
||||
func NewConfigFactory() factory.ConfigFactory {
|
||||
return factory.NewConfigFactory(factory.MustNewName("instrumentation"), newConfig)
|
||||
}
|
||||
|
||||
func newConfig() factory.Config {
|
||||
host := "0.0.0.0"
|
||||
port := 9090
|
||||
|
||||
return Config{
|
||||
Logs: LogsConfig{
|
||||
Enabled: false,
|
||||
Level: zapcore.InfoLevel,
|
||||
Level: zapcore.DebugLevel,
|
||||
},
|
||||
Traces: TracesConfig{
|
||||
Enabled: false,
|
||||
},
|
||||
Metrics: MetricsConfig{
|
||||
Enabled: false,
|
||||
Enabled: true,
|
||||
Readers: MetricsReaders{
|
||||
Pull: contribsdkconfig.PullMetricReader{
|
||||
Exporter: contribsdkconfig.MetricExporter{
|
||||
Prometheus: &contribsdkconfig.Prometheus{
|
||||
Host: &host,
|
||||
Port: &port,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (c *Config) Validate() error {
|
||||
func (c Config) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,85 +1,34 @@
|
||||
package instrumentation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
contribsdkconfig "go.opentelemetry.io/contrib/config"
|
||||
"go.opentelemetry.io/contrib/bridges/otelzap"
|
||||
sdklog "go.opentelemetry.io/otel/log"
|
||||
sdkmetric "go.opentelemetry.io/otel/metric"
|
||||
sdkresource "go.opentelemetry.io/otel/sdk/resource"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
|
||||
sdktrace "go.opentelemetry.io/otel/trace"
|
||||
"go.signoz.io/signoz/pkg/version"
|
||||
"go.signoz.io/signoz/pkg/factory"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
// Instrumentation holds the core components for application instrumentation.
|
||||
type Instrumentation struct {
|
||||
LoggerProvider sdklog.LoggerProvider
|
||||
Logger *zap.Logger
|
||||
MeterProvider sdkmetric.MeterProvider
|
||||
TracerProvider sdktrace.TracerProvider
|
||||
// Instrumentation provides the core components for application instrumentation.
|
||||
type Instrumentation interface {
|
||||
// LoggerProvider returns the OpenTelemetry logger provider.
|
||||
LoggerProvider() sdklog.LoggerProvider
|
||||
// Logger returns the Zap logger.
|
||||
Logger() *zap.Logger
|
||||
// MeterProvider returns the OpenTelemetry meter provider.
|
||||
MeterProvider() sdkmetric.MeterProvider
|
||||
// TracerProvider returns the OpenTelemetry tracer provider.
|
||||
TracerProvider() sdktrace.TracerProvider
|
||||
// ToProviderSettings converts instrumentation to provider settings.
|
||||
ToProviderSettings() factory.ProviderSettings
|
||||
}
|
||||
|
||||
// New creates a new Instrumentation instance with configured providers.
|
||||
// It sets up logging, tracing, and metrics based on the provided configuration.
|
||||
func New(ctx context.Context, build version.Build, cfg Config) (*Instrumentation, error) {
|
||||
// Set default resource attributes if not provided
|
||||
if cfg.Resource.Attributes == nil {
|
||||
cfg.Resource.Attributes = map[string]any{
|
||||
string(semconv.ServiceNameKey): build.Name,
|
||||
string(semconv.ServiceVersionKey): build.Version,
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new resource with default detectors.
|
||||
// The upstream contrib repository is not taking detectors into account.
|
||||
// We are, therefore, using some sensible defaults here.
|
||||
resource, err := sdkresource.New(
|
||||
ctx,
|
||||
sdkresource.WithContainer(),
|
||||
sdkresource.WithFromEnv(),
|
||||
sdkresource.WithHost(),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Prepare the resource configuration by merging
|
||||
// resource and attributes.
|
||||
sch := semconv.SchemaURL
|
||||
configResource := contribsdkconfig.Resource{
|
||||
Attributes: attributes(cfg.Resource.Attributes, resource),
|
||||
Detectors: nil,
|
||||
SchemaUrl: &sch,
|
||||
}
|
||||
|
||||
loggerProvider, err := newLoggerProvider(ctx, cfg, configResource)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create logger provider: %w", err)
|
||||
}
|
||||
|
||||
tracerProvider, err := newTracerProvider(ctx, cfg, configResource)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create tracer provider: %w", err)
|
||||
}
|
||||
|
||||
meterProvider, err := newMeterProvider(ctx, cfg, configResource)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create meter provider: %w", err)
|
||||
}
|
||||
|
||||
return &Instrumentation{
|
||||
LoggerProvider: loggerProvider,
|
||||
TracerProvider: tracerProvider,
|
||||
MeterProvider: meterProvider,
|
||||
Logger: newLogger(cfg, loggerProvider),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// attributes merges the input attributes with the resource attributes.
|
||||
func attributes(input map[string]any, resource *sdkresource.Resource) map[string]any {
|
||||
// Merges the input attributes with the resource attributes.
|
||||
func mergeAttributes(input map[string]any, resource *sdkresource.Resource) map[string]any {
|
||||
output := make(map[string]any)
|
||||
|
||||
for k, v := range input {
|
||||
@@ -93,3 +42,14 @@ func attributes(input map[string]any, resource *sdkresource.Resource) map[string
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
// newLogger creates a new Zap logger with the configured level and output.
|
||||
// It combines a JSON encoder for stdout and an OpenTelemetry bridge.
|
||||
func newLogger(cfg Config, provider sdklog.LoggerProvider) *zap.Logger {
|
||||
core := zapcore.NewTee(
|
||||
zapcore.NewCore(zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), zapcore.AddSync(os.Stdout), cfg.Logs.Level),
|
||||
otelzap.NewCore("go.signoz.io/pkg/instrumentation", otelzap.WithLoggerProvider(provider)),
|
||||
)
|
||||
|
||||
return zap.New(core, zap.AddCaller(), zap.AddStacktrace(zap.ErrorLevel))
|
||||
}
|
||||
|
||||
54
pkg/instrumentation/instrumentationtest/instrumentation.go
Normal file
54
pkg/instrumentation/instrumentationtest/instrumentation.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package instrumentationtest
|
||||
|
||||
import (
|
||||
sdklog "go.opentelemetry.io/otel/log"
|
||||
nooplog "go.opentelemetry.io/otel/log/noop"
|
||||
sdkmetric "go.opentelemetry.io/otel/metric"
|
||||
noopmetric "go.opentelemetry.io/otel/metric/noop"
|
||||
sdktrace "go.opentelemetry.io/otel/trace"
|
||||
nooptrace "go.opentelemetry.io/otel/trace/noop"
|
||||
"go.signoz.io/signoz/pkg/factory"
|
||||
"go.signoz.io/signoz/pkg/instrumentation"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type noopInstrumentation struct {
|
||||
logger *zap.Logger
|
||||
loggerProvider sdklog.LoggerProvider
|
||||
meterProvider sdkmetric.MeterProvider
|
||||
tracerProvider sdktrace.TracerProvider
|
||||
}
|
||||
|
||||
func New() instrumentation.Instrumentation {
|
||||
return &noopInstrumentation{
|
||||
logger: zap.NewNop(),
|
||||
loggerProvider: nooplog.NewLoggerProvider(),
|
||||
meterProvider: noopmetric.NewMeterProvider(),
|
||||
tracerProvider: nooptrace.NewTracerProvider(),
|
||||
}
|
||||
}
|
||||
|
||||
func (i *noopInstrumentation) LoggerProvider() sdklog.LoggerProvider {
|
||||
return i.loggerProvider
|
||||
}
|
||||
|
||||
func (i *noopInstrumentation) Logger() *zap.Logger {
|
||||
return i.logger
|
||||
}
|
||||
|
||||
func (i *noopInstrumentation) MeterProvider() sdkmetric.MeterProvider {
|
||||
return i.meterProvider
|
||||
}
|
||||
|
||||
func (i *noopInstrumentation) TracerProvider() sdktrace.TracerProvider {
|
||||
return i.tracerProvider
|
||||
}
|
||||
|
||||
func (i *noopInstrumentation) ToProviderSettings() factory.ProviderSettings {
|
||||
return factory.ProviderSettings{
|
||||
LoggerProvider: i.LoggerProvider(),
|
||||
ZapLogger: i.Logger(),
|
||||
MeterProvider: i.MeterProvider(),
|
||||
TracerProvider: i.TracerProvider(),
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package instrumentation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"go.opentelemetry.io/contrib/bridges/otelzap"
|
||||
contribsdkconfig "go.opentelemetry.io/contrib/config"
|
||||
sdklog "go.opentelemetry.io/otel/log"
|
||||
nooplog "go.opentelemetry.io/otel/log/noop"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
// newLoggerProvider creates a new logger provider based on the configuration.
|
||||
// If logging is disabled, it returns a no-op logger provider.
|
||||
func newLoggerProvider(ctx context.Context, cfg Config, cfgResource contribsdkconfig.Resource) (sdklog.LoggerProvider, error) {
|
||||
if !cfg.Logs.Enabled {
|
||||
return nooplog.NewLoggerProvider(), nil
|
||||
}
|
||||
|
||||
sdk, err := contribsdkconfig.NewSDK(
|
||||
contribsdkconfig.WithContext(ctx),
|
||||
contribsdkconfig.WithOpenTelemetryConfiguration(contribsdkconfig.OpenTelemetryConfiguration{
|
||||
LoggerProvider: &cfg.Logs.LoggerProvider,
|
||||
Resource: &cfgResource,
|
||||
}),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return sdk.LoggerProvider(), nil
|
||||
}
|
||||
|
||||
// newLogger creates a new Zap logger with the configured level and output.
|
||||
// It combines a JSON encoder for stdout and an OpenTelemetry bridge.
|
||||
func newLogger(cfg Config, provider sdklog.LoggerProvider) *zap.Logger {
|
||||
core := zapcore.NewTee(
|
||||
zapcore.NewCore(zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), zapcore.AddSync(os.Stdout), cfg.Logs.Level),
|
||||
otelzap.NewCore("go.signoz.io/pkg/instrumentation", otelzap.WithLoggerProvider(provider)),
|
||||
)
|
||||
|
||||
return zap.New(core, zap.AddCaller(), zap.AddStacktrace(zap.ErrorLevel))
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package instrumentation
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
contribsdkconfig "go.opentelemetry.io/contrib/config"
|
||||
sdkmetric "go.opentelemetry.io/otel/metric"
|
||||
noopmetric "go.opentelemetry.io/otel/metric/noop"
|
||||
)
|
||||
|
||||
// newMeterProvider creates a new meter provider based on the configuration.
|
||||
// If metrics are disabled, it returns a no-op meter provider.
|
||||
func newMeterProvider(ctx context.Context, cfg Config, cfgResource contribsdkconfig.Resource) (sdkmetric.MeterProvider, error) {
|
||||
if !cfg.Metrics.Enabled {
|
||||
return noopmetric.NewMeterProvider(), nil
|
||||
}
|
||||
|
||||
sdk, err := contribsdkconfig.NewSDK(
|
||||
contribsdkconfig.WithContext(ctx),
|
||||
contribsdkconfig.WithOpenTelemetryConfiguration(contribsdkconfig.OpenTelemetryConfiguration{
|
||||
MeterProvider: &cfg.Metrics.MeterProvider,
|
||||
Resource: &cfgResource,
|
||||
}),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return sdk.MeterProvider(), nil
|
||||
}
|
||||
137
pkg/instrumentation/sdk.go
Normal file
137
pkg/instrumentation/sdk.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package instrumentation
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
contribsdkconfig "go.opentelemetry.io/contrib/config"
|
||||
sdklog "go.opentelemetry.io/otel/log"
|
||||
sdkmetric "go.opentelemetry.io/otel/metric"
|
||||
sdkresource "go.opentelemetry.io/otel/sdk/resource"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
|
||||
sdktrace "go.opentelemetry.io/otel/trace"
|
||||
"go.signoz.io/signoz/pkg/factory"
|
||||
"go.signoz.io/signoz/pkg/version"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var _ factory.Service = (*SDK)(nil)
|
||||
var _ Instrumentation = (*SDK)(nil)
|
||||
|
||||
// SDK holds the core components for application instrumentation.
|
||||
type SDK struct {
|
||||
sdk contribsdkconfig.SDK
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// New creates a new Instrumentation instance with configured providers.
|
||||
// It sets up logging, tracing, and metrics based on the provided configuration.
|
||||
func New(ctx context.Context, build version.Build, cfg Config) (*SDK, error) {
|
||||
// Set default resource attributes if not provided
|
||||
if cfg.Resource.Attributes == nil {
|
||||
cfg.Resource.Attributes = map[string]any{
|
||||
string(semconv.ServiceNameKey): build.Name,
|
||||
string(semconv.ServiceVersionKey): build.Version,
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new resource with default detectors.
|
||||
// The upstream contrib repository is not taking detectors into account.
|
||||
// We are, therefore, using some sensible defaults here.
|
||||
resource, err := sdkresource.New(
|
||||
ctx,
|
||||
sdkresource.WithContainer(),
|
||||
sdkresource.WithFromEnv(),
|
||||
sdkresource.WithHost(),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Prepare the resource configuration by merging
|
||||
// resource and attributes.
|
||||
sch := semconv.SchemaURL
|
||||
configResource := contribsdkconfig.Resource{
|
||||
Attributes: mergeAttributes(cfg.Resource.Attributes, resource),
|
||||
Detectors: nil,
|
||||
SchemaUrl: &sch,
|
||||
}
|
||||
|
||||
var loggerProvider *contribsdkconfig.LoggerProvider
|
||||
if cfg.Logs.Enabled {
|
||||
loggerProvider = &contribsdkconfig.LoggerProvider{
|
||||
Processors: []contribsdkconfig.LogRecordProcessor{
|
||||
{Batch: &cfg.Logs.Processors.Batch},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
var tracerProvider *contribsdkconfig.TracerProvider
|
||||
if cfg.Traces.Enabled {
|
||||
tracerProvider = &contribsdkconfig.TracerProvider{
|
||||
Processors: []contribsdkconfig.SpanProcessor{
|
||||
{Batch: &cfg.Traces.Processors.Batch},
|
||||
},
|
||||
Sampler: &cfg.Traces.Sampler,
|
||||
}
|
||||
}
|
||||
|
||||
var meterProvider *contribsdkconfig.MeterProvider
|
||||
if cfg.Metrics.Enabled {
|
||||
meterProvider = &contribsdkconfig.MeterProvider{
|
||||
Readers: []contribsdkconfig.MetricReader{
|
||||
{Pull: &cfg.Metrics.Readers.Pull},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
sdk, err := contribsdkconfig.NewSDK(
|
||||
contribsdkconfig.WithContext(ctx),
|
||||
contribsdkconfig.WithOpenTelemetryConfiguration(contribsdkconfig.OpenTelemetryConfiguration{
|
||||
LoggerProvider: loggerProvider,
|
||||
TracerProvider: tracerProvider,
|
||||
MeterProvider: meterProvider,
|
||||
Resource: &configResource,
|
||||
}),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &SDK{
|
||||
sdk: sdk,
|
||||
logger: newLogger(cfg, sdk.LoggerProvider()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (i *SDK) Start(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *SDK) Stop(ctx context.Context) error {
|
||||
return i.sdk.Shutdown(ctx)
|
||||
}
|
||||
|
||||
func (i *SDK) LoggerProvider() sdklog.LoggerProvider {
|
||||
return i.sdk.LoggerProvider()
|
||||
}
|
||||
|
||||
func (i *SDK) Logger() *zap.Logger {
|
||||
return i.logger
|
||||
}
|
||||
|
||||
func (i *SDK) MeterProvider() sdkmetric.MeterProvider {
|
||||
return i.sdk.MeterProvider()
|
||||
}
|
||||
|
||||
func (i *SDK) TracerProvider() sdktrace.TracerProvider {
|
||||
return i.sdk.TracerProvider()
|
||||
}
|
||||
|
||||
func (i *SDK) ToProviderSettings() factory.ProviderSettings {
|
||||
return factory.ProviderSettings{
|
||||
LoggerProvider: i.LoggerProvider(),
|
||||
ZapLogger: i.Logger(),
|
||||
MeterProvider: i.MeterProvider(),
|
||||
TracerProvider: i.TracerProvider(),
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package instrumentation
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
contribsdkconfig "go.opentelemetry.io/contrib/config"
|
||||
sdktrace "go.opentelemetry.io/otel/trace"
|
||||
nooptrace "go.opentelemetry.io/otel/trace/noop"
|
||||
)
|
||||
|
||||
// newTracerProvider creates a new tracer provider based on the configuration.
|
||||
// If tracing is disabled, it returns a no-op tracer provider.
|
||||
func newTracerProvider(ctx context.Context, cfg Config, cfgResource contribsdkconfig.Resource) (sdktrace.TracerProvider, error) {
|
||||
if !cfg.Traces.Enabled {
|
||||
return nooptrace.NewTracerProvider(), nil
|
||||
}
|
||||
|
||||
sdk, err := contribsdkconfig.NewSDK(
|
||||
contribsdkconfig.WithContext(ctx),
|
||||
contribsdkconfig.WithOpenTelemetryConfiguration(contribsdkconfig.OpenTelemetryConfiguration{
|
||||
TracerProvider: &cfg.Traces.TracerProvider,
|
||||
Resource: &cfgResource,
|
||||
}),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return sdk.TracerProvider(), nil
|
||||
}
|
||||
@@ -37,8 +37,8 @@ type cloudProviderAccountsRepository interface {
|
||||
func newCloudProviderAccountsRepository(db *sqlx.DB) (
|
||||
*cloudProviderAccountsSQLRepository, error,
|
||||
) {
|
||||
if err := InitSqliteDBIfNeeded(db); err != nil {
|
||||
return nil, fmt.Errorf("could not init sqlite DB for cloudintegrations: %w", err)
|
||||
if err := initAccountsSqliteDBIfNeeded(db); err != nil {
|
||||
return nil, fmt.Errorf("could not init sqlite DB for cloudintegrations accounts: %w", err)
|
||||
}
|
||||
|
||||
return &cloudProviderAccountsSQLRepository{
|
||||
@@ -46,7 +46,7 @@ func newCloudProviderAccountsRepository(db *sqlx.DB) (
|
||||
}, nil
|
||||
}
|
||||
|
||||
func InitSqliteDBIfNeeded(db *sqlx.DB) error {
|
||||
func initAccountsSqliteDBIfNeeded(db *sqlx.DB) error {
|
||||
if db == nil {
|
||||
return fmt.Errorf("db is required")
|
||||
}
|
||||
@@ -66,7 +66,7 @@ func InitSqliteDBIfNeeded(db *sqlx.DB) error {
|
||||
_, err := db.Exec(createTablesStatements)
|
||||
if err != nil {
|
||||
return fmt.Errorf(
|
||||
"could not ensure cloud provider integrations schema in sqlite DB: %w", err,
|
||||
"could not ensure cloud provider accounts schema in sqlite DB: %w", err,
|
||||
)
|
||||
}
|
||||
|
||||
217
pkg/query-service/app/cloudintegrations/availableServices.go
Normal file
217
pkg/query-service/app/cloudintegrations/availableServices.go
Normal file
@@ -0,0 +1,217 @@
|
||||
package cloudintegrations
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"path"
|
||||
"sort"
|
||||
|
||||
koanfJson "github.com/knadh/koanf/parsers/json"
|
||||
"go.signoz.io/signoz/pkg/query-service/app/integrations"
|
||||
"go.signoz.io/signoz/pkg/query-service/model"
|
||||
"golang.org/x/exp/maps"
|
||||
)
|
||||
|
||||
func listCloudProviderServices(
|
||||
cloudProvider string,
|
||||
) ([]CloudServiceDetails, *model.ApiError) {
|
||||
cloudServices := availableServices[cloudProvider]
|
||||
if cloudServices == nil {
|
||||
return nil, model.NotFoundError(fmt.Errorf(
|
||||
"unsupported cloud provider: %s", cloudProvider,
|
||||
))
|
||||
}
|
||||
|
||||
services := maps.Values(cloudServices)
|
||||
sort.Slice(services, func(i, j int) bool {
|
||||
return services[i].Id < services[j].Id
|
||||
})
|
||||
|
||||
return services, nil
|
||||
}
|
||||
|
||||
func getCloudProviderService(
|
||||
cloudProvider string, serviceId string,
|
||||
) (*CloudServiceDetails, *model.ApiError) {
|
||||
cloudServices := availableServices[cloudProvider]
|
||||
if cloudServices == nil {
|
||||
return nil, model.NotFoundError(fmt.Errorf(
|
||||
"unsupported cloud provider: %s", cloudProvider,
|
||||
))
|
||||
}
|
||||
|
||||
svc, exists := cloudServices[serviceId]
|
||||
if !exists {
|
||||
return nil, model.NotFoundError(fmt.Errorf(
|
||||
"%s service not found: %s", cloudProvider, serviceId,
|
||||
))
|
||||
}
|
||||
|
||||
return &svc, nil
|
||||
}
|
||||
|
||||
// End of API. Logic for reading service definition files follows
|
||||
|
||||
// Service details read from ./serviceDefinitions
|
||||
// { "providerName": { "service_id": {...}} }
|
||||
var availableServices map[string]map[string]CloudServiceDetails
|
||||
|
||||
func init() {
|
||||
err := readAllServiceDefinitions()
|
||||
if err != nil {
|
||||
panic(fmt.Errorf(
|
||||
"couldn't read cloud service definitions: %w", err,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
//go:embed serviceDefinitions/*
|
||||
var serviceDefinitionFiles embed.FS
|
||||
|
||||
func readAllServiceDefinitions() error {
|
||||
availableServices = map[string]map[string]CloudServiceDetails{}
|
||||
|
||||
rootDirName := "serviceDefinitions"
|
||||
|
||||
cloudProviderDirs, err := fs.ReadDir(serviceDefinitionFiles, rootDirName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("couldn't read dirs in %s: %w", rootDirName, err)
|
||||
}
|
||||
|
||||
for _, d := range cloudProviderDirs {
|
||||
if !d.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
cloudProviderDirPath := path.Join(rootDirName, d.Name())
|
||||
cloudServices, err := readServiceDefinitionsFromDir(cloudProviderDirPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("couldn't read %s service definitions", d.Name())
|
||||
}
|
||||
|
||||
if len(cloudServices) < 1 {
|
||||
return fmt.Errorf("no %s services could be read", d.Name())
|
||||
}
|
||||
|
||||
availableServices[d.Name()] = cloudServices
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func readServiceDefinitionsFromDir(cloudProviderDirPath string) (
|
||||
map[string]CloudServiceDetails, error,
|
||||
) {
|
||||
svcDefDirs, err := fs.ReadDir(serviceDefinitionFiles, cloudProviderDirPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("couldn't list integrations dirs: %w", err)
|
||||
}
|
||||
|
||||
svcDefs := map[string]CloudServiceDetails{}
|
||||
|
||||
for _, d := range svcDefDirs {
|
||||
if !d.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
svcDirPath := path.Join(cloudProviderDirPath, d.Name())
|
||||
s, err := readServiceDefinition(svcDirPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("couldn't read svc definition for %s: %w", d.Name(), err)
|
||||
}
|
||||
|
||||
_, exists := svcDefs[s.Id]
|
||||
if exists {
|
||||
return nil, fmt.Errorf(
|
||||
"duplicate service definition for id %s at %s", s.Id, d.Name(),
|
||||
)
|
||||
}
|
||||
svcDefs[s.Id] = *s
|
||||
}
|
||||
|
||||
return svcDefs, nil
|
||||
}
|
||||
|
||||
func readServiceDefinition(dirpath string) (*CloudServiceDetails, error) {
|
||||
integrationJsonPath := path.Join(dirpath, "integration.json")
|
||||
|
||||
serializedSpec, err := serviceDefinitionFiles.ReadFile(integrationJsonPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"couldn't find integration.json in %s: %w",
|
||||
dirpath, err,
|
||||
)
|
||||
}
|
||||
|
||||
integrationSpec, err := koanfJson.Parser().Unmarshal(serializedSpec)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"couldn't parse integration.json from %s: %w",
|
||||
integrationJsonPath, err,
|
||||
)
|
||||
}
|
||||
|
||||
hydrated, err := integrations.HydrateFileUris(
|
||||
integrationSpec, serviceDefinitionFiles, dirpath,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"couldn't hydrate files referenced in service definition %s: %w",
|
||||
integrationJsonPath, err,
|
||||
)
|
||||
}
|
||||
|
||||
hydratedSpec := hydrated.(map[string]interface{})
|
||||
hydratedSpecJson, err := koanfJson.Parser().Marshal(hydratedSpec)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"couldn't serialize hydrated integration spec back to JSON %s: %w",
|
||||
integrationJsonPath, err,
|
||||
)
|
||||
}
|
||||
|
||||
var serviceDef CloudServiceDetails
|
||||
decoder := json.NewDecoder(bytes.NewReader(hydratedSpecJson))
|
||||
decoder.DisallowUnknownFields()
|
||||
err = decoder.Decode(&serviceDef)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"couldn't parse hydrated JSON spec read from %s: %w",
|
||||
integrationJsonPath, err,
|
||||
)
|
||||
}
|
||||
|
||||
err = validateServiceDefinition(serviceDef)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid service definition %s: %w", serviceDef.Id, err)
|
||||
}
|
||||
|
||||
return &serviceDef, nil
|
||||
|
||||
}
|
||||
|
||||
func validateServiceDefinition(s CloudServiceDetails) error {
|
||||
// Validate dashboard data
|
||||
seenDashboardIds := map[string]interface{}{}
|
||||
for _, dd := range s.Assets.Dashboards {
|
||||
did, exists := dd["id"]
|
||||
if !exists {
|
||||
return fmt.Errorf("id is required. not specified in dashboard titled %v", dd["title"])
|
||||
}
|
||||
dashboardId, ok := did.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("id must be string in dashboard titled %v", dd["title"])
|
||||
}
|
||||
if _, seen := seenDashboardIds[dashboardId]; seen {
|
||||
return fmt.Errorf("multiple dashboards found with id %s", dashboardId)
|
||||
}
|
||||
seenDashboardIds[dashboardId] = nil
|
||||
}
|
||||
|
||||
// potentially more to follow
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package cloudintegrations
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.signoz.io/signoz/pkg/query-service/model"
|
||||
)
|
||||
|
||||
func TestAvailableServices(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
// should be able to list available services.
|
||||
_, apiErr := listCloudProviderServices("bad-cloud-provider")
|
||||
require.NotNil(apiErr)
|
||||
require.Equal(model.ErrorNotFound, apiErr.Type())
|
||||
|
||||
awsSvcs, apiErr := listCloudProviderServices("aws")
|
||||
require.Nil(apiErr)
|
||||
require.Greater(len(awsSvcs), 0)
|
||||
|
||||
// should be able to get details of a service
|
||||
_, apiErr = getCloudProviderService(
|
||||
"aws", "bad-service-id",
|
||||
)
|
||||
require.NotNil(apiErr)
|
||||
require.Equal(model.ErrorNotFound, apiErr.Type())
|
||||
|
||||
svc, apiErr := getCloudProviderService(
|
||||
"aws", awsSvcs[0].Id,
|
||||
)
|
||||
require.Nil(apiErr)
|
||||
require.Equal(*svc, awsSvcs[0])
|
||||
}
|
||||
@@ -22,19 +22,26 @@ func validateCloudProviderName(name string) *model.ApiError {
|
||||
}
|
||||
|
||||
type Controller struct {
|
||||
repo cloudProviderAccountsRepository
|
||||
accountsRepo cloudProviderAccountsRepository
|
||||
serviceConfigRepo serviceConfigRepository
|
||||
}
|
||||
|
||||
func NewController(db *sqlx.DB) (
|
||||
*Controller, error,
|
||||
) {
|
||||
repo, err := newCloudProviderAccountsRepository(db)
|
||||
accountsRepo, err := newCloudProviderAccountsRepository(db)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("couldn't create cloud provider accounts repo: %w", err)
|
||||
}
|
||||
|
||||
serviceConfigRepo, err := newServiceConfigRepository(db)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("couldn't create cloud provider service config repo: %w", err)
|
||||
}
|
||||
|
||||
return &Controller{
|
||||
repo: repo,
|
||||
accountsRepo: accountsRepo,
|
||||
serviceConfigRepo: serviceConfigRepo,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -58,7 +65,7 @@ func (c *Controller) ListConnectedAccounts(
|
||||
return nil, apiErr
|
||||
}
|
||||
|
||||
accountRecords, apiErr := c.repo.listConnected(ctx, cloudProvider)
|
||||
accountRecords, apiErr := c.accountsRepo.listConnected(ctx, cloudProvider)
|
||||
if apiErr != nil {
|
||||
return nil, model.WrapApiError(apiErr, "couldn't list cloud accounts")
|
||||
}
|
||||
@@ -100,7 +107,7 @@ func (c *Controller) GenerateConnectionUrl(
|
||||
return nil, model.BadRequest(fmt.Errorf("unsupported cloud provider: %s", cloudProvider))
|
||||
}
|
||||
|
||||
account, apiErr := c.repo.upsert(
|
||||
account, apiErr := c.accountsRepo.upsert(
|
||||
ctx, cloudProvider, req.AccountId, &req.AccountConfig, nil, nil, nil,
|
||||
)
|
||||
if apiErr != nil {
|
||||
@@ -120,8 +127,9 @@ func (c *Controller) GenerateConnectionUrl(
|
||||
}
|
||||
|
||||
type AccountStatusResponse struct {
|
||||
Id string `json:"id"`
|
||||
Status AccountStatus `json:"status"`
|
||||
Id string `json:"id"`
|
||||
CloudAccountId *string `json:"cloud_account_id,omitempty"`
|
||||
Status AccountStatus `json:"status"`
|
||||
}
|
||||
|
||||
func (c *Controller) GetAccountStatus(
|
||||
@@ -133,14 +141,15 @@ func (c *Controller) GetAccountStatus(
|
||||
return nil, apiErr
|
||||
}
|
||||
|
||||
account, apiErr := c.repo.get(ctx, cloudProvider, accountId)
|
||||
account, apiErr := c.accountsRepo.get(ctx, cloudProvider, accountId)
|
||||
if apiErr != nil {
|
||||
return nil, apiErr
|
||||
}
|
||||
|
||||
resp := AccountStatusResponse{
|
||||
Id: account.Id,
|
||||
Status: account.status(),
|
||||
Id: account.Id,
|
||||
CloudAccountId: account.CloudAccountId,
|
||||
Status: account.status(),
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
@@ -164,7 +173,7 @@ func (c *Controller) CheckInAsAgent(
|
||||
return nil, apiErr
|
||||
}
|
||||
|
||||
existingAccount, apiErr := c.repo.get(ctx, cloudProvider, req.AccountId)
|
||||
existingAccount, apiErr := c.accountsRepo.get(ctx, cloudProvider, req.AccountId)
|
||||
if existingAccount != nil && existingAccount.CloudAccountId != nil && *existingAccount.CloudAccountId != req.CloudAccountId {
|
||||
return nil, model.BadRequest(fmt.Errorf(
|
||||
"can't check in with new %s account id %s for account %s with existing %s id %s",
|
||||
@@ -172,7 +181,7 @@ func (c *Controller) CheckInAsAgent(
|
||||
))
|
||||
}
|
||||
|
||||
existingAccount, apiErr = c.repo.getConnectedCloudAccount(ctx, cloudProvider, req.CloudAccountId)
|
||||
existingAccount, apiErr = c.accountsRepo.getConnectedCloudAccount(ctx, cloudProvider, req.CloudAccountId)
|
||||
if existingAccount != nil && existingAccount.Id != req.AccountId {
|
||||
return nil, model.BadRequest(fmt.Errorf(
|
||||
"can't check in to %s account %s with id %s. already connected with id %s",
|
||||
@@ -185,7 +194,7 @@ func (c *Controller) CheckInAsAgent(
|
||||
Data: req.Data,
|
||||
}
|
||||
|
||||
account, apiErr := c.repo.upsert(
|
||||
account, apiErr := c.accountsRepo.upsert(
|
||||
ctx, cloudProvider, &req.AccountId, nil, &req.CloudAccountId, &agentReport, nil,
|
||||
)
|
||||
if apiErr != nil {
|
||||
@@ -211,7 +220,7 @@ func (c *Controller) UpdateAccountConfig(
|
||||
return nil, apiErr
|
||||
}
|
||||
|
||||
accountRecord, apiErr := c.repo.upsert(
|
||||
accountRecord, apiErr := c.accountsRepo.upsert(
|
||||
ctx, cloudProvider, &accountId, &req.Config, nil, nil, nil,
|
||||
)
|
||||
if apiErr != nil {
|
||||
@@ -230,13 +239,13 @@ func (c *Controller) DisconnectAccount(
|
||||
return nil, apiErr
|
||||
}
|
||||
|
||||
account, apiErr := c.repo.get(ctx, cloudProvider, accountId)
|
||||
account, apiErr := c.accountsRepo.get(ctx, cloudProvider, accountId)
|
||||
if apiErr != nil {
|
||||
return nil, model.WrapApiError(apiErr, "couldn't disconnect account")
|
||||
}
|
||||
|
||||
tsNow := time.Now()
|
||||
account, apiErr = c.repo.upsert(
|
||||
account, apiErr = c.accountsRepo.upsert(
|
||||
ctx, cloudProvider, &accountId, nil, nil, nil, &tsNow,
|
||||
)
|
||||
if apiErr != nil {
|
||||
@@ -245,3 +254,127 @@ func (c *Controller) DisconnectAccount(
|
||||
|
||||
return account, nil
|
||||
}
|
||||
|
||||
type ListServicesResponse struct {
|
||||
Services []CloudServiceSummary `json:"services"`
|
||||
}
|
||||
|
||||
func (c *Controller) ListServices(
|
||||
ctx context.Context,
|
||||
cloudProvider string,
|
||||
cloudAccountId *string,
|
||||
) (*ListServicesResponse, *model.ApiError) {
|
||||
|
||||
if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil {
|
||||
return nil, apiErr
|
||||
}
|
||||
|
||||
services, apiErr := listCloudProviderServices(cloudProvider)
|
||||
if apiErr != nil {
|
||||
return nil, model.WrapApiError(apiErr, "couldn't list cloud services")
|
||||
}
|
||||
|
||||
svcConfigs := map[string]*CloudServiceConfig{}
|
||||
if cloudAccountId != nil {
|
||||
svcConfigs, apiErr = c.serviceConfigRepo.getAllForAccount(
|
||||
ctx, cloudProvider, *cloudAccountId,
|
||||
)
|
||||
if apiErr != nil {
|
||||
return nil, model.WrapApiError(
|
||||
apiErr, "couldn't get service configs for cloud account",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
summaries := []CloudServiceSummary{}
|
||||
for _, s := range services {
|
||||
summary := s.CloudServiceSummary
|
||||
summary.Config = svcConfigs[summary.Id]
|
||||
|
||||
summaries = append(summaries, summary)
|
||||
}
|
||||
|
||||
return &ListServicesResponse{
|
||||
Services: summaries,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Controller) GetServiceDetails(
|
||||
ctx context.Context,
|
||||
cloudProvider string,
|
||||
serviceId string,
|
||||
cloudAccountId *string,
|
||||
) (*CloudServiceDetails, *model.ApiError) {
|
||||
|
||||
if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil {
|
||||
return nil, apiErr
|
||||
}
|
||||
|
||||
service, apiErr := getCloudProviderService(cloudProvider, serviceId)
|
||||
if apiErr != nil {
|
||||
return nil, apiErr
|
||||
}
|
||||
|
||||
if cloudAccountId != nil {
|
||||
config, apiErr := c.serviceConfigRepo.get(
|
||||
ctx, cloudProvider, *cloudAccountId, serviceId,
|
||||
)
|
||||
if apiErr != nil && apiErr.Type() != model.ErrorNotFound {
|
||||
return nil, model.WrapApiError(apiErr, "couldn't fetch service config")
|
||||
}
|
||||
|
||||
if config != nil {
|
||||
service.Config = config
|
||||
}
|
||||
}
|
||||
|
||||
return service, nil
|
||||
}
|
||||
|
||||
type UpdateServiceConfigRequest struct {
|
||||
CloudAccountId string `json:"cloud_account_id"`
|
||||
Config CloudServiceConfig `json:"config"`
|
||||
}
|
||||
|
||||
type UpdateServiceConfigResponse struct {
|
||||
Id string `json:"id"`
|
||||
Config CloudServiceConfig `json:"config"`
|
||||
}
|
||||
|
||||
func (c *Controller) UpdateServiceConfig(
|
||||
ctx context.Context,
|
||||
cloudProvider string,
|
||||
serviceId string,
|
||||
req UpdateServiceConfigRequest,
|
||||
) (*UpdateServiceConfigResponse, *model.ApiError) {
|
||||
|
||||
if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil {
|
||||
return nil, apiErr
|
||||
}
|
||||
|
||||
// can only update config for a connected cloud account id
|
||||
_, apiErr := c.accountsRepo.getConnectedCloudAccount(
|
||||
ctx, cloudProvider, req.CloudAccountId,
|
||||
)
|
||||
if apiErr != nil {
|
||||
return nil, model.WrapApiError(apiErr, "couldn't find connected cloud account")
|
||||
}
|
||||
|
||||
// can only update config for a valid service.
|
||||
_, apiErr = getCloudProviderService(cloudProvider, serviceId)
|
||||
if apiErr != nil {
|
||||
return nil, model.WrapApiError(apiErr, "unsupported service")
|
||||
}
|
||||
|
||||
updatedConfig, apiErr := c.serviceConfigRepo.upsert(
|
||||
ctx, cloudProvider, req.CloudAccountId, serviceId, req.Config,
|
||||
)
|
||||
if apiErr != nil {
|
||||
return nil, model.WrapApiError(apiErr, "couldn't update service config")
|
||||
}
|
||||
|
||||
return &UpdateServiceConfigResponse{
|
||||
Id: serviceId,
|
||||
Config: *updatedConfig,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ func TestRegenerateConnectionUrlWithUpdatedConfig(t *testing.T) {
|
||||
require.NotEmpty(resp1.AccountId)
|
||||
|
||||
testAccountId := resp1.AccountId
|
||||
account, apiErr := controller.repo.get(
|
||||
account, apiErr := controller.accountsRepo.get(
|
||||
context.TODO(), "aws", testAccountId,
|
||||
)
|
||||
require.Nil(apiErr)
|
||||
@@ -47,7 +47,7 @@ func TestRegenerateConnectionUrlWithUpdatedConfig(t *testing.T) {
|
||||
require.Nil(apiErr)
|
||||
require.Equal(testAccountId, resp2.AccountId)
|
||||
|
||||
account, apiErr = controller.repo.get(
|
||||
account, apiErr = controller.accountsRepo.get(
|
||||
context.TODO(), "aws", testAccountId,
|
||||
)
|
||||
require.Nil(apiErr)
|
||||
@@ -89,7 +89,7 @@ func TestAgentCheckIns(t *testing.T) {
|
||||
// if another connected AccountRecord exists for same cloud account
|
||||
// i.e. there can't be 2 connected account records for the same cloud account id
|
||||
// at any point in time.
|
||||
existingConnected, apiErr := controller.repo.getConnectedCloudAccount(
|
||||
existingConnected, apiErr := controller.accountsRepo.getConnectedCloudAccount(
|
||||
context.TODO(), "aws", testCloudAccountId1,
|
||||
)
|
||||
require.Nil(apiErr)
|
||||
@@ -112,7 +112,7 @@ func TestAgentCheckIns(t *testing.T) {
|
||||
context.TODO(), "aws", testAccountId1,
|
||||
)
|
||||
|
||||
existingConnected, apiErr = controller.repo.getConnectedCloudAccount(
|
||||
existingConnected, apiErr = controller.accountsRepo.getConnectedCloudAccount(
|
||||
context.TODO(), "aws", testCloudAccountId1,
|
||||
)
|
||||
require.Nil(existingConnected)
|
||||
@@ -151,3 +151,120 @@ func TestCantDisconnectNonExistentAccount(t *testing.T) {
|
||||
require.Equal(model.ErrorNotFound, apiErr.Type())
|
||||
require.Nil(account)
|
||||
}
|
||||
|
||||
func TestConfigureService(t *testing.T) {
|
||||
require := require.New(t)
|
||||
testDB, _ := utils.NewTestSqliteDB(t)
|
||||
controller, err := NewController(testDB)
|
||||
require.NoError(err)
|
||||
|
||||
testCloudAccountId := "546311234"
|
||||
|
||||
// should start out without any service config
|
||||
svcListResp, apiErr := controller.ListServices(
|
||||
context.TODO(), "aws", &testCloudAccountId,
|
||||
)
|
||||
require.Nil(apiErr)
|
||||
|
||||
testSvcId := svcListResp.Services[0].Id
|
||||
require.Nil(svcListResp.Services[0].Config)
|
||||
|
||||
svcDetails, apiErr := controller.GetServiceDetails(
|
||||
context.TODO(), "aws", testSvcId, &testCloudAccountId,
|
||||
)
|
||||
require.Nil(apiErr)
|
||||
require.Equal(testSvcId, svcDetails.Id)
|
||||
require.Nil(svcDetails.Config)
|
||||
|
||||
// should be able to configure a service for a connected account
|
||||
testConnectedAccount := makeTestConnectedAccount(t, controller, testCloudAccountId)
|
||||
require.Nil(testConnectedAccount.RemovedAt)
|
||||
require.NotNil(testConnectedAccount.CloudAccountId)
|
||||
require.Equal(testCloudAccountId, *testConnectedAccount.CloudAccountId)
|
||||
|
||||
testSvcConfig := CloudServiceConfig{
|
||||
Metrics: &CloudServiceMetricsConfig{
|
||||
Enabled: true,
|
||||
},
|
||||
}
|
||||
updateSvcConfigResp, apiErr := controller.UpdateServiceConfig(
|
||||
context.TODO(), "aws", testSvcId, UpdateServiceConfigRequest{
|
||||
CloudAccountId: testCloudAccountId,
|
||||
Config: testSvcConfig,
|
||||
},
|
||||
)
|
||||
require.Nil(apiErr)
|
||||
require.Equal(testSvcId, updateSvcConfigResp.Id)
|
||||
require.Equal(testSvcConfig, updateSvcConfigResp.Config)
|
||||
|
||||
svcDetails, apiErr = controller.GetServiceDetails(
|
||||
context.TODO(), "aws", testSvcId, &testCloudAccountId,
|
||||
)
|
||||
require.Nil(apiErr)
|
||||
require.Equal(testSvcId, svcDetails.Id)
|
||||
require.Equal(testSvcConfig, *svcDetails.Config)
|
||||
|
||||
svcListResp, apiErr = controller.ListServices(
|
||||
context.TODO(), "aws", &testCloudAccountId,
|
||||
)
|
||||
require.Nil(apiErr)
|
||||
for _, svc := range svcListResp.Services {
|
||||
if svc.Id == testSvcId {
|
||||
require.Equal(testSvcConfig, *svc.Config)
|
||||
}
|
||||
}
|
||||
|
||||
// should not be able to configure service after cloud account has been disconnected
|
||||
_, apiErr = controller.DisconnectAccount(
|
||||
context.TODO(), "aws", testConnectedAccount.Id,
|
||||
)
|
||||
require.Nil(apiErr)
|
||||
|
||||
_, apiErr = controller.UpdateServiceConfig(
|
||||
context.TODO(), "aws", testSvcId,
|
||||
UpdateServiceConfigRequest{
|
||||
CloudAccountId: testCloudAccountId,
|
||||
Config: testSvcConfig,
|
||||
},
|
||||
)
|
||||
require.NotNil(apiErr)
|
||||
|
||||
// should not be able to configure a service for a cloud account id that is not connected yet
|
||||
_, apiErr = controller.UpdateServiceConfig(
|
||||
context.TODO(), "aws", testSvcId,
|
||||
UpdateServiceConfigRequest{
|
||||
CloudAccountId: "9999999999",
|
||||
Config: testSvcConfig,
|
||||
},
|
||||
)
|
||||
require.NotNil(apiErr)
|
||||
|
||||
// should not be able to set config for an unsupported service
|
||||
_, apiErr = controller.UpdateServiceConfig(
|
||||
context.TODO(), "aws", "bad-service", UpdateServiceConfigRequest{
|
||||
CloudAccountId: testCloudAccountId,
|
||||
Config: testSvcConfig,
|
||||
},
|
||||
)
|
||||
require.NotNil(apiErr)
|
||||
|
||||
}
|
||||
|
||||
func makeTestConnectedAccount(t *testing.T, controller *Controller, cloudAccountId string) *AccountRecord {
|
||||
require := require.New(t)
|
||||
|
||||
// a check in from SigNoz agent creates or updates a connected account.
|
||||
testAccountId := uuid.NewString()
|
||||
resp, apiErr := controller.CheckInAsAgent(
|
||||
context.TODO(), "aws", AgentCheckInRequest{
|
||||
AccountId: testAccountId,
|
||||
CloudAccountId: cloudAccountId,
|
||||
},
|
||||
)
|
||||
require.Nil(apiErr)
|
||||
require.Equal(testAccountId, resp.Account.Id)
|
||||
require.Equal(cloudAccountId, *resp.Account.CloudAccountId)
|
||||
|
||||
return &resp.Account
|
||||
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.signoz.io/signoz/pkg/query-service/app/dashboards"
|
||||
)
|
||||
|
||||
// Represents a cloud provider account for cloud integrations
|
||||
@@ -115,3 +117,102 @@ func (a *AccountRecord) account() Account {
|
||||
|
||||
return ca
|
||||
}
|
||||
|
||||
type CloudServiceSummary struct {
|
||||
Id string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Icon string `json:"icon"`
|
||||
|
||||
// Present only if the service has been configured in the
|
||||
// context of a cloud provider account.
|
||||
Config *CloudServiceConfig `json:"config,omitempty"`
|
||||
}
|
||||
|
||||
type CloudServiceDetails struct {
|
||||
CloudServiceSummary
|
||||
|
||||
Overview string `json:"overview"` // markdown
|
||||
|
||||
Assets CloudServiceAssets `json:"assets"`
|
||||
|
||||
SupportedSignals SupportedSignals `json:"supported_signals"`
|
||||
|
||||
DataCollected DataCollectedForService `json:"data_collected"`
|
||||
|
||||
ConnectionStatus *CloudServiceConnectionStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
type CloudServiceConfig struct {
|
||||
Logs *CloudServiceLogsConfig `json:"logs,omitempty"`
|
||||
Metrics *CloudServiceMetricsConfig `json:"metrics,omitempty"`
|
||||
}
|
||||
|
||||
// For serializing from db
|
||||
func (c *CloudServiceConfig) Scan(src any) error {
|
||||
data, ok := src.([]byte)
|
||||
if !ok {
|
||||
return fmt.Errorf("tried to scan from %T instead of bytes", src)
|
||||
}
|
||||
|
||||
return json.Unmarshal(data, &c)
|
||||
}
|
||||
|
||||
// For serializing to db
|
||||
func (c *CloudServiceConfig) Value() (driver.Value, error) {
|
||||
if c == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
serialized, err := json.Marshal(c)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"couldn't serialize cloud service config to JSON: %w", err,
|
||||
)
|
||||
}
|
||||
return serialized, nil
|
||||
}
|
||||
|
||||
type CloudServiceLogsConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
type CloudServiceMetricsConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
type CloudServiceAssets struct {
|
||||
Dashboards []dashboards.Data `json:"dashboards"`
|
||||
}
|
||||
|
||||
type SupportedSignals struct {
|
||||
Logs bool `json:"logs"`
|
||||
Metrics bool `json:"metrics"`
|
||||
}
|
||||
|
||||
type DataCollectedForService struct {
|
||||
Logs []CollectedLogAttribute `json:"logs"`
|
||||
Metrics []CollectedMetric `json:"metrics"`
|
||||
}
|
||||
|
||||
type CollectedLogAttribute struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type CollectedMetric struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Unit string `json:"unit"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type CloudServiceConnectionStatus struct {
|
||||
Logs *SignalConnectionStatus `json:"logs"`
|
||||
Metrics *SignalConnectionStatus `json:"metrics"`
|
||||
}
|
||||
|
||||
type SignalConnectionStatus struct {
|
||||
LastReceivedTsMillis int64 `json:"last_received_ts_ms"` // epoch milliseconds
|
||||
LastReceivedFrom string `json:"last_received_from"` // resource identifier
|
||||
}
|
||||
|
||||
198
pkg/query-service/app/cloudintegrations/serviceConfigRepo.go
Normal file
198
pkg/query-service/app/cloudintegrations/serviceConfigRepo.go
Normal file
@@ -0,0 +1,198 @@
|
||||
package cloudintegrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"go.signoz.io/signoz/pkg/query-service/model"
|
||||
)
|
||||
|
||||
type serviceConfigRepository interface {
|
||||
get(
|
||||
ctx context.Context,
|
||||
cloudProvider string,
|
||||
cloudAccountId string,
|
||||
serviceId string,
|
||||
) (*CloudServiceConfig, *model.ApiError)
|
||||
|
||||
upsert(
|
||||
ctx context.Context,
|
||||
cloudProvider string,
|
||||
cloudAccountId string,
|
||||
serviceId string,
|
||||
config CloudServiceConfig,
|
||||
) (*CloudServiceConfig, *model.ApiError)
|
||||
|
||||
getAllForAccount(
|
||||
ctx context.Context,
|
||||
cloudProvider string,
|
||||
cloudAccountId string,
|
||||
) (
|
||||
configsBySvcId map[string]*CloudServiceConfig,
|
||||
apiErr *model.ApiError,
|
||||
)
|
||||
}
|
||||
|
||||
func newServiceConfigRepository(db *sqlx.DB) (
|
||||
*serviceConfigSQLRepository, error,
|
||||
) {
|
||||
|
||||
if err := initServiceConfigSqliteDBIfNeeded(db); err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"could not init sqlite DB for cloudintegrations service configs: %w", err,
|
||||
)
|
||||
}
|
||||
|
||||
return &serviceConfigSQLRepository{
|
||||
db: db,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func initServiceConfigSqliteDBIfNeeded(db *sqlx.DB) error {
|
||||
|
||||
if db == nil {
|
||||
return fmt.Errorf("db is required")
|
||||
}
|
||||
|
||||
createTableStatement := `
|
||||
CREATE TABLE IF NOT EXISTS cloud_integrations_service_configs(
|
||||
cloud_provider TEXT NOT NULL,
|
||||
cloud_account_id TEXT NOT NULL,
|
||||
service_id TEXT NOT NULL,
|
||||
config_json TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
UNIQUE(cloud_provider, cloud_account_id, service_id)
|
||||
)
|
||||
`
|
||||
_, err := db.Exec(createTableStatement)
|
||||
if err != nil {
|
||||
return fmt.Errorf(
|
||||
"could not ensure cloud provider service configs schema in sqlite DB: %w", err,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type serviceConfigSQLRepository struct {
|
||||
db *sqlx.DB
|
||||
}
|
||||
|
||||
func (r *serviceConfigSQLRepository) get(
|
||||
ctx context.Context,
|
||||
cloudProvider string,
|
||||
cloudAccountId string,
|
||||
serviceId string,
|
||||
) (*CloudServiceConfig, *model.ApiError) {
|
||||
|
||||
var result CloudServiceConfig
|
||||
|
||||
err := r.db.GetContext(
|
||||
ctx, &result, `
|
||||
select
|
||||
config_json
|
||||
from cloud_integrations_service_configs
|
||||
where
|
||||
cloud_provider=$1
|
||||
and cloud_account_id=$2
|
||||
and service_id=$3
|
||||
`,
|
||||
cloudProvider, cloudAccountId, serviceId,
|
||||
)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, model.NotFoundError(fmt.Errorf(
|
||||
"couldn't find %s %s config for %s",
|
||||
cloudProvider, serviceId, cloudAccountId,
|
||||
))
|
||||
|
||||
} else if err != nil {
|
||||
return nil, model.InternalError(fmt.Errorf(
|
||||
"couldn't query cloud service config: %w", err,
|
||||
))
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
|
||||
}
|
||||
|
||||
func (r *serviceConfigSQLRepository) upsert(
|
||||
ctx context.Context,
|
||||
cloudProvider string,
|
||||
cloudAccountId string,
|
||||
serviceId string,
|
||||
config CloudServiceConfig,
|
||||
) (*CloudServiceConfig, *model.ApiError) {
|
||||
|
||||
query := `
|
||||
INSERT INTO cloud_integrations_service_configs (
|
||||
cloud_provider,
|
||||
cloud_account_id,
|
||||
service_id,
|
||||
config_json
|
||||
) values ($1, $2, $3, $4)
|
||||
on conflict(cloud_provider, cloud_account_id, service_id)
|
||||
do update set config_json=excluded.config_json
|
||||
`
|
||||
_, dbErr := r.db.ExecContext(
|
||||
ctx, query,
|
||||
cloudProvider, cloudAccountId, serviceId, &config,
|
||||
)
|
||||
if dbErr != nil {
|
||||
return nil, model.InternalError(fmt.Errorf(
|
||||
"could not upsert cloud service config: %w", dbErr,
|
||||
))
|
||||
}
|
||||
|
||||
upsertedConfig, apiErr := r.get(ctx, cloudProvider, cloudAccountId, serviceId)
|
||||
if apiErr != nil {
|
||||
return nil, model.InternalError(fmt.Errorf(
|
||||
"couldn't fetch upserted service config: %w", apiErr.ToError(),
|
||||
))
|
||||
}
|
||||
|
||||
return upsertedConfig, nil
|
||||
|
||||
}
|
||||
|
||||
func (r *serviceConfigSQLRepository) getAllForAccount(
|
||||
ctx context.Context,
|
||||
cloudProvider string,
|
||||
cloudAccountId string,
|
||||
) (map[string]*CloudServiceConfig, *model.ApiError) {
|
||||
|
||||
type ScannedServiceConfigRecord struct {
|
||||
ServiceId string `db:"service_id"`
|
||||
Config CloudServiceConfig `db:"config_json"`
|
||||
}
|
||||
|
||||
records := []ScannedServiceConfigRecord{}
|
||||
|
||||
err := r.db.SelectContext(
|
||||
ctx, &records, `
|
||||
select
|
||||
service_id,
|
||||
config_json
|
||||
from cloud_integrations_service_configs
|
||||
where
|
||||
cloud_provider=$1
|
||||
and cloud_account_id=$2
|
||||
`,
|
||||
cloudProvider, cloudAccountId,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, model.InternalError(fmt.Errorf(
|
||||
"could not query service configs from db: %w", err,
|
||||
))
|
||||
}
|
||||
|
||||
result := map[string]*CloudServiceConfig{}
|
||||
|
||||
for _, r := range records {
|
||||
result[r.ServiceId] = &r.Config
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none">
|
||||
<path fill="#9D5025" d="M1.702 2.98L1 3.312v9.376l.702.332 2.842-4.777L1.702 2.98z" />
|
||||
<path fill="#F58536" d="M3.339 12.657l-1.637.363V2.98l1.637.353v9.324z" />
|
||||
<path fill="#9D5025" d="M2.476 2.612l.863-.406 4.096 6.216-4.096 5.372-.863-.406V2.612z" />
|
||||
<path fill="#F58536" d="M5.38 13.248l-2.041.546V2.206l2.04.548v10.494z" />
|
||||
<path fill="#9D5025" d="M4.3 1.75l1.08-.512 6.043 7.864-6.043 5.66-1.08-.511V1.749z" />
|
||||
<path fill="#F58536" d="M7.998 13.856l-2.618.906V1.238l2.618.908v11.71z" />
|
||||
<path fill="#9D5025" d="M6.602.66L7.998 0l6.538 8.453L7.998 16l-1.396-.66V.66z" />
|
||||
<path fill="#F58536" d="M15 12.686L7.998 16V0L15 3.314v9.372z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 805 B |
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"id": "ec2",
|
||||
"title": "EC2",
|
||||
"icon": "file://icon.svg",
|
||||
"overview": "file://overview.md",
|
||||
"assets": {
|
||||
"dashboards": []
|
||||
},
|
||||
"supported_signals": {
|
||||
"metrics": true,
|
||||
"logs": false
|
||||
},
|
||||
"data_collected": {
|
||||
"metrics": [
|
||||
{
|
||||
"name": "ec2_cpuutilization_average",
|
||||
"type": "Gauge",
|
||||
"unit": "number",
|
||||
"description": "CloudWatch metric CPUUtilization"
|
||||
},
|
||||
{
|
||||
"name": "ec2_cpuutilization_maximum",
|
||||
"type": "Gauge",
|
||||
"unit": "number",
|
||||
"description": "CloudWatch metric CPUUtilization"
|
||||
}
|
||||
],
|
||||
"logs": []
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
### Monitor EC2 with SigNoz
|
||||
|
||||
Collect key EC2 metrics and view them with an out of the box dashboard.
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="80px" height="80px" viewBox="0 0 80 80" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>Icon-Architecture/64/Arch_Amazon-RDS_64</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
<linearGradient x1="0%" y1="100%" x2="100%" y2="0%" id="linearGradient-1">
|
||||
<stop stop-color="#2E27AD" offset="0%"></stop>
|
||||
<stop stop-color="#527FFF" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g id="Icon-Architecture/64/Arch_Amazon-RDS_64" stroke="none" stroke-width="1" fill="none"
|
||||
fill-rule="evenodd">
|
||||
<g id="Icon-Architecture-BG/64/Database" fill="url(#linearGradient-1)">
|
||||
<rect id="Rectangle" x="0" y="0" width="80" height="80"></rect>
|
||||
</g>
|
||||
<path
|
||||
d="M15.414,14 L24.707,23.293 L23.293,24.707 L14,15.414 L14,23 L12,23 L12,13 C12,12.448 12.447,12 13,12 L23,12 L23,14 L15.414,14 Z M68,13 L68,23 L66,23 L66,15.414 L56.707,24.707 L55.293,23.293 L64.586,14 L57,14 L57,12 L67,12 C67.553,12 68,12.448 68,13 L68,13 Z M66,57 L68,57 L68,67 C68,67.552 67.553,68 67,68 L57,68 L57,66 L64.586,66 L55.293,56.707 L56.707,55.293 L66,64.586 L66,57 Z M65.5,39.213 C65.5,35.894 61.668,32.615 55.25,30.442 L55.891,28.548 C63.268,31.045 67.5,34.932 67.5,39.213 C67.5,43.495 63.268,47.383 55.89,49.879 L55.249,47.984 C61.668,45.812 65.5,42.534 65.5,39.213 L65.5,39.213 Z M14.556,39.213 C14.556,42.393 18.143,45.585 24.152,47.753 L23.473,49.634 C16.535,47.131 12.556,43.333 12.556,39.213 C12.556,35.094 16.535,31.296 23.473,28.792 L24.152,30.673 C18.143,32.842 14.556,36.034 14.556,39.213 L14.556,39.213 Z M24.707,56.707 L15.414,66 L23,66 L23,68 L13,68 C12.447,68 12,67.552 12,67 L12,57 L14,57 L14,64.586 L23.293,55.293 L24.707,56.707 Z M40,31.286 C32.854,31.286 29,29.44 29,28.686 C29,27.931 32.854,26.086 40,26.086 C47.145,26.086 51,27.931 51,28.686 C51,29.44 47.145,31.286 40,31.286 L40,31.286 Z M40.029,39.031 C33.187,39.031 29,37.162 29,36.145 L29,31.284 C31.463,32.643 35.832,33.286 40,33.286 C44.168,33.286 48.537,32.643 51,31.284 L51,36.145 C51,37.163 46.835,39.031 40.029,39.031 L40.029,39.031 Z M40.029,46.667 C33.187,46.667 29,44.798 29,43.781 L29,38.862 C31.431,40.291 35.742,41.031 40.029,41.031 C44.292,41.031 48.578,40.292 51,38.867 L51,43.781 C51,44.799 46.835,46.667 40.029,46.667 L40.029,46.667 Z M40,53.518 C32.883,53.518 29,51.605 29,50.622 L29,46.498 C31.431,47.927 35.742,48.667 40.029,48.667 C44.292,48.667 48.578,47.929 51,46.503 L51,50.622 C51,51.605 47.117,53.518 40,53.518 L40,53.518 Z M40,24.086 C33.739,24.086 27,25.525 27,28.686 L27,50.622 C27,53.836 33.54,55.518 40,55.518 C46.46,55.518 53,53.836 53,50.622 L53,28.686 C53,25.525 46.261,24.086 40,24.086 L40,24.086 Z"
|
||||
id="Amazon-RDS_Icon_64_Squid" fill="#FFFFFF"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"id": "rds-postgres",
|
||||
"title": "RDS Postgres",
|
||||
"icon": "file://icon.svg",
|
||||
"overview": "file://overview.md",
|
||||
"assets": {
|
||||
"dashboards": []
|
||||
},
|
||||
"supported_signals": {
|
||||
"metrics": true,
|
||||
"logs": true
|
||||
},
|
||||
"data_collected": {
|
||||
"metrics": [
|
||||
{
|
||||
"name": "rds_postgres_cpuutilization_average",
|
||||
"type": "Gauge",
|
||||
"unit": "number",
|
||||
"description": "CloudWatch metric CPUUtilization"
|
||||
},
|
||||
{
|
||||
"name": "rds_postgres_cpuutilization_maximum",
|
||||
"type": "Gauge",
|
||||
"unit": "number",
|
||||
"description": "CloudWatch metric CPUUtilization"
|
||||
}
|
||||
],
|
||||
"logs": []
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
### Monitor RDS Postgres with SigNoz
|
||||
|
||||
Collect key RDS Postgres metrics and view them with an out of the box dashboard.
|
||||
@@ -3902,6 +3902,18 @@ func (aH *APIHandler) RegisterCloudIntegrationsRoutes(router *mux.Router, am *Au
|
||||
"/{cloudProvider}/agent-check-in", am.EditAccess(aH.CloudIntegrationsAgentCheckIn),
|
||||
).Methods(http.MethodPost)
|
||||
|
||||
subRouter.HandleFunc(
|
||||
"/{cloudProvider}/services", am.ViewAccess(aH.CloudIntegrationsListServices),
|
||||
).Methods(http.MethodGet)
|
||||
|
||||
subRouter.HandleFunc(
|
||||
"/{cloudProvider}/services/{serviceId}", am.ViewAccess(aH.CloudIntegrationsGetServiceDetails),
|
||||
).Methods(http.MethodGet)
|
||||
|
||||
subRouter.HandleFunc(
|
||||
"/{cloudProvider}/services/{serviceId}/config", am.EditAccess(aH.CloudIntegrationsUpdateServiceConfig),
|
||||
).Methods(http.MethodPost)
|
||||
|
||||
}
|
||||
|
||||
func (aH *APIHandler) CloudIntegrationsListConnectedAccounts(
|
||||
@@ -4025,6 +4037,77 @@ func (aH *APIHandler) CloudIntegrationsDisconnectAccount(
|
||||
aH.Respond(w, result)
|
||||
}
|
||||
|
||||
func (aH *APIHandler) CloudIntegrationsListServices(
|
||||
w http.ResponseWriter, r *http.Request,
|
||||
) {
|
||||
cloudProvider := mux.Vars(r)["cloudProvider"]
|
||||
|
||||
var cloudAccountId *string
|
||||
|
||||
cloudAccountIdQP := r.URL.Query().Get("cloud_account_id")
|
||||
if len(cloudAccountIdQP) > 0 {
|
||||
cloudAccountId = &cloudAccountIdQP
|
||||
}
|
||||
|
||||
resp, apiErr := aH.CloudIntegrationsController.ListServices(
|
||||
r.Context(), cloudProvider, cloudAccountId,
|
||||
)
|
||||
|
||||
if apiErr != nil {
|
||||
RespondError(w, apiErr, nil)
|
||||
return
|
||||
}
|
||||
aH.Respond(w, resp)
|
||||
}
|
||||
|
||||
func (aH *APIHandler) CloudIntegrationsGetServiceDetails(
|
||||
w http.ResponseWriter, r *http.Request,
|
||||
) {
|
||||
cloudProvider := mux.Vars(r)["cloudProvider"]
|
||||
serviceId := mux.Vars(r)["serviceId"]
|
||||
|
||||
var cloudAccountId *string
|
||||
|
||||
cloudAccountIdQP := r.URL.Query().Get("cloud_account_id")
|
||||
if len(cloudAccountIdQP) > 0 {
|
||||
cloudAccountId = &cloudAccountIdQP
|
||||
}
|
||||
|
||||
resp, apiErr := aH.CloudIntegrationsController.GetServiceDetails(
|
||||
r.Context(), cloudProvider, serviceId, cloudAccountId,
|
||||
)
|
||||
|
||||
if apiErr != nil {
|
||||
RespondError(w, apiErr, nil)
|
||||
return
|
||||
}
|
||||
aH.Respond(w, resp)
|
||||
}
|
||||
|
||||
func (aH *APIHandler) CloudIntegrationsUpdateServiceConfig(
|
||||
w http.ResponseWriter, r *http.Request,
|
||||
) {
|
||||
cloudProvider := mux.Vars(r)["cloudProvider"]
|
||||
serviceId := mux.Vars(r)["serviceId"]
|
||||
|
||||
req := cloudintegrations.UpdateServiceConfigRequest{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
RespondError(w, model.BadRequest(err), nil)
|
||||
return
|
||||
}
|
||||
|
||||
result, apiErr := aH.CloudIntegrationsController.UpdateServiceConfig(
|
||||
r.Context(), cloudProvider, serviceId, req,
|
||||
)
|
||||
|
||||
if apiErr != nil {
|
||||
RespondError(w, apiErr, nil)
|
||||
return
|
||||
}
|
||||
|
||||
aH.Respond(w, result)
|
||||
}
|
||||
|
||||
// logs
|
||||
func (aH *APIHandler) RegisterLogsRoutes(router *mux.Router, am *AuthMiddleware) {
|
||||
subRouter := router.PathPrefix("/api/v1/logs").Subrouter()
|
||||
|
||||
@@ -105,7 +105,7 @@ func readBuiltInIntegration(dirpath string) (
|
||||
)
|
||||
}
|
||||
|
||||
hydrated, err := hydrateFileUris(integrationSpec, dirpath)
|
||||
hydrated, err := HydrateFileUris(integrationSpec, integrationFiles, dirpath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"couldn't hydrate files referenced in integration %s: %w", integrationJsonPath, err,
|
||||
@@ -172,11 +172,11 @@ func validateIntegration(i IntegrationDetails) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func hydrateFileUris(spec interface{}, basedir string) (interface{}, error) {
|
||||
func HydrateFileUris(spec interface{}, fs embed.FS, basedir string) (interface{}, error) {
|
||||
if specMap, ok := spec.(map[string]interface{}); ok {
|
||||
result := map[string]interface{}{}
|
||||
for k, v := range specMap {
|
||||
hydrated, err := hydrateFileUris(v, basedir)
|
||||
hydrated, err := HydrateFileUris(v, fs, basedir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -187,7 +187,7 @@ func hydrateFileUris(spec interface{}, basedir string) (interface{}, error) {
|
||||
} else if specSlice, ok := spec.([]interface{}); ok {
|
||||
result := []interface{}{}
|
||||
for _, v := range specSlice {
|
||||
hydrated, err := hydrateFileUris(v, basedir)
|
||||
hydrated, err := HydrateFileUris(v, fs, basedir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -196,14 +196,14 @@ func hydrateFileUris(spec interface{}, basedir string) (interface{}, error) {
|
||||
return result, nil
|
||||
|
||||
} else if maybeFileUri, ok := spec.(string); ok {
|
||||
return readFileIfUri(maybeFileUri, basedir)
|
||||
return readFileIfUri(fs, maybeFileUri, basedir)
|
||||
}
|
||||
|
||||
return spec, nil
|
||||
|
||||
}
|
||||
|
||||
func readFileIfUri(maybeFileUri string, basedir string) (interface{}, error) {
|
||||
func readFileIfUri(fs embed.FS, maybeFileUri string, basedir string) (interface{}, error) {
|
||||
fileUriPrefix := "file://"
|
||||
if !strings.HasPrefix(maybeFileUri, fileUriPrefix) {
|
||||
return maybeFileUri, nil
|
||||
@@ -212,7 +212,7 @@ func readFileIfUri(maybeFileUri string, basedir string) (interface{}, error) {
|
||||
relativePath := maybeFileUri[len(fileUriPrefix):]
|
||||
fullPath := path.Join(basedir, relativePath)
|
||||
|
||||
fileContents, err := integrationFiles.ReadFile(fullPath)
|
||||
fileContents, err := fs.ReadFile(fullPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("couldn't read referenced file: %w", err)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jmoiron/sqlx"
|
||||
mockhouse "github.com/srikanthccv/ClickHouse-go-mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -19,7 +20,7 @@ import (
|
||||
"go.signoz.io/signoz/pkg/query-service/utils"
|
||||
)
|
||||
|
||||
func TestAWSIntegrationLifecycle(t *testing.T) {
|
||||
func TestAWSIntegrationAccountLifecycle(t *testing.T) {
|
||||
// Test for happy path of connecting and managing AWS integration accounts
|
||||
|
||||
t0 := time.Now()
|
||||
@@ -51,6 +52,7 @@ func TestAWSIntegrationLifecycle(t *testing.T) {
|
||||
accountStatusResp := testbed.GetAccountStatusFromQS("aws", testAccountId)
|
||||
require.Equal(testAccountId, accountStatusResp.Id)
|
||||
require.Nil(accountStatusResp.Status.Integration.LastHeartbeatTsMillis)
|
||||
require.Nil(accountStatusResp.CloudAccountId)
|
||||
|
||||
// The unconnected account should not show up in connected accounts list yet
|
||||
accountsListResp1 := testbed.GetConnectedAccountsListFromQS("aws")
|
||||
@@ -74,6 +76,8 @@ func TestAWSIntegrationLifecycle(t *testing.T) {
|
||||
// Polling for connection status from UI should now return latest status
|
||||
accountStatusResp1 := testbed.GetAccountStatusFromQS("aws", testAccountId)
|
||||
require.Equal(testAccountId, accountStatusResp1.Id)
|
||||
require.NotNil(accountStatusResp1.CloudAccountId)
|
||||
require.Equal(testAWSAccountId, *accountStatusResp1.CloudAccountId)
|
||||
require.NotNil(accountStatusResp1.Status.Integration.LastHeartbeatTsMillis)
|
||||
require.LessOrEqual(
|
||||
tsMillisBeforeAgentCheckIn,
|
||||
@@ -126,6 +130,70 @@ func TestAWSIntegrationLifecycle(t *testing.T) {
|
||||
require.LessOrEqual(tsBeforeDisconnect, *agentCheckInResp2.Account.RemovedAt)
|
||||
}
|
||||
|
||||
func TestAWSIntegrationServices(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
testbed := NewCloudIntegrationsTestBed(t, nil)
|
||||
|
||||
// should be able to list available cloud services.
|
||||
svcListResp := testbed.GetServicesFromQS("aws", nil)
|
||||
require.Greater(len(svcListResp.Services), 0)
|
||||
for _, svc := range svcListResp.Services {
|
||||
require.NotEmpty(svc.Id)
|
||||
require.Nil(svc.Config)
|
||||
}
|
||||
|
||||
// should be able to get details of a particular service.
|
||||
svcId := svcListResp.Services[0].Id
|
||||
svcDetailResp := testbed.GetServiceDetailFromQS("aws", svcId, nil)
|
||||
require.Equal(svcId, svcDetailResp.Id)
|
||||
require.NotEmpty(svcDetailResp.Overview)
|
||||
require.Nil(svcDetailResp.Config)
|
||||
require.Nil(svcDetailResp.ConnectionStatus)
|
||||
|
||||
// should be able to configure a service in the ctx of a connected account
|
||||
|
||||
// create a connected account
|
||||
testAccountId := uuid.NewString()
|
||||
testAWSAccountId := "389389489489"
|
||||
testbed.CheckInAsAgentWithQS(
|
||||
"aws", cloudintegrations.AgentCheckInRequest{
|
||||
AccountId: testAccountId,
|
||||
CloudAccountId: testAWSAccountId,
|
||||
},
|
||||
)
|
||||
|
||||
testSvcConfig := cloudintegrations.CloudServiceConfig{
|
||||
Metrics: &cloudintegrations.CloudServiceMetricsConfig{
|
||||
Enabled: true,
|
||||
},
|
||||
}
|
||||
updateSvcConfigResp := testbed.UpdateServiceConfigWithQS("aws", svcId, cloudintegrations.UpdateServiceConfigRequest{
|
||||
CloudAccountId: testAWSAccountId,
|
||||
Config: testSvcConfig,
|
||||
})
|
||||
require.Equal(svcId, updateSvcConfigResp.Id)
|
||||
require.Equal(testSvcConfig, updateSvcConfigResp.Config)
|
||||
|
||||
// service list should include config when queried in the ctx of an account
|
||||
svcListResp = testbed.GetServicesFromQS("aws", &testAWSAccountId)
|
||||
require.Greater(len(svcListResp.Services), 0)
|
||||
for _, svc := range svcListResp.Services {
|
||||
if svc.Id == svcId {
|
||||
require.NotNil(svc.Config)
|
||||
require.Equal(testSvcConfig, *svc.Config)
|
||||
}
|
||||
}
|
||||
|
||||
// service detail should include config and status info when
|
||||
// queried in the ctx of an account
|
||||
svcDetailResp = testbed.GetServiceDetailFromQS("aws", svcId, &testAWSAccountId)
|
||||
require.Equal(svcId, svcDetailResp.Id)
|
||||
require.NotNil(svcDetailResp.Config)
|
||||
require.Equal(testSvcConfig, *svcDetailResp.Config)
|
||||
|
||||
}
|
||||
|
||||
type CloudIntegrationsTestBed struct {
|
||||
t *testing.T
|
||||
testUser *model.User
|
||||
@@ -275,6 +343,41 @@ func (tb *CloudIntegrationsTestBed) DisconnectAccountWithQS(
|
||||
return &resp
|
||||
}
|
||||
|
||||
func (tb *CloudIntegrationsTestBed) GetServicesFromQS(
|
||||
cloudProvider string, cloudAccountId *string,
|
||||
) *cloudintegrations.ListServicesResponse {
|
||||
path := fmt.Sprintf("/api/v1/cloud-integrations/%s/services", cloudProvider)
|
||||
if cloudAccountId != nil {
|
||||
path = fmt.Sprintf("%s?cloud_account_id=%s", path, *cloudAccountId)
|
||||
}
|
||||
|
||||
return RequestQSAndParseResp[cloudintegrations.ListServicesResponse](
|
||||
tb, path, nil,
|
||||
)
|
||||
}
|
||||
|
||||
func (tb *CloudIntegrationsTestBed) GetServiceDetailFromQS(
|
||||
cloudProvider string, serviceId string, cloudAccountId *string,
|
||||
) *cloudintegrations.CloudServiceDetails {
|
||||
path := fmt.Sprintf("/api/v1/cloud-integrations/%s/services/%s", cloudProvider, serviceId)
|
||||
if cloudAccountId != nil {
|
||||
path = fmt.Sprintf("%s?cloud_account_id=%s", path, *cloudAccountId)
|
||||
}
|
||||
|
||||
return RequestQSAndParseResp[cloudintegrations.CloudServiceDetails](
|
||||
tb, path, nil,
|
||||
)
|
||||
}
|
||||
func (tb *CloudIntegrationsTestBed) UpdateServiceConfigWithQS(
|
||||
cloudProvider string, serviceId string, req any,
|
||||
) *cloudintegrations.UpdateServiceConfigResponse {
|
||||
path := fmt.Sprintf("/api/v1/cloud-integrations/%s/services/%s/config", cloudProvider, serviceId)
|
||||
|
||||
return RequestQSAndParseResp[cloudintegrations.UpdateServiceConfigResponse](
|
||||
tb, path, req,
|
||||
)
|
||||
}
|
||||
|
||||
func (tb *CloudIntegrationsTestBed) RequestQS(
|
||||
path string,
|
||||
postData interface{},
|
||||
@@ -297,3 +400,20 @@ func (tb *CloudIntegrationsTestBed) RequestQS(
|
||||
}
|
||||
return dataJson
|
||||
}
|
||||
|
||||
func RequestQSAndParseResp[ResponseType any](
|
||||
tb *CloudIntegrationsTestBed,
|
||||
path string,
|
||||
postData interface{},
|
||||
) *ResponseType {
|
||||
respDataJson := tb.RequestQS(path, postData)
|
||||
|
||||
var resp ResponseType
|
||||
|
||||
err := json.Unmarshal(respDataJson, &resp)
|
||||
if err != nil {
|
||||
tb.t.Fatalf("could not unmarshal apiResponse.Data json into %T", resp)
|
||||
}
|
||||
|
||||
return &resp
|
||||
}
|
||||
|
||||
41
pkg/signoz/config.go
Normal file
41
pkg/signoz/config.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package signoz
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.signoz.io/signoz/pkg/cache"
|
||||
"go.signoz.io/signoz/pkg/config"
|
||||
"go.signoz.io/signoz/pkg/factory"
|
||||
"go.signoz.io/signoz/pkg/instrumentation"
|
||||
"go.signoz.io/signoz/pkg/sqlstore"
|
||||
"go.signoz.io/signoz/pkg/web"
|
||||
)
|
||||
|
||||
// Config defines the entire configuration of signoz.
|
||||
type Config struct {
|
||||
Instrumentation instrumentation.Config `mapstructure:"instrumentation"`
|
||||
Web web.Config `mapstructure:"web"`
|
||||
Cache cache.Config `mapstructure:"cache"`
|
||||
SQLStore sqlstore.Config `mapstructure:"sqlstore"`
|
||||
}
|
||||
|
||||
func NewConfig(ctx context.Context, resolverConfig config.ResolverConfig) (Config, error) {
|
||||
configFactories := []factory.ConfigFactory{
|
||||
instrumentation.NewConfigFactory(),
|
||||
web.NewConfigFactory(),
|
||||
sqlstore.NewConfigFactory(),
|
||||
cache.NewConfigFactory(),
|
||||
}
|
||||
|
||||
conf, err := config.New(ctx, resolverConfig, configFactories)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
|
||||
var config Config
|
||||
if err := conf.Unmarshal("", &config); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
@@ -2,30 +2,31 @@ package signoz
|
||||
|
||||
import (
|
||||
"go.signoz.io/signoz/pkg/cache"
|
||||
"go.signoz.io/signoz/pkg/cache/strategy/memory"
|
||||
"go.signoz.io/signoz/pkg/cache/strategy/redis"
|
||||
"go.signoz.io/signoz/pkg/cache/memorycache"
|
||||
"go.signoz.io/signoz/pkg/cache/rediscache"
|
||||
"go.signoz.io/signoz/pkg/config"
|
||||
"go.signoz.io/signoz/pkg/web"
|
||||
"go.signoz.io/signoz/pkg/web/routerweb"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type SigNoz struct {
|
||||
Cache cache.Cache
|
||||
Web *web.Web
|
||||
Web web.Web
|
||||
}
|
||||
|
||||
func New(config *config.Config, skipWebFrontend bool) (*SigNoz, error) {
|
||||
func New(config config.Config, skipWebFrontend bool) (*SigNoz, error) {
|
||||
var cache cache.Cache
|
||||
|
||||
// init for the cache
|
||||
switch config.Cache.Provider {
|
||||
case "memory":
|
||||
cache = memory.New(&config.Cache.Memory)
|
||||
cache = memorycache.New(&config.Cache.Memory)
|
||||
case "redis":
|
||||
cache = redis.New(&config.Cache.Redis)
|
||||
cache = rediscache.New(&config.Cache.Redis)
|
||||
}
|
||||
|
||||
web, err := web.New(zap.L(), config.Web)
|
||||
web, err := routerweb.New(zap.L(), config.Web)
|
||||
if err != nil && !skipWebFrontend {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
32
pkg/sqlmigrator/000_noop.go
Normal file
32
pkg/sqlmigrator/000_noop.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package sqlmigrator
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
"go.signoz.io/signoz/pkg/factory"
|
||||
)
|
||||
|
||||
type noopMigration struct{}
|
||||
|
||||
func NoopMigrationFactory() factory.ProviderFactory[SQLMigration, Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("noop"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
|
||||
return &noopMigration{}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (migration *noopMigration) Register(migrations *migrate.Migrations) error {
|
||||
if err := migrations.Register(migration.Up, migration.Down); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (migration *noopMigration) Up(ctx context.Context, db *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (migration *noopMigration) Down(ctx context.Context, db *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
41
pkg/sqlmigrator/config.go
Normal file
41
pkg/sqlmigrator/config.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package sqlmigrator
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"go.signoz.io/signoz/pkg/factory"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
// Lock is the lock configuration.
|
||||
Lock Lock `mapstructure:"lock"`
|
||||
}
|
||||
|
||||
type Lock struct {
|
||||
// Timeout is the time to wait for the migration lock.
|
||||
Timeout time.Duration `mapstructure:"timeout"`
|
||||
// Interval is the interval to try to acquire the migration lock.
|
||||
Interval time.Duration `mapstructure:"interval"`
|
||||
}
|
||||
|
||||
func NewConfigFactory() factory.ConfigFactory {
|
||||
return factory.NewConfigFactory(factory.MustNewName("sqlmigrator"), newConfig)
|
||||
}
|
||||
|
||||
func newConfig() factory.Config {
|
||||
return Config{
|
||||
Lock: Lock{
|
||||
Timeout: 2 * time.Minute,
|
||||
Interval: 10 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c Config) Validate() error {
|
||||
if c.Lock.Timeout < c.Lock.Interval {
|
||||
return errors.New("lock_timeout must be greater than lock_interval")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
77
pkg/sqlmigrator/migration.go
Normal file
77
pkg/sqlmigrator/migration.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package sqlmigrator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/dialect"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
"go.signoz.io/signoz/pkg/factory"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoExecute = errors.New("no execute")
|
||||
)
|
||||
|
||||
func NewMigrations(
|
||||
ctx context.Context,
|
||||
settings factory.ProviderSettings,
|
||||
config Config,
|
||||
factories factory.NamedMap[factory.ProviderFactory[SQLMigration, Config]],
|
||||
) (*migrate.Migrations, error) {
|
||||
migrations := migrate.NewMigrations()
|
||||
|
||||
for _, factory := range factories.GetInOrder() {
|
||||
migration, err := factory.New(ctx, settings, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = migration.Register(migrations)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return migrations, nil
|
||||
}
|
||||
|
||||
func MustNewMigrations(
|
||||
ctx context.Context,
|
||||
settings factory.ProviderSettings,
|
||||
config Config,
|
||||
factories factory.NamedMap[factory.ProviderFactory[SQLMigration, Config]],
|
||||
) *migrate.Migrations {
|
||||
migrations, err := NewMigrations(ctx, settings, config, factories)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return migrations
|
||||
}
|
||||
|
||||
func WrapIfNotExists(ctx context.Context, db *bun.DB, table string, column string) func(q *bun.AddColumnQuery) *bun.AddColumnQuery {
|
||||
return func(q *bun.AddColumnQuery) *bun.AddColumnQuery {
|
||||
if db.Dialect().Name() != dialect.SQLite {
|
||||
return q.IfNotExists()
|
||||
}
|
||||
|
||||
var result string
|
||||
err := db.
|
||||
NewSelect().
|
||||
ColumnExpr("name").
|
||||
Table("pragma_table_info").
|
||||
Where("arg = ?", table).
|
||||
Where("name = ?", column).
|
||||
Scan(ctx, &result)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return q
|
||||
}
|
||||
return q.Err(err)
|
||||
}
|
||||
|
||||
return q.Err(ErrNoExecute)
|
||||
}
|
||||
}
|
||||
45
pkg/sqlmigrator/migration/000_add_data_migrations.go
Normal file
45
pkg/sqlmigrator/migration/000_add_data_migrations.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package migration
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
"go.signoz.io/signoz/pkg/factory"
|
||||
"go.signoz.io/signoz/pkg/sqlmigrator"
|
||||
)
|
||||
|
||||
type addDataMigrations struct{}
|
||||
|
||||
func NewAddDataMigrationsFactory() factory.ProviderFactory[sqlmigrator.SQLMigration, sqlmigrator.Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("add_data_migrations"), newAddDataMigrations)
|
||||
}
|
||||
|
||||
func newAddDataMigrations(_ context.Context, _ factory.ProviderSettings, _ sqlmigrator.Config) (sqlmigrator.SQLMigration, error) {
|
||||
return &addDataMigrations{}, nil
|
||||
}
|
||||
|
||||
func (migration *addDataMigrations) Register(migrations *migrate.Migrations) error {
|
||||
if err := migrations.Register(migration.Up, migration.Down); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (migration *addDataMigrations) Up(ctx context.Context, db *bun.DB) error {
|
||||
// table:data_migrations
|
||||
if _, err := db.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS data_migrations (
|
||||
id SERIAL PRIMARY KEY,
|
||||
version VARCHAR(255) NOT NULL UNIQUE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
succeeded BOOLEAN NOT NULL DEFAULT FALSE
|
||||
);`); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (migration *addDataMigrations) Down(ctx context.Context, db *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
125
pkg/sqlmigrator/migration/001_add_organization.go
Normal file
125
pkg/sqlmigrator/migration/001_add_organization.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package migration
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
"go.signoz.io/signoz/pkg/factory"
|
||||
"go.signoz.io/signoz/pkg/sqlmigrator"
|
||||
)
|
||||
|
||||
type addOrganization struct{}
|
||||
|
||||
func NewAddOrganizationFactory() factory.ProviderFactory[sqlmigrator.SQLMigration, sqlmigrator.Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("add_organization"), newAddOrganization)
|
||||
}
|
||||
|
||||
func newAddOrganization(_ context.Context, _ factory.ProviderSettings, _ sqlmigrator.Config) (sqlmigrator.SQLMigration, error) {
|
||||
return &addOrganization{}, nil
|
||||
}
|
||||
|
||||
func (migration *addOrganization) Register(migrations *migrate.Migrations) error {
|
||||
if err := migrations.Register(migration.Up, migration.Down); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (migration *addOrganization) Up(ctx context.Context, db *bun.DB) error {
|
||||
// table:invites
|
||||
if _, err := db.NewRaw(`CREATE TABLE IF NOT EXISTS invites (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
token TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
role TEXT NOT NULL,
|
||||
org_id TEXT NOT NULL,
|
||||
FOREIGN KEY(org_id) REFERENCES organizations(id)
|
||||
)`).Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// table:organizations
|
||||
if _, err := db.NewRaw(`CREATE TABLE IF NOT EXISTS organizations (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
is_anonymous INTEGER NOT NULL DEFAULT 0 CHECK(is_anonymous IN (0,1)),
|
||||
has_opted_updates INTEGER NOT NULL DEFAULT 1 CHECK(has_opted_updates IN (0,1))
|
||||
)`).Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// table:users
|
||||
if _, err := db.NewRaw(`CREATE TABLE IF NOT EXISTS users (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
password TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
profile_picture_url TEXT,
|
||||
group_id TEXT NOT NULL,
|
||||
org_id TEXT NOT NULL,
|
||||
FOREIGN KEY(group_id) REFERENCES groups(id),
|
||||
FOREIGN KEY(org_id) REFERENCES organizations(id)
|
||||
)`).Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// table:groups
|
||||
if _, err := db.NewRaw(`CREATE TABLE IF NOT EXISTS groups (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE
|
||||
)`).Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// table:reset_password_request
|
||||
if _, err := db.NewRaw(`CREATE TABLE IF NOT EXISTS reset_password_request (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id TEXT NOT NULL,
|
||||
token TEXT NOT NULL,
|
||||
FOREIGN KEY(user_id) REFERENCES users(id)
|
||||
)`).Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// table:user_flags
|
||||
if _, err := db.NewRaw(`CREATE TABLE IF NOT EXISTS user_flags (
|
||||
user_id TEXT PRIMARY KEY,
|
||||
flags TEXT,
|
||||
FOREIGN KEY(user_id) REFERENCES users(id)
|
||||
)`).Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// table:apdex_settings
|
||||
if _, err := db.NewRaw(`CREATE TABLE IF NOT EXISTS apdex_settings (
|
||||
service_name TEXT PRIMARY KEY,
|
||||
threshold FLOAT NOT NULL,
|
||||
exclude_status_codes TEXT NOT NULL
|
||||
)`).Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// table:ingestion_keys
|
||||
if _, err := db.NewRaw(`CREATE TABLE IF NOT EXISTS ingestion_keys (
|
||||
key_id TEXT PRIMARY KEY,
|
||||
name TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
ingestion_key TEXT NOT NULL,
|
||||
ingestion_url TEXT NOT NULL,
|
||||
data_region TEXT NOT NULL
|
||||
)`).Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (migration *addOrganization) Down(ctx context.Context, db *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
58
pkg/sqlmigrator/migration/002_add_preferences.go
Normal file
58
pkg/sqlmigrator/migration/002_add_preferences.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package migration
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
"go.signoz.io/signoz/pkg/factory"
|
||||
"go.signoz.io/signoz/pkg/sqlmigrator"
|
||||
)
|
||||
|
||||
type addPreferences struct{}
|
||||
|
||||
func NewAddPreferencesFactory() factory.ProviderFactory[sqlmigrator.SQLMigration, sqlmigrator.Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("add_preferences"), newAddPreferences)
|
||||
}
|
||||
|
||||
func newAddPreferences(_ context.Context, _ factory.ProviderSettings, _ sqlmigrator.Config) (sqlmigrator.SQLMigration, error) {
|
||||
return &addPreferences{}, nil
|
||||
}
|
||||
|
||||
func (migration *addPreferences) Register(migrations *migrate.Migrations) error {
|
||||
if err := migrations.Register(migration.Up, migration.Down); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (migration *addPreferences) Up(ctx context.Context, db *bun.DB) error {
|
||||
// table:user_preference
|
||||
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS user_preference (
|
||||
preference_id TEXT NOT NULL,
|
||||
preference_value TEXT,
|
||||
user_id TEXT NOT NULL,
|
||||
PRIMARY KEY (preference_id,user_id),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON UPDATE CASCADE ON DELETE CASCADE
|
||||
)`); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// table:org_preference
|
||||
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS org_preference (
|
||||
preference_id TEXT NOT NULL,
|
||||
preference_value TEXT,
|
||||
org_id TEXT NOT NULL,
|
||||
PRIMARY KEY (preference_id,org_id),
|
||||
FOREIGN KEY (org_id) REFERENCES organizations(id) ON UPDATE CASCADE ON DELETE CASCADE
|
||||
);`); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (migration *addPreferences) Down(ctx context.Context, db *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
159
pkg/sqlmigrator/migration/003_add_dashboards.go
Normal file
159
pkg/sqlmigrator/migration/003_add_dashboards.go
Normal file
@@ -0,0 +1,159 @@
|
||||
package migration
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
"go.signoz.io/signoz/pkg/factory"
|
||||
"go.signoz.io/signoz/pkg/sqlmigrator"
|
||||
)
|
||||
|
||||
type addDashboards struct{}
|
||||
|
||||
func NewAddDashboardsFactory() factory.ProviderFactory[sqlmigrator.SQLMigration, sqlmigrator.Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("add_dashboards"), newAddDashboards)
|
||||
}
|
||||
|
||||
func newAddDashboards(_ context.Context, _ factory.ProviderSettings, _ sqlmigrator.Config) (sqlmigrator.SQLMigration, error) {
|
||||
return &addDashboards{}, nil
|
||||
}
|
||||
|
||||
func (migration *addDashboards) Register(migrations *migrate.Migrations) error {
|
||||
if err := migrations.Register(migration.Up, migration.Down); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (migration *addDashboards) Up(ctx context.Context, db *bun.DB) error {
|
||||
// table:dashboards
|
||||
if _, err := db.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS dashboards (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
uuid TEXT NOT NULL UNIQUE,
|
||||
created_at datetime NOT NULL,
|
||||
updated_at datetime NOT NULL,
|
||||
data TEXT NOT NULL
|
||||
);`); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// table:rules
|
||||
if _, err := db.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS rules (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
updated_at datetime NOT NULL,
|
||||
deleted INTEGER DEFAULT 0,
|
||||
data TEXT NOT NULL
|
||||
);`); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// table:notification_channels
|
||||
if _, err := db.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS notification_channels (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
created_at datetime NOT NULL,
|
||||
updated_at datetime NOT NULL,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
type TEXT NOT NULL,
|
||||
deleted INTEGER DEFAULT 0,
|
||||
data TEXT NOT NULL
|
||||
);`); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// table:planned_maintenance
|
||||
if _, err := db.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS planned_maintenance (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
alert_ids TEXT,
|
||||
schedule TEXT NOT NULL,
|
||||
created_at datetime NOT NULL,
|
||||
created_by TEXT NOT NULL,
|
||||
updated_at datetime NOT NULL,
|
||||
updated_by TEXT NOT NULL
|
||||
);`); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// table:ttl_status
|
||||
if _, err := db.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS ttl_status (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
transaction_id TEXT NOT NULL,
|
||||
created_at datetime NOT NULL,
|
||||
updated_at datetime NOT NULL,
|
||||
table_name TEXT NOT NULL,
|
||||
ttl INTEGER DEFAULT 0,
|
||||
cold_storage_ttl INTEGER DEFAULT 0,
|
||||
status TEXT NOT NULL
|
||||
);`); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// table:rules op:add column created_at
|
||||
if _, err := db.
|
||||
NewAddColumn().
|
||||
Table("rules").
|
||||
ColumnExpr("created_at datetime").
|
||||
Apply(sqlmigrator.WrapIfNotExists(ctx, db, "rules", "created_at")).
|
||||
Exec(ctx); err != nil && err != sqlmigrator.ErrNoExecute {
|
||||
return err
|
||||
}
|
||||
|
||||
// table:rules op:add column created_by
|
||||
if _, err := db.
|
||||
NewAddColumn().
|
||||
Table("rules").
|
||||
ColumnExpr("created_by TEXT").
|
||||
Apply(sqlmigrator.WrapIfNotExists(ctx, db, "rules", "created_by")).
|
||||
Exec(ctx); err != nil && err != sqlmigrator.ErrNoExecute {
|
||||
return err
|
||||
}
|
||||
|
||||
// table:rules op:add column updated_by
|
||||
if _, err := db.
|
||||
NewAddColumn().
|
||||
Table("rules").
|
||||
ColumnExpr("updated_by TEXT").
|
||||
Apply(sqlmigrator.WrapIfNotExists(ctx, db, "rules", "updated_by")).
|
||||
Exec(ctx); err != nil && err != sqlmigrator.ErrNoExecute {
|
||||
return err
|
||||
}
|
||||
|
||||
// table:dashboards op:add column created_by
|
||||
if _, err := db.
|
||||
NewAddColumn().
|
||||
Table("dashboards").
|
||||
ColumnExpr("created_by TEXT").
|
||||
Apply(sqlmigrator.WrapIfNotExists(ctx, db, "dashboards", "created_by")).
|
||||
Exec(ctx); err != nil && err != sqlmigrator.ErrNoExecute {
|
||||
return err
|
||||
}
|
||||
|
||||
// table:dashboards op:add column updated_by
|
||||
if _, err := db.
|
||||
NewAddColumn().
|
||||
Table("dashboards").
|
||||
ColumnExpr("updated_by TEXT").
|
||||
Apply(sqlmigrator.WrapIfNotExists(ctx, db, "dashboards", "updated_by")).
|
||||
Exec(ctx); err != nil && err != sqlmigrator.ErrNoExecute {
|
||||
return err
|
||||
}
|
||||
|
||||
// table:dashboards op:add column locked
|
||||
if _, err := db.
|
||||
NewAddColumn().
|
||||
Table("dashboards").
|
||||
ColumnExpr("locked INTEGER DEFAULT 0").
|
||||
Apply(sqlmigrator.WrapIfNotExists(ctx, db, "dashboards", "locked")).
|
||||
Exec(ctx); err != nil && err != sqlmigrator.ErrNoExecute {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (migration *addDashboards) Down(ctx context.Context, db *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
53
pkg/sqlmigrator/migration/004_add_saved_views.go
Normal file
53
pkg/sqlmigrator/migration/004_add_saved_views.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package migration
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
"go.signoz.io/signoz/pkg/factory"
|
||||
"go.signoz.io/signoz/pkg/sqlmigrator"
|
||||
)
|
||||
|
||||
type addSavedViews struct{}
|
||||
|
||||
func NewAddSavedViewsFactory() factory.ProviderFactory[sqlmigrator.SQLMigration, sqlmigrator.Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("add_saved_views"), newAddSavedViews)
|
||||
}
|
||||
|
||||
func newAddSavedViews(_ context.Context, _ factory.ProviderSettings, _ sqlmigrator.Config) (sqlmigrator.SQLMigration, error) {
|
||||
return &addSavedViews{}, nil
|
||||
}
|
||||
|
||||
func (migration *addSavedViews) Register(migrations *migrate.Migrations) error {
|
||||
if err := migrations.Register(migration.Up, migration.Down); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (migration *addSavedViews) Up(ctx context.Context, db *bun.DB) error {
|
||||
// table:saved_views op:create
|
||||
if _, err := db.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS saved_views (
|
||||
uuid TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
category TEXT NOT NULL,
|
||||
created_at datetime NOT NULL,
|
||||
created_by TEXT,
|
||||
updated_at datetime NOT NULL,
|
||||
updated_by TEXT,
|
||||
source_page TEXT NOT NULL,
|
||||
tags TEXT,
|
||||
data TEXT NOT NULL,
|
||||
extra_data TEXT
|
||||
);`); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (migration *addSavedViews) Down(ctx context.Context, db *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
92
pkg/sqlmigrator/migration/005_add_agents.go
Normal file
92
pkg/sqlmigrator/migration/005_add_agents.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package migration
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
"go.signoz.io/signoz/pkg/factory"
|
||||
"go.signoz.io/signoz/pkg/sqlmigrator"
|
||||
)
|
||||
|
||||
type addAgents struct{}
|
||||
|
||||
func NewAddAgentsFactory() factory.ProviderFactory[sqlmigrator.SQLMigration, sqlmigrator.Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("add_agents"), newAddAgents)
|
||||
}
|
||||
|
||||
func newAddAgents(_ context.Context, _ factory.ProviderSettings, _ sqlmigrator.Config) (sqlmigrator.SQLMigration, error) {
|
||||
return &addAgents{}, nil
|
||||
}
|
||||
|
||||
func (migration *addAgents) Register(migrations *migrate.Migrations) error {
|
||||
if err := migrations.Register(migration.Up, migration.Down); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (migration *addAgents) Up(ctx context.Context, db *bun.DB) error {
|
||||
if _, err := db.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS agents (
|
||||
agent_id TEXT PRIMARY KEY UNIQUE,
|
||||
started_at datetime NOT NULL,
|
||||
terminated_at datetime,
|
||||
current_status TEXT NOT NULL,
|
||||
effective_config TEXT NOT NULL
|
||||
);`); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := db.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS agent_config_versions(
|
||||
id TEXT PRIMARY KEY,
|
||||
created_by TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_by TEXT,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
version INTEGER DEFAULT 1,
|
||||
active int,
|
||||
is_valid int,
|
||||
disabled int,
|
||||
element_type VARCHAR(120) NOT NULL,
|
||||
deploy_status VARCHAR(80) NOT NULL DEFAULT 'DIRTY',
|
||||
deploy_sequence INTEGER,
|
||||
deploy_result TEXT,
|
||||
last_hash TEXT,
|
||||
last_config TEXT,
|
||||
UNIQUE(element_type, version)
|
||||
);`); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := db.ExecContext(ctx, `CREATE UNIQUE INDEX IF NOT EXISTS agent_config_versions_u1 ON agent_config_versions(element_type, version);`); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := db.ExecContext(ctx, `CREATE INDEX IF NOT EXISTS agent_config_versions_nu1 ON agent_config_versions(last_hash);`); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := db.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS agent_config_elements(
|
||||
id TEXT PRIMARY KEY,
|
||||
created_by TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_by TEXT,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
element_id TEXT NOT NULL,
|
||||
element_type VARCHAR(120) NOT NULL,
|
||||
version_id TEXT NOT NULL
|
||||
);`); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := db.ExecContext(ctx, `CREATE UNIQUE INDEX IF NOT EXISTS agent_config_elements_u1 ON agent_config_elements(version_id, element_id, element_type);`); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (migration *addAgents) Down(ctx context.Context, db *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
51
pkg/sqlmigrator/migration/006_add_pipelines.go
Normal file
51
pkg/sqlmigrator/migration/006_add_pipelines.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package migration
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
"go.signoz.io/signoz/pkg/factory"
|
||||
"go.signoz.io/signoz/pkg/sqlmigrator"
|
||||
)
|
||||
|
||||
type addPipelines struct{}
|
||||
|
||||
func NewAddPipelinesFactory() factory.ProviderFactory[sqlmigrator.SQLMigration, sqlmigrator.Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("add_pipelines"), newAddPipelines)
|
||||
}
|
||||
|
||||
func newAddPipelines(_ context.Context, _ factory.ProviderSettings, _ sqlmigrator.Config) (sqlmigrator.SQLMigration, error) {
|
||||
return &addPipelines{}, nil
|
||||
}
|
||||
|
||||
func (migration *addPipelines) Register(migrations *migrate.Migrations) error {
|
||||
if err := migrations.Register(migration.Up, migration.Down); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (migration *addPipelines) Up(ctx context.Context, db *bun.DB) error {
|
||||
if _, err := db.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS pipelines(
|
||||
id TEXT PRIMARY KEY,
|
||||
order_id INTEGER,
|
||||
enabled BOOLEAN,
|
||||
created_by TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
name VARCHAR(400) NOT NULL,
|
||||
alias VARCHAR(20) NOT NULL,
|
||||
description TEXT,
|
||||
filter TEXT NOT NULL,
|
||||
config_json TEXT
|
||||
);`); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (migration *addPipelines) Down(ctx context.Context, db *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
57
pkg/sqlmigrator/migration/007_add_integrations.go
Normal file
57
pkg/sqlmigrator/migration/007_add_integrations.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package migration
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
"go.signoz.io/signoz/pkg/factory"
|
||||
"go.signoz.io/signoz/pkg/sqlmigrator"
|
||||
)
|
||||
|
||||
type addIntegrations struct{}
|
||||
|
||||
func NewAddIntegrationsFactory() factory.ProviderFactory[sqlmigrator.SQLMigration, sqlmigrator.Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("add_integrations"), newAddIntegrations)
|
||||
}
|
||||
|
||||
func newAddIntegrations(_ context.Context, _ factory.ProviderSettings, _ sqlmigrator.Config) (sqlmigrator.SQLMigration, error) {
|
||||
return &addIntegrations{}, nil
|
||||
}
|
||||
|
||||
func (migration *addIntegrations) Register(migrations *migrate.Migrations) error {
|
||||
if err := migrations.Register(migration.Up, migration.Down); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (migration *addIntegrations) Up(ctx context.Context, db *bun.DB) error {
|
||||
if _, err := db.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS integrations_installed(
|
||||
integration_id TEXT PRIMARY KEY,
|
||||
config_json TEXT,
|
||||
installed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);`); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := db.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS cloud_integrations_accounts(
|
||||
cloud_provider TEXT NOT NULL,
|
||||
id TEXT NOT NULL,
|
||||
config_json TEXT,
|
||||
cloud_account_id TEXT,
|
||||
last_agent_report_json TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
removed_at TIMESTAMP,
|
||||
UNIQUE(cloud_provider, id)
|
||||
)`); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (migration *addIntegrations) Down(ctx context.Context, db *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
108
pkg/sqlmigrator/migrator.go
Normal file
108
pkg/sqlmigrator/migrator.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package sqlmigrator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/uptrace/bun/migrate"
|
||||
"go.signoz.io/signoz/pkg/factory"
|
||||
"go.signoz.io/signoz/pkg/sqlstore"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var (
|
||||
migrationTableName string = "migration"
|
||||
migrationLockTableName string = "migration_lock"
|
||||
)
|
||||
|
||||
type migrator struct {
|
||||
settings factory.ScopedProviderSettings
|
||||
config Config
|
||||
migrator *migrate.Migrator
|
||||
dialect string
|
||||
}
|
||||
|
||||
func New(ctx context.Context, providerSettings factory.ProviderSettings, sqlstore sqlstore.SQLStore, migrations *migrate.Migrations, config Config) SQLMigrator {
|
||||
return &migrator{
|
||||
migrator: migrate.NewMigrator(sqlstore.BunDB(), migrations, migrate.WithTableName(migrationTableName), migrate.WithLocksTableName(migrationLockTableName)),
|
||||
settings: factory.NewScopedProviderSettings(providerSettings, "go.signoz.io/signoz/pkg/sqlmigrator"),
|
||||
config: config,
|
||||
dialect: sqlstore.BunDB().Dialect().Name().String(),
|
||||
}
|
||||
}
|
||||
|
||||
func (migrator *migrator) Migrate(ctx context.Context) error {
|
||||
migrator.settings.ZapLogger().Info("starting sqlstore migrations", zap.String("dialect", migrator.dialect))
|
||||
if err := migrator.migrator.Init(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := migrator.Lock(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer migrator.migrator.Unlock(ctx) //nolint:errcheck
|
||||
|
||||
group, err := migrator.migrator.Migrate(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if group.IsZero() {
|
||||
migrator.settings.ZapLogger().Info("no new migrations to run (database is up to date)", zap.String("dialect", migrator.dialect))
|
||||
return nil
|
||||
}
|
||||
|
||||
migrator.settings.ZapLogger().Info("migrated to", zap.String("group", group.String()), zap.String("dialect", migrator.dialect))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (migrator *migrator) Rollback(ctx context.Context) error {
|
||||
if err := migrator.Lock(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
defer migrator.migrator.Unlock(ctx) //nolint:errcheck
|
||||
|
||||
group, err := migrator.migrator.Rollback(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if group.IsZero() {
|
||||
migrator.settings.ZapLogger().Info("no groups to roll back", zap.String("dialect", migrator.dialect))
|
||||
return nil
|
||||
}
|
||||
|
||||
migrator.settings.ZapLogger().Info("rolled back", zap.String("group", group.String()), zap.String("dialect", migrator.dialect))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (migrator *migrator) Lock(ctx context.Context) error {
|
||||
if err := migrator.migrator.Lock(ctx); err == nil {
|
||||
migrator.settings.ZapLogger().Info("acquired migration lock", zap.String("dialect", migrator.dialect))
|
||||
return nil
|
||||
}
|
||||
|
||||
timer := time.NewTimer(migrator.config.Lock.Timeout)
|
||||
defer timer.Stop()
|
||||
|
||||
ticker := time.NewTicker(migrator.config.Lock.Interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-timer.C:
|
||||
err := errors.New("timed out waiting for lock")
|
||||
migrator.settings.ZapLogger().Error("cannot acquire lock", zap.Error(err), zap.Duration("lock_timeout", migrator.config.Lock.Timeout), zap.String("dialect", migrator.dialect))
|
||||
return err
|
||||
case <-ticker.C:
|
||||
if err := migrator.migrator.Lock(ctx); err == nil {
|
||||
migrator.settings.ZapLogger().Info("acquired migration lock", zap.String("dialect", migrator.dialect))
|
||||
return nil
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
48
pkg/sqlmigrator/migrator_test.go
Normal file
48
pkg/sqlmigrator/migrator_test.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package sqlmigrator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql/driver"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.signoz.io/signoz/pkg/factory"
|
||||
"go.signoz.io/signoz/pkg/instrumentation/instrumentationtest"
|
||||
"go.signoz.io/signoz/pkg/sqlstore"
|
||||
"go.signoz.io/signoz/pkg/sqlstore/sqlstoretest"
|
||||
)
|
||||
|
||||
func TestMigratorWithSqliteAndNoopMigration(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
sqlstoreConfig := sqlstore.Config{
|
||||
Provider: "sqlite",
|
||||
}
|
||||
|
||||
migrationConfig := Config{
|
||||
Lock: Lock{
|
||||
Timeout: 10 * time.Second,
|
||||
Interval: 1 * time.Second,
|
||||
},
|
||||
}
|
||||
|
||||
providerSettings := instrumentationtest.New().ToProviderSettings()
|
||||
sqlstore := sqlstoretest.New(sqlstoreConfig, sqlmock.QueryMatcherRegexp)
|
||||
migrator := New(
|
||||
ctx,
|
||||
providerSettings,
|
||||
sqlstore,
|
||||
MustNewMigrations(ctx, providerSettings, migrationConfig, factory.MustNewNamedMap(NoopMigrationFactory())),
|
||||
migrationConfig,
|
||||
)
|
||||
|
||||
sqlstore.Mock().ExpectExec("CREATE TABLE IF NOT EXISTS migration (.+)").WillReturnResult(driver.ResultNoRows)
|
||||
sqlstore.Mock().ExpectExec("CREATE TABLE IF NOT EXISTS migration_lock (.+)").WillReturnResult(driver.ResultNoRows)
|
||||
sqlstore.Mock().ExpectQuery("INSERT INTO migration_lock (.+)").WillReturnRows(sqlstore.Mock().NewRows([]string{"id"}).AddRow(1))
|
||||
sqlstore.Mock().ExpectQuery("(.+) FROM migration").WillReturnRows(sqlstore.Mock().NewRows([]string{"id"}).AddRow(1))
|
||||
sqlstore.Mock().ExpectQuery("INSERT INTO migration (.+)").WillReturnRows(sqlstore.Mock().NewRows([]string{"id", "migrated_at"}).AddRow(1, time.Now()))
|
||||
|
||||
err := migrator.Migrate(ctx)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
27
pkg/sqlmigrator/sqlmigrator.go
Normal file
27
pkg/sqlmigrator/sqlmigrator.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package sqlmigrator
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
)
|
||||
|
||||
// SQLMigrator is the interface for the SQLMigrator.
|
||||
type SQLMigrator interface {
|
||||
// Migrate migrates the database. Migrate acquires a lock on the database and runs the migrations.
|
||||
Migrate(context.Context) error
|
||||
// Rollback rolls back the database. Rollback acquires a lock on the database and rolls back the migrations.
|
||||
Rollback(context.Context) error
|
||||
}
|
||||
|
||||
// SQLMigration is the interface for a single migration.
|
||||
type SQLMigration interface {
|
||||
// Register registers the migration with the given migrations. Each migration needs to be registered
|
||||
//in a dedicated `*.go` file so that the correct migration semantics can be detected.
|
||||
Register(*migrate.Migrations) error
|
||||
// Up runs the migration.
|
||||
Up(context.Context, *bun.DB) error
|
||||
// Down rolls back the migration.
|
||||
Down(context.Context, *bun.DB) error
|
||||
}
|
||||
45
pkg/sqlstore/config.go
Normal file
45
pkg/sqlstore/config.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"go.signoz.io/signoz/pkg/factory"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
// Provider is the provider to use.
|
||||
Provider string `mapstructure:"provider"`
|
||||
// Connection is the connection configuration.
|
||||
Connection ConnectionConfig `mapstructure:",squash"`
|
||||
// Sqlite is the sqlite configuration.
|
||||
Sqlite SqliteConfig `mapstructure:"sqlite"`
|
||||
}
|
||||
|
||||
type SqliteConfig struct {
|
||||
// Path is the path to the sqlite database.
|
||||
Path string `mapstructure:"path"`
|
||||
}
|
||||
|
||||
type ConnectionConfig struct {
|
||||
// MaxOpenConns is the maximum number of open connections to the database.
|
||||
MaxOpenConns int `mapstructure:"max_open_conns"`
|
||||
}
|
||||
|
||||
func NewConfigFactory() factory.ConfigFactory {
|
||||
return factory.NewConfigFactory(factory.MustNewName("sqlstore"), newConfig)
|
||||
}
|
||||
|
||||
func newConfig() factory.Config {
|
||||
return Config{
|
||||
Provider: "sqlite",
|
||||
Connection: ConnectionConfig{
|
||||
MaxOpenConns: 100,
|
||||
},
|
||||
Sqlite: SqliteConfig{
|
||||
Path: "/var/lib/signoz/signoz.db",
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (c Config) Validate() error {
|
||||
return nil
|
||||
}
|
||||
55
pkg/sqlstore/sqlitesqlstore/provider.go
Normal file
55
pkg/sqlstore/sqlitesqlstore/provider.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package sqlitesqlstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/dialect/sqlitedialect"
|
||||
"go.signoz.io/signoz/pkg/factory"
|
||||
"go.signoz.io/signoz/pkg/sqlstore"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type provider struct {
|
||||
settings factory.ScopedProviderSettings
|
||||
sqldb *sql.DB
|
||||
bundb *bun.DB
|
||||
sqlxdb *sqlx.DB
|
||||
}
|
||||
|
||||
func NewFactory() factory.ProviderFactory[sqlstore.SQLStore, sqlstore.Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("sqlite"), New)
|
||||
}
|
||||
|
||||
func New(ctx context.Context, providerSettings factory.ProviderSettings, config sqlstore.Config) (sqlstore.SQLStore, error) {
|
||||
settings := factory.NewScopedProviderSettings(providerSettings, "go.signoz.io/signoz/pkg/sqlitesqlstore")
|
||||
|
||||
sqldb, err := sql.Open("sqlite3", "file:"+config.Sqlite.Path+"?_foreign_keys=true")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
settings.ZapLogger().Info("connected to sqlite", zap.String("path", config.Sqlite.Path))
|
||||
sqldb.SetMaxOpenConns(config.Connection.MaxOpenConns)
|
||||
|
||||
return &provider{
|
||||
settings: settings,
|
||||
sqldb: sqldb,
|
||||
bundb: bun.NewDB(sqldb, sqlitedialect.New()),
|
||||
sqlxdb: sqlx.NewDb(sqldb, "sqlite3"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (provider *provider) BunDB() *bun.DB {
|
||||
return provider.bundb
|
||||
}
|
||||
|
||||
func (provider *provider) SQLDB() *sql.DB {
|
||||
return provider.sqldb
|
||||
}
|
||||
|
||||
func (provider *provider) SQLxDB() *sqlx.DB {
|
||||
return provider.sqlxdb
|
||||
}
|
||||
18
pkg/sqlstore/sqlstore.go
Normal file
18
pkg/sqlstore/sqlstore.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
// SQLStore is the interface for the SQLStore.
|
||||
type SQLStore interface {
|
||||
// SQLDB returns the underlying sql.DB.
|
||||
SQLDB() *sql.DB
|
||||
// BunDB returns an instance of bun.DB. This is the recommended way to interact with the database.
|
||||
BunDB() *bun.DB
|
||||
// SQLxDB returns an instance of sqlx.DB.
|
||||
SQLxDB() *sqlx.DB
|
||||
}
|
||||
61
pkg/sqlstore/sqlstoretest/provider.go
Normal file
61
pkg/sqlstore/sqlstoretest/provider.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package sqlstoretest
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/dialect/sqlitedialect"
|
||||
"go.signoz.io/signoz/pkg/sqlstore"
|
||||
)
|
||||
|
||||
var _ sqlstore.SQLStore = (*Provider)(nil)
|
||||
|
||||
type Provider struct {
|
||||
db *sql.DB
|
||||
mock sqlmock.Sqlmock
|
||||
bunDB *bun.DB
|
||||
sqlxDB *sqlx.DB
|
||||
}
|
||||
|
||||
func New(config sqlstore.Config, matcher sqlmock.QueryMatcher) *Provider {
|
||||
db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(matcher))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var bunDB *bun.DB
|
||||
var sqlxDB *sqlx.DB
|
||||
|
||||
if config.Provider == "sqlite" {
|
||||
bunDB = bun.NewDB(db, sqlitedialect.New())
|
||||
sqlxDB = sqlx.NewDb(db, "sqlite3")
|
||||
} else {
|
||||
panic(fmt.Errorf("provider %q is not supported by mockSQLStore", config.Provider))
|
||||
}
|
||||
|
||||
return &Provider{
|
||||
db: db,
|
||||
mock: mock,
|
||||
bunDB: bunDB,
|
||||
sqlxDB: sqlxDB,
|
||||
}
|
||||
}
|
||||
|
||||
func (provider *Provider) BunDB() *bun.DB {
|
||||
return provider.bunDB
|
||||
}
|
||||
|
||||
func (provider *Provider) SQLDB() *sql.DB {
|
||||
return provider.db
|
||||
}
|
||||
|
||||
func (provider *Provider) SQLxDB() *sqlx.DB {
|
||||
return provider.sqlxDB
|
||||
}
|
||||
|
||||
func (provider *Provider) Mock() sqlmock.Sqlmock {
|
||||
return provider.mock
|
||||
}
|
||||
22
pkg/web/noopweb/provider.go
Normal file
22
pkg/web/noopweb/provider.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package noopweb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"go.signoz.io/signoz/pkg/factory"
|
||||
"go.signoz.io/signoz/pkg/web"
|
||||
)
|
||||
|
||||
type provider struct{}
|
||||
|
||||
func New(ctx context.Context, settings factory.ProviderSettings, config web.Config) (web.Web, error) {
|
||||
return &provider{}, nil
|
||||
}
|
||||
|
||||
func (provider *provider) AddToRouter(router *mux.Router) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *provider) ServeHTTP(w http.ResponseWriter, r *http.Request) {}
|
||||
93
pkg/web/routerweb/provider.go
Normal file
93
pkg/web/routerweb/provider.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package routerweb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"go.signoz.io/signoz/pkg/http/middleware"
|
||||
"go.signoz.io/signoz/pkg/web"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const (
|
||||
indexFileName string = "index.html"
|
||||
)
|
||||
|
||||
type Web struct {
|
||||
logger *zap.Logger
|
||||
cfg web.Config
|
||||
}
|
||||
|
||||
func New(logger *zap.Logger, cfg web.Config) (*Web, error) {
|
||||
if logger == nil {
|
||||
return nil, fmt.Errorf("cannot build web, logger is required")
|
||||
}
|
||||
|
||||
fi, err := os.Stat(cfg.Directory)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot access web directory: %w", err)
|
||||
}
|
||||
|
||||
ok := fi.IsDir()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("web directory is not a directory")
|
||||
}
|
||||
|
||||
fi, err = os.Stat(filepath.Join(cfg.Directory, indexFileName))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot access %q in web directory: %w", indexFileName, err)
|
||||
}
|
||||
|
||||
if os.IsNotExist(err) || fi.IsDir() {
|
||||
return nil, fmt.Errorf("%q does not exist", indexFileName)
|
||||
}
|
||||
|
||||
return &Web{
|
||||
logger: logger.Named("go.signoz.io/pkg/web"),
|
||||
cfg: cfg,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (web *Web) AddToRouter(router *mux.Router) error {
|
||||
cache := middleware.NewCache(7 * 24 * time.Hour)
|
||||
err := router.PathPrefix(web.cfg.Prefix).
|
||||
Handler(
|
||||
http.StripPrefix(
|
||||
web.cfg.Prefix,
|
||||
cache.Wrap(http.HandlerFunc(web.ServeHTTP)),
|
||||
),
|
||||
).GetError()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to add web to router: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (web *Web) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
// Join internally call path.Clean to prevent directory traversal
|
||||
path := filepath.Join(web.cfg.Directory, req.URL.Path)
|
||||
|
||||
// check whether a file exists or is a directory at the given path
|
||||
fi, err := os.Stat(path)
|
||||
if os.IsNotExist(err) || fi.IsDir() {
|
||||
// file does not exist or path is a directory, serve index.html
|
||||
http.ServeFile(rw, req, filepath.Join(web.cfg.Directory, indexFileName))
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
// if we got an error (that wasn't that the file doesn't exist) stating the
|
||||
// file, return a 500 internal server error and stop
|
||||
// TODO: Put down a crash html page here
|
||||
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// otherwise, use http.FileServer to serve the static file
|
||||
http.FileServer(http.Dir(web.cfg.Directory)).ServeHTTP(rw, req)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package web
|
||||
package routerweb
|
||||
|
||||
import (
|
||||
"io"
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.signoz.io/signoz/pkg/web"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
@@ -22,7 +23,7 @@ func TestServeHttpWithoutPrefix(t *testing.T) {
|
||||
expected, err := io.ReadAll(fi)
|
||||
require.NoError(t, err)
|
||||
|
||||
web, err := New(zap.NewNop(), Config{Prefix: "/", Directory: filepath.Join("testdata")})
|
||||
web, err := New(zap.NewNop(), web.Config{Prefix: "/", Directory: filepath.Join("testdata")})
|
||||
require.NoError(t, err)
|
||||
|
||||
router := mux.NewRouter()
|
||||
@@ -87,7 +88,7 @@ func TestServeHttpWithPrefix(t *testing.T) {
|
||||
expected, err := io.ReadAll(fi)
|
||||
require.NoError(t, err)
|
||||
|
||||
web, err := New(zap.NewNop(), Config{Prefix: "/web", Directory: filepath.Join("testdata")})
|
||||
web, err := New(zap.NewNop(), web.Config{Prefix: "/web", Directory: filepath.Join("testdata")})
|
||||
require.NoError(t, err)
|
||||
|
||||
router := mux.NewRouter()
|
||||
@@ -1,94 +1,15 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"go.signoz.io/signoz/pkg/http/middleware"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var _ http.Handler = (*Web)(nil)
|
||||
|
||||
const (
|
||||
indexFileName string = "index.html"
|
||||
)
|
||||
|
||||
type Web struct {
|
||||
logger *zap.Logger
|
||||
cfg Config
|
||||
}
|
||||
|
||||
func New(logger *zap.Logger, cfg Config) (*Web, error) {
|
||||
if logger == nil {
|
||||
return nil, fmt.Errorf("cannot build web, logger is required")
|
||||
}
|
||||
|
||||
fi, err := os.Stat(cfg.Directory)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot access web directory: %w", err)
|
||||
}
|
||||
|
||||
ok := fi.IsDir()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("web directory is not a directory")
|
||||
}
|
||||
|
||||
fi, err = os.Stat(filepath.Join(cfg.Directory, indexFileName))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot access %q in web directory: %w", indexFileName, err)
|
||||
}
|
||||
|
||||
if os.IsNotExist(err) || fi.IsDir() {
|
||||
return nil, fmt.Errorf("%q does not exist", indexFileName)
|
||||
}
|
||||
|
||||
return &Web{
|
||||
logger: logger.Named("go.signoz.io/pkg/web"),
|
||||
cfg: cfg,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (web *Web) AddToRouter(router *mux.Router) error {
|
||||
cache := middleware.NewCache(7 * 24 * time.Hour)
|
||||
err := router.PathPrefix(web.cfg.Prefix).
|
||||
Handler(
|
||||
http.StripPrefix(
|
||||
web.cfg.Prefix,
|
||||
cache.Wrap(http.HandlerFunc(web.ServeHTTP)),
|
||||
),
|
||||
).GetError()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to add web to router: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (web *Web) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
// Join internally call path.Clean to prevent directory traversal
|
||||
path := filepath.Join(web.cfg.Directory, req.URL.Path)
|
||||
|
||||
// check whether a file exists or is a directory at the given path
|
||||
fi, err := os.Stat(path)
|
||||
if os.IsNotExist(err) || fi.IsDir() {
|
||||
// file does not exist or path is a directory, serve index.html
|
||||
http.ServeFile(rw, req, filepath.Join(web.cfg.Directory, indexFileName))
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
// if we got an error (that wasn't that the file doesn't exist) stating the
|
||||
// file, return a 500 internal server error and stop
|
||||
// TODO: Put down a crash html page here
|
||||
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// otherwise, use http.FileServer to serve the static file
|
||||
http.FileServer(http.Dir(web.cfg.Directory)).ServeHTTP(rw, req)
|
||||
// Web is the interface that wraps the methods of the web package.
|
||||
type Web interface {
|
||||
// AddToRouter adds the web routes to an existing router.
|
||||
AddToRouter(router *mux.Router) error
|
||||
// ServeHTTP serves the web routes.
|
||||
http.Handler
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user