mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-29 17:24:16 +00:00
Compare commits
62 Commits
issue_2705
...
v0.52.0-cl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4878f6430 | ||
|
|
4489df6f39 | ||
|
|
06c075466b | ||
|
|
62be3e7c13 | ||
|
|
bb84960442 | ||
|
|
52199361d5 | ||
|
|
f031845300 | ||
|
|
6f73bb6eca | ||
|
|
fe398bcc49 | ||
|
|
6781c29082 | ||
|
|
eb146491f2 | ||
|
|
ae325ec1ca | ||
|
|
fd6f0574f5 | ||
|
|
b819a90c80 | ||
|
|
a6848f6abd | ||
|
|
abe65975c9 | ||
|
|
5cedd57aa2 | ||
|
|
80a7b9d16d | ||
|
|
9f7b2542ec | ||
|
|
4a4c9f26a2 | ||
|
|
c957c0f757 | ||
|
|
3ff0aa4b4b | ||
|
|
063c9adba6 | ||
|
|
5c3ce146fa | ||
|
|
481bb6e8b8 | ||
|
|
61e6316736 | ||
|
|
f9d1494657 | ||
|
|
0021b4d784 | ||
|
|
a5d5800871 | ||
|
|
16dc90bbd1 | ||
|
|
fff61379fe | ||
|
|
08a415032c | ||
|
|
3783ffdd4c | ||
|
|
a8e4359d95 | ||
|
|
d9e94a4067 | ||
|
|
ae19eaa76a | ||
|
|
fff9954da2 | ||
|
|
220edd139a | ||
|
|
59121bd932 | ||
|
|
aef935a817 | ||
|
|
f300518d61 | ||
|
|
18b608a1d8 | ||
|
|
738d62c9cf | ||
|
|
38e694cd36 | ||
|
|
1281330c52 | ||
|
|
7b7cca7db7 | ||
|
|
3134e8c1cf | ||
|
|
d00024b64a | ||
|
|
4360cd0397 | ||
|
|
a688b6c60e | ||
|
|
522e73b48e | ||
|
|
ba7e6fcf23 | ||
|
|
eefccafa5b | ||
|
|
05bd6d52f1 | ||
|
|
d60daef171 | ||
|
|
d50530f58c | ||
|
|
6957bd71ca | ||
|
|
ef8b50c19e | ||
|
|
1585065fff | ||
|
|
99c68ddbcd | ||
|
|
b08e859426 | ||
|
|
89fd3e4f55 |
@@ -211,6 +211,7 @@ services:
|
||||
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
|
||||
- ./otel-collector-opamp-config.yaml:/etc/manager-config.yaml
|
||||
- /var/lib/docker/containers:/var/lib/docker/containers:ro
|
||||
- /:/hostfs:ro
|
||||
environment:
|
||||
- OTEL_RESOURCE_ATTRIBUTES=host.name={{.Node.Hostname}},os.type={{.Node.Platform.OS}},dockerswarm.service.name={{.Service.Name}},dockerswarm.task.name={{.Task.Name}}
|
||||
- DOCKER_MULTI_NODE_CLUSTER=false
|
||||
|
||||
@@ -36,6 +36,7 @@ receivers:
|
||||
# endpoint: 0.0.0.0:6832
|
||||
hostmetrics:
|
||||
collection_interval: 30s
|
||||
root_path: /hostfs
|
||||
scrapers:
|
||||
cpu: {}
|
||||
load: {}
|
||||
|
||||
@@ -93,6 +93,8 @@ services:
|
||||
volumes:
|
||||
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
|
||||
- ./otel-collector-opamp-config.yaml:/etc/manager-config.yaml
|
||||
- /var/lib/docker/containers:/var/lib/docker/containers:ro
|
||||
- /:/hostfs:ro
|
||||
environment:
|
||||
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
|
||||
ports:
|
||||
|
||||
@@ -244,6 +244,7 @@ services:
|
||||
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
|
||||
- ./otel-collector-opamp-config.yaml:/etc/manager-config.yaml
|
||||
- /var/lib/docker/containers:/var/lib/docker/containers:ro
|
||||
- /:/hostfs:ro
|
||||
environment:
|
||||
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
|
||||
- DOCKER_MULTI_NODE_CLUSTER=false
|
||||
|
||||
@@ -243,6 +243,7 @@ services:
|
||||
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
|
||||
- ./otel-collector-opamp-config.yaml:/etc/manager-config.yaml
|
||||
- /var/lib/docker/containers:/var/lib/docker/containers:ro
|
||||
- /:/hostfs:ro
|
||||
environment:
|
||||
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
|
||||
- DOCKER_MULTI_NODE_CLUSTER=false
|
||||
|
||||
@@ -36,6 +36,7 @@ receivers:
|
||||
# endpoint: 0.0.0.0:6832
|
||||
hostmetrics:
|
||||
collection_interval: 30s
|
||||
root_path: /hostfs
|
||||
scrapers:
|
||||
cpu: {}
|
||||
load: {}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"go.signoz.io/signoz/pkg/query-service/app/dashboards"
|
||||
@@ -29,6 +31,10 @@ func (ah *APIHandler) lockUnlockDashboard(w http.ResponseWriter, r *http.Request
|
||||
|
||||
// Get the dashboard UUID from the request
|
||||
uuid := mux.Vars(r)["uuid"]
|
||||
if strings.HasPrefix(uuid,"integration") {
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorForbidden, Err: errors.New("dashboards created by integrations cannot be unlocked")}, "You are not authorized to lock/unlock this dashboard")
|
||||
return
|
||||
}
|
||||
dashboard, err := dashboards.GetDashboard(r.Context(), uuid)
|
||||
if err != nil {
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, err.Error())
|
||||
|
||||
@@ -4,11 +4,11 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
_ "net/http/pprof" // http profiler
|
||||
"os"
|
||||
"regexp"
|
||||
@@ -28,6 +28,8 @@ import (
|
||||
"go.signoz.io/signoz/ee/query-service/integrations/gateway"
|
||||
"go.signoz.io/signoz/ee/query-service/interfaces"
|
||||
baseauth "go.signoz.io/signoz/pkg/query-service/auth"
|
||||
"go.signoz.io/signoz/pkg/query-service/migrate"
|
||||
"go.signoz.io/signoz/pkg/query-service/model"
|
||||
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
||||
|
||||
licensepkg "go.signoz.io/signoz/ee/query-service/license"
|
||||
@@ -41,6 +43,7 @@ import (
|
||||
"go.signoz.io/signoz/pkg/query-service/app/logparsingpipeline"
|
||||
"go.signoz.io/signoz/pkg/query-service/app/opamp"
|
||||
opAmpModel "go.signoz.io/signoz/pkg/query-service/app/opamp/model"
|
||||
"go.signoz.io/signoz/pkg/query-service/app/preferences"
|
||||
"go.signoz.io/signoz/pkg/query-service/cache"
|
||||
baseconst "go.signoz.io/signoz/pkg/query-service/constants"
|
||||
"go.signoz.io/signoz/pkg/query-service/healthcheck"
|
||||
@@ -110,6 +113,10 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
|
||||
baseexplorer.InitWithDSN(baseconst.RELATIONAL_DATASOURCE_PATH)
|
||||
|
||||
if err := preferences.InitDB(baseconst.RELATIONAL_DATASOURCE_PATH); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
localDB, err := dashboards.InitDB(baseconst.RELATIONAL_DATASOURCE_PATH)
|
||||
|
||||
if err != nil {
|
||||
@@ -118,33 +125,13 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
|
||||
localDB.SetMaxOpenConns(10)
|
||||
|
||||
gatewayFeature := basemodel.Feature{
|
||||
Name: "GATEWAY",
|
||||
Active: false,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
}
|
||||
|
||||
//Activate this feature if the url is not empty
|
||||
var gatewayProxy *httputil.ReverseProxy
|
||||
if serverOptions.GatewayUrl == "" {
|
||||
gatewayFeature.Active = false
|
||||
gatewayProxy, err = gateway.NewNoopProxy()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
zap.L().Info("Enabling gateway feature flag ...")
|
||||
gatewayFeature.Active = true
|
||||
gatewayProxy, err = gateway.NewProxy(serverOptions.GatewayUrl, gateway.RoutePrefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gatewayProxy, err := gateway.NewProxy(serverOptions.GatewayUrl, gateway.RoutePrefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// initiate license manager
|
||||
lm, err := licensepkg.StartManager("sqlite", localDB, gatewayFeature)
|
||||
lm, err := licensepkg.StartManager("sqlite", localDB)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -193,6 +180,13 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go func() {
|
||||
err = migrate.ClickHouseMigrate(reader.GetConn(), serverOptions.Cluster)
|
||||
if err != nil {
|
||||
zap.L().Error("error while running clickhouse migrations", zap.Error(err))
|
||||
}
|
||||
}()
|
||||
|
||||
// initiate opamp
|
||||
_, err = opAmpModel.InitDB(localDB)
|
||||
if err != nil {
|
||||
@@ -340,7 +334,17 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e
|
||||
|
||||
// add auth middleware
|
||||
getUserFromRequest := func(r *http.Request) (*basemodel.UserPayload, error) {
|
||||
return auth.GetUserFromRequest(r, apiHandler)
|
||||
user, err := auth.GetUserFromRequest(r, apiHandler)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user.User.OrgId == "" {
|
||||
return nil, model.UnauthorizedError(errors.New("orgId is missing in the claims"))
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
am := baseapp.NewAuthMiddleware(getUserFromRequest)
|
||||
|
||||
@@ -732,6 +736,7 @@ func makeRulesManager(
|
||||
DisableRules: disableRules,
|
||||
FeatureFlags: fm,
|
||||
Reader: ch,
|
||||
EvalDelay: baseconst.GetEvalDelay(),
|
||||
}
|
||||
|
||||
// create Manager
|
||||
|
||||
@@ -20,11 +20,14 @@ import (
|
||||
func (m *modelDao) createUserForSAMLRequest(ctx context.Context, email string) (*basemodel.User, basemodel.BaseApiError) {
|
||||
// get auth domain from email domain
|
||||
domain, apierr := m.GetDomainByEmail(ctx, email)
|
||||
|
||||
if apierr != nil {
|
||||
zap.L().Error("failed to get domain from email", zap.Error(apierr))
|
||||
return nil, model.InternalErrorStr("failed to get domain from email")
|
||||
}
|
||||
if domain == nil {
|
||||
zap.L().Error("email domain does not match any authenticated domain", zap.String("email", email))
|
||||
return nil, model.InternalErrorStr("email domain does not match any authenticated domain")
|
||||
}
|
||||
|
||||
hash, err := baseauth.PasswordHash(utils.GeneratePassowrd())
|
||||
if err != nil {
|
||||
|
||||
@@ -5,5 +5,5 @@ import (
|
||||
)
|
||||
|
||||
func NewNoopProxy() (*httputil.ReverseProxy, error) {
|
||||
return nil, nil
|
||||
return &httputil.ReverseProxy{}, nil
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ const Enterprise = "ENTERPRISE_PLAN"
|
||||
const DisableUpsell = "DISABLE_UPSELL"
|
||||
const Onboarding = "ONBOARDING"
|
||||
const ChatSupport = "CHAT_SUPPORT"
|
||||
const Gateway = "GATEWAY"
|
||||
|
||||
var BasicPlan = basemodel.FeatureSet{
|
||||
basemodel.Feature{
|
||||
@@ -111,6 +112,13 @@ var BasicPlan = basemodel.FeatureSet{
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: Gateway,
|
||||
Active: false,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
}
|
||||
|
||||
var ProPlan = basemodel.FeatureSet{
|
||||
@@ -205,6 +213,13 @@ var ProPlan = basemodel.FeatureSet{
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: Gateway,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
}
|
||||
|
||||
var EnterprisePlan = basemodel.FeatureSet{
|
||||
@@ -313,4 +328,11 @@ var EnterprisePlan = basemodel.FeatureSet{
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: Gateway,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -76,9 +76,8 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
isUserFetching: false,
|
||||
},
|
||||
});
|
||||
|
||||
if (!isLoggedIn) {
|
||||
history.push(ROUTES.LOGIN);
|
||||
history.push(ROUTES.LOGIN, { from: pathname });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ConfigProvider } from 'antd';
|
||||
import getLocalStorageApi from 'api/browser/localstorage/get';
|
||||
import setLocalStorageApi from 'api/browser/localstorage/set';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import NotFound from 'components/NotFound';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
@@ -48,7 +49,7 @@ function App(): JSX.Element {
|
||||
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
|
||||
const { trackPageView, trackEvent } = useAnalytics();
|
||||
const { trackPageView } = useAnalytics();
|
||||
|
||||
const { hostname, pathname } = window.location;
|
||||
|
||||
@@ -199,7 +200,7 @@ function App(): JSX.Element {
|
||||
LOCALSTORAGE.THEME_ANALYTICS_V1,
|
||||
);
|
||||
if (!isThemeAnalyticsSent) {
|
||||
trackEvent('Theme Analytics', {
|
||||
logEvent('Theme Analytics', {
|
||||
theme: isDarkMode ? THEME_MODE.DARK : THEME_MODE.LIGHT,
|
||||
user: pick(user, ['email', 'userId', 'name']),
|
||||
org,
|
||||
|
||||
@@ -9,9 +9,9 @@ export function ErrorResponseHandler(error: AxiosError): ErrorResponse {
|
||||
// making the error status code as standard Error Status Code
|
||||
const statusCode = response.status as ErrorStatusCode;
|
||||
|
||||
if (statusCode >= 400 && statusCode < 500) {
|
||||
const { data } = response as AxiosResponse;
|
||||
const { data } = response as AxiosResponse;
|
||||
|
||||
if (statusCode >= 400 && statusCode < 500) {
|
||||
if (statusCode === 404) {
|
||||
return {
|
||||
statusCode,
|
||||
@@ -34,12 +34,11 @@ export function ErrorResponseHandler(error: AxiosError): ErrorResponse {
|
||||
body: JSON.stringify((response.data as any).data),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode,
|
||||
payload: null,
|
||||
error: 'Something went wrong',
|
||||
message: null,
|
||||
message: data?.error,
|
||||
};
|
||||
}
|
||||
if (request) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import axios from 'api';
|
||||
import { ApiBaseInstance as axios } from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
@@ -21,6 +21,7 @@ const logEvent = async (
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -96,6 +96,10 @@ const interceptorRejected = async (
|
||||
}
|
||||
};
|
||||
|
||||
const interceptorRejectedBase = async (
|
||||
value: AxiosResponse<any>,
|
||||
): Promise<AxiosResponse<any>> => Promise.reject(value);
|
||||
|
||||
const instance = axios.create({
|
||||
baseURL: `${ENVIRONMENT.baseURL}${apiV1}`,
|
||||
});
|
||||
@@ -140,6 +144,18 @@ ApiV4Instance.interceptors.response.use(
|
||||
ApiV4Instance.interceptors.request.use(interceptorsRequestResponse);
|
||||
//
|
||||
|
||||
// axios Base
|
||||
export const ApiBaseInstance = axios.create({
|
||||
baseURL: `${ENVIRONMENT.baseURL}${apiV1}`,
|
||||
});
|
||||
|
||||
ApiBaseInstance.interceptors.response.use(
|
||||
interceptorsResponse,
|
||||
interceptorRejectedBase,
|
||||
);
|
||||
ApiBaseInstance.interceptors.request.use(interceptorsRequestResponse);
|
||||
//
|
||||
|
||||
// gateway Api V1
|
||||
export const GatewayApiV1Instance = axios.create({
|
||||
baseURL: `${ENVIRONMENT.baseURL}${gatewayApiV1}`,
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
import axios from 'api';
|
||||
import { isNil } from 'lodash-es';
|
||||
|
||||
const getTopLevelOperations = async (): Promise<ServiceDataProps> => {
|
||||
const response = await axios.post(`/service/top_level_operations`);
|
||||
interface GetTopLevelOperationsProps {
|
||||
service?: string;
|
||||
start?: number;
|
||||
end?: number;
|
||||
}
|
||||
|
||||
const getTopLevelOperations = async (
|
||||
props: GetTopLevelOperationsProps,
|
||||
): Promise<ServiceDataProps> => {
|
||||
const response = await axios.post(`/service/top_level_operations`, {
|
||||
start: !isNil(props.start) ? `${props.start}` : undefined,
|
||||
end: !isNil(props.end) ? `${props.end}` : undefined,
|
||||
service: props.service,
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { Dayjs } from 'dayjs';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
|
||||
import { Recurrence } from './getAllDowntimeSchedules';
|
||||
@@ -11,8 +12,8 @@ export interface DowntimeSchedulePayload {
|
||||
alertIds: string[];
|
||||
schedule: {
|
||||
timezone?: string;
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
startTime?: string | Dayjs;
|
||||
endTime?: string | Dayjs;
|
||||
recurrence?: Recurrence;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import axios from 'api';
|
||||
import { AxiosError, AxiosResponse } from 'axios';
|
||||
import { Option } from 'container/PlannedDowntime/DropdownWithSubMenu/DropdownWithSubMenu';
|
||||
import { Option } from 'container/PlannedDowntime/PlannedDowntimeutils';
|
||||
import { useQuery, UseQueryResult } from 'react-query';
|
||||
|
||||
export type Recurrence = {
|
||||
@@ -28,6 +28,7 @@ export interface DowntimeSchedules {
|
||||
createdBy: string | null;
|
||||
updatedAt: string | null;
|
||||
updatedBy: string | null;
|
||||
kind: string | null;
|
||||
}
|
||||
export type PayloadProps = { data: DowntimeSchedules[] };
|
||||
|
||||
|
||||
@@ -28,12 +28,17 @@ export const SEVERITY_TEXT_TYPE = {
|
||||
FATAL2: 'FATAL2',
|
||||
FATAL3: 'FATAL3',
|
||||
FATAL4: 'FATAL4',
|
||||
UNKNOWN: 'UNKNOWN',
|
||||
} as const;
|
||||
|
||||
export const LogType = {
|
||||
TRACE: 'TRACE',
|
||||
DEBUG: 'DEBUG',
|
||||
INFO: 'INFO',
|
||||
WARNING: 'WARNING',
|
||||
WARN: 'WARN',
|
||||
ERROR: 'ERROR',
|
||||
FATAL: 'FATAL',
|
||||
UNKNOWN: 'UNKNOWN',
|
||||
} as const;
|
||||
|
||||
function LogStateIndicator({
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
import { getLogIndicatorType, getLogIndicatorTypeForTable } from './utils';
|
||||
|
||||
describe('getLogIndicatorType', () => {
|
||||
it('should return severity type for valid log with severityText', () => {
|
||||
it('severity_number should be given priority over severity_text', () => {
|
||||
const log = {
|
||||
date: '2024-02-29T12:34:46Z',
|
||||
timestamp: 1646115296,
|
||||
@@ -20,11 +21,57 @@ describe('getLogIndicatorType', () => {
|
||||
attributesInt: {},
|
||||
attributesFloat: {},
|
||||
severity_text: 'INFO',
|
||||
severity_number: 2,
|
||||
};
|
||||
expect(getLogIndicatorType(log)).toBe('INFO');
|
||||
// severity_number should get priority over severity_text
|
||||
expect(getLogIndicatorType(log)).toBe('TRACE');
|
||||
});
|
||||
|
||||
it('should return log level if severityText is missing', () => {
|
||||
it('severity_text should be used when severity_number is absent ', () => {
|
||||
const log = {
|
||||
date: '2024-02-29T12:34:46Z',
|
||||
timestamp: 1646115296,
|
||||
id: '123456',
|
||||
traceId: '987654',
|
||||
spanId: '54321',
|
||||
traceFlags: 0,
|
||||
severityText: 'INFO',
|
||||
severityNumber: 2,
|
||||
body: 'Sample log Message',
|
||||
resources_string: {},
|
||||
attributesString: {},
|
||||
attributes_string: {},
|
||||
attributesInt: {},
|
||||
attributesFloat: {},
|
||||
severity_text: 'FATAL',
|
||||
severity_number: 0,
|
||||
};
|
||||
expect(getLogIndicatorType(log)).toBe('FATAL');
|
||||
});
|
||||
|
||||
it('case insensitive severity_text should be valid', () => {
|
||||
const log = {
|
||||
date: '2024-02-29T12:34:46Z',
|
||||
timestamp: 1646115296,
|
||||
id: '123456',
|
||||
traceId: '987654',
|
||||
spanId: '54321',
|
||||
traceFlags: 0,
|
||||
severityText: 'INFO',
|
||||
severityNumber: 2,
|
||||
body: 'Sample log Message',
|
||||
resources_string: {},
|
||||
attributesString: {},
|
||||
attributes_string: {},
|
||||
attributesInt: {},
|
||||
attributesFloat: {},
|
||||
severity_text: 'fatAl',
|
||||
severity_number: 0,
|
||||
};
|
||||
expect(getLogIndicatorType(log)).toBe('FATAL');
|
||||
});
|
||||
|
||||
it('should return log level if severityText and severityNumber is missing', () => {
|
||||
const log: ILog = {
|
||||
date: '2024-02-29T12:34:58Z',
|
||||
timestamp: 1646115296,
|
||||
@@ -36,13 +83,16 @@ describe('getLogIndicatorType', () => {
|
||||
body: 'Sample log',
|
||||
resources_string: {},
|
||||
attributesString: {},
|
||||
attributes_string: {},
|
||||
attributes_string: {
|
||||
log_level: 'INFO' as never,
|
||||
},
|
||||
attributesInt: {},
|
||||
attributesFloat: {},
|
||||
severity_text: 'FATAL',
|
||||
severity_text: 'some_random',
|
||||
severityText: '',
|
||||
severity_number: 0,
|
||||
};
|
||||
expect(getLogIndicatorType(log)).toBe('FATAL');
|
||||
expect(getLogIndicatorType(log)).toBe('INFO');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -55,6 +105,7 @@ describe('getLogIndicatorTypeForTable', () => {
|
||||
traceId: '987654',
|
||||
spanId: '54321',
|
||||
traceFlags: 0,
|
||||
severityNumber: 2,
|
||||
severity_number: 2,
|
||||
body: 'Sample log message',
|
||||
resources_string: {},
|
||||
@@ -64,7 +115,7 @@ describe('getLogIndicatorTypeForTable', () => {
|
||||
attributesFloat: {},
|
||||
severity_text: 'WARN',
|
||||
};
|
||||
expect(getLogIndicatorTypeForTable(log)).toBe('WARN');
|
||||
expect(getLogIndicatorTypeForTable(log)).toBe('TRACE');
|
||||
});
|
||||
|
||||
it('should return log level if severityText is missing', () => {
|
||||
@@ -75,7 +126,8 @@ describe('getLogIndicatorTypeForTable', () => {
|
||||
traceId: '987654',
|
||||
spanId: '54321',
|
||||
traceFlags: 0,
|
||||
severityNumber: 2,
|
||||
severityNumber: 0,
|
||||
severity_number: 0,
|
||||
body: 'Sample log message',
|
||||
resources_string: {},
|
||||
attributesString: {},
|
||||
@@ -87,3 +139,47 @@ describe('getLogIndicatorTypeForTable', () => {
|
||||
expect(getLogIndicatorTypeForTable(log)).toBe('INFO');
|
||||
});
|
||||
});
|
||||
|
||||
describe('logIndicatorBySeverityNumber', () => {
|
||||
// https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-severitynumber
|
||||
const logLevelExpectations = [
|
||||
{ minSevNumber: 1, maxSevNumber: 4, expectedIndicatorType: 'TRACE' },
|
||||
{ minSevNumber: 5, maxSevNumber: 8, expectedIndicatorType: 'DEBUG' },
|
||||
{ minSevNumber: 9, maxSevNumber: 12, expectedIndicatorType: 'INFO' },
|
||||
{ minSevNumber: 13, maxSevNumber: 16, expectedIndicatorType: 'WARN' },
|
||||
{ minSevNumber: 17, maxSevNumber: 20, expectedIndicatorType: 'ERROR' },
|
||||
{ minSevNumber: 21, maxSevNumber: 24, expectedIndicatorType: 'FATAL' },
|
||||
];
|
||||
logLevelExpectations.forEach((e) => {
|
||||
for (let sevNum = e.minSevNumber; sevNum <= e.maxSevNumber; sevNum++) {
|
||||
const sevText = (Math.random() + 1).toString(36).substring(2);
|
||||
|
||||
const log = {
|
||||
date: '2024-02-29T12:34:46Z',
|
||||
timestamp: 1646115296,
|
||||
id: '123456',
|
||||
traceId: '987654',
|
||||
spanId: '54321',
|
||||
traceFlags: 0,
|
||||
severityText: sevText,
|
||||
severityNumber: sevNum,
|
||||
body: 'Sample log Message',
|
||||
resources_string: {},
|
||||
attributesString: {},
|
||||
attributes_string: {},
|
||||
attributesInt: {},
|
||||
attributesFloat: {},
|
||||
severity_text: sevText,
|
||||
severity_number: sevNum,
|
||||
};
|
||||
|
||||
it(`getLogIndicatorType should return ${e.expectedIndicatorType} for severity_text: ${sevText} and severity_number: ${sevNum}`, () => {
|
||||
expect(getLogIndicatorType(log)).toBe(e.expectedIndicatorType);
|
||||
});
|
||||
|
||||
it(`getLogIndicatorTypeForTable should return ${e.expectedIndicatorType} for severity_text: ${sevText} and severity_number: ${sevNum}`, () => {
|
||||
expect(getLogIndicatorTypeForTable(log)).toBe(e.expectedIndicatorType);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,56 +2,112 @@ import { ILog } from 'types/api/logs/log';
|
||||
|
||||
import { LogType, SEVERITY_TEXT_TYPE } from './LogStateIndicator';
|
||||
|
||||
const getSeverityType = (severityText: string): string => {
|
||||
const getLogTypeBySeverityText = (severityText: string): string => {
|
||||
switch (severityText) {
|
||||
case SEVERITY_TEXT_TYPE.TRACE:
|
||||
case SEVERITY_TEXT_TYPE.TRACE2:
|
||||
case SEVERITY_TEXT_TYPE.TRACE3:
|
||||
case SEVERITY_TEXT_TYPE.TRACE4:
|
||||
return SEVERITY_TEXT_TYPE.TRACE;
|
||||
return LogType.TRACE;
|
||||
case SEVERITY_TEXT_TYPE.DEBUG:
|
||||
case SEVERITY_TEXT_TYPE.DEBUG2:
|
||||
case SEVERITY_TEXT_TYPE.DEBUG3:
|
||||
case SEVERITY_TEXT_TYPE.DEBUG4:
|
||||
return SEVERITY_TEXT_TYPE.DEBUG;
|
||||
return LogType.DEBUG;
|
||||
case SEVERITY_TEXT_TYPE.INFO:
|
||||
case SEVERITY_TEXT_TYPE.INFO2:
|
||||
case SEVERITY_TEXT_TYPE.INFO3:
|
||||
case SEVERITY_TEXT_TYPE.INFO4:
|
||||
return SEVERITY_TEXT_TYPE.INFO;
|
||||
return LogType.INFO;
|
||||
case SEVERITY_TEXT_TYPE.WARN:
|
||||
case SEVERITY_TEXT_TYPE.WARN2:
|
||||
case SEVERITY_TEXT_TYPE.WARN3:
|
||||
case SEVERITY_TEXT_TYPE.WARN4:
|
||||
case SEVERITY_TEXT_TYPE.WARNING:
|
||||
return SEVERITY_TEXT_TYPE.WARN;
|
||||
return LogType.WARN;
|
||||
case SEVERITY_TEXT_TYPE.ERROR:
|
||||
case SEVERITY_TEXT_TYPE.ERROR2:
|
||||
case SEVERITY_TEXT_TYPE.ERROR3:
|
||||
case SEVERITY_TEXT_TYPE.ERROR4:
|
||||
return SEVERITY_TEXT_TYPE.ERROR;
|
||||
return LogType.ERROR;
|
||||
case SEVERITY_TEXT_TYPE.FATAL:
|
||||
case SEVERITY_TEXT_TYPE.FATAL2:
|
||||
case SEVERITY_TEXT_TYPE.FATAL3:
|
||||
case SEVERITY_TEXT_TYPE.FATAL4:
|
||||
return SEVERITY_TEXT_TYPE.FATAL;
|
||||
return LogType.FATAL;
|
||||
default:
|
||||
return SEVERITY_TEXT_TYPE.INFO;
|
||||
return LogType.UNKNOWN;
|
||||
}
|
||||
};
|
||||
|
||||
export const getLogIndicatorType = (logData: ILog): string => {
|
||||
if (logData.severity_text) {
|
||||
return getSeverityType(logData.severity_text);
|
||||
// https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-severitynumber
|
||||
const getLogTypeBySeverityNumber = (severityNumber: number): string => {
|
||||
if (severityNumber < 1) {
|
||||
return LogType.UNKNOWN;
|
||||
}
|
||||
return logData.attributes_string?.log_level || LogType.INFO;
|
||||
if (severityNumber < 5) {
|
||||
return LogType.TRACE;
|
||||
}
|
||||
if (severityNumber < 9) {
|
||||
return LogType.DEBUG;
|
||||
}
|
||||
if (severityNumber < 13) {
|
||||
return LogType.INFO;
|
||||
}
|
||||
if (severityNumber < 17) {
|
||||
return LogType.WARN;
|
||||
}
|
||||
if (severityNumber < 21) {
|
||||
return LogType.ERROR;
|
||||
}
|
||||
if (severityNumber < 25) {
|
||||
return LogType.FATAL;
|
||||
}
|
||||
return LogType.UNKNOWN;
|
||||
};
|
||||
|
||||
const getLogType = (
|
||||
severityText: string,
|
||||
severityNumber: number,
|
||||
defaultType: string,
|
||||
): string => {
|
||||
// give priority to the severityNumber
|
||||
if (severityNumber) {
|
||||
const logType = getLogTypeBySeverityNumber(severityNumber);
|
||||
if (logType !== LogType.UNKNOWN) {
|
||||
return logType;
|
||||
}
|
||||
}
|
||||
|
||||
// is severityNumber is not present then rely on the severityText
|
||||
if (severityText) {
|
||||
const logType = getLogTypeBySeverityText(severityText);
|
||||
if (logType !== LogType.UNKNOWN) {
|
||||
return logType;
|
||||
}
|
||||
}
|
||||
|
||||
return defaultType;
|
||||
};
|
||||
|
||||
export const getLogIndicatorType = (logData: ILog): string => {
|
||||
const defaultType = logData.attributes_string?.log_level || LogType.INFO;
|
||||
// convert the severity_text to upper case for the comparison to support case insensitive values
|
||||
return getLogType(
|
||||
logData?.severity_text?.toUpperCase(),
|
||||
logData?.severity_number || 0,
|
||||
defaultType,
|
||||
);
|
||||
};
|
||||
|
||||
export const getLogIndicatorTypeForTable = (
|
||||
log: Record<string, unknown>,
|
||||
): string => {
|
||||
if (log.severity_text) {
|
||||
return getSeverityType(log.severity_text as string);
|
||||
}
|
||||
return (log.log_level as string) || LogType.INFO;
|
||||
const defaultType = (log.log_level as string) || LogType.INFO;
|
||||
// convert the severity_text to upper case for the comparison to support case insensitive values
|
||||
return getLogType(
|
||||
(log?.severity_text as string)?.toUpperCase(),
|
||||
(log?.severity_number as number) || 0,
|
||||
defaultType,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Button } from 'antd';
|
||||
import { Tag } from 'antd/lib';
|
||||
import Input from 'components/Input';
|
||||
import { Check, X } from 'lucide-react';
|
||||
import { TweenOneGroup } from 'rc-tween-one';
|
||||
import React, { Dispatch, SetStateAction, useState } from 'react';
|
||||
|
||||
function Tags({ tags, setTags }: AddTagsProps): JSX.Element {
|
||||
@@ -46,41 +45,19 @@ function Tags({ tags, setTags }: AddTagsProps): JSX.Element {
|
||||
func(value);
|
||||
};
|
||||
|
||||
const forMap = (tag: string): React.ReactElement => (
|
||||
<span key={tag} style={{ display: 'inline-block' }}>
|
||||
<Tag
|
||||
closable
|
||||
onClose={(e): void => {
|
||||
e.preventDefault();
|
||||
handleClose(tag);
|
||||
}}
|
||||
>
|
||||
{tag}
|
||||
</Tag>
|
||||
</span>
|
||||
);
|
||||
|
||||
const tagChild = tags.map(forMap);
|
||||
|
||||
const renderTagsAnimated = (): React.ReactElement => (
|
||||
<TweenOneGroup
|
||||
appear={false}
|
||||
className="tags"
|
||||
enter={{ scale: 0.8, opacity: 0, type: 'from', duration: 100 }}
|
||||
leave={{ opacity: 0, width: 0, scale: 0, duration: 200 }}
|
||||
onEnd={(e): void => {
|
||||
if (e.type === 'appear' || e.type === 'enter') {
|
||||
(e.target as any).style = 'display: inline-block';
|
||||
}
|
||||
}}
|
||||
>
|
||||
{tagChild}
|
||||
</TweenOneGroup>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="tags-container">
|
||||
{renderTagsAnimated()}
|
||||
{tags.map<React.ReactNode>((tag) => (
|
||||
<Tag
|
||||
key={tag}
|
||||
closable
|
||||
style={{ userSelect: 'none' }}
|
||||
onClose={(): void => handleClose(tag)}
|
||||
>
|
||||
<span>{tag}</span>
|
||||
</Tag>
|
||||
))}
|
||||
|
||||
{inputVisible && (
|
||||
<div className="add-tag-container">
|
||||
<Input
|
||||
|
||||
@@ -49,7 +49,10 @@ function ValueGraph({
|
||||
}
|
||||
>
|
||||
<Tooltip title={t('this_value_satisfies_multiple_thresholds')}>
|
||||
<ExclamationCircleFilled className="value-graph-icon" />
|
||||
<ExclamationCircleFilled
|
||||
className="value-graph-icon"
|
||||
data-testid="conflicting-thresholds"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface FacingIssueBtnProps {
|
||||
buttonText?: string;
|
||||
className?: string;
|
||||
onHoverText?: string;
|
||||
intercomMessageDisabled?: boolean;
|
||||
}
|
||||
|
||||
function FacingIssueBtn({
|
||||
@@ -25,11 +26,12 @@ function FacingIssueBtn({
|
||||
buttonText = '',
|
||||
className = '',
|
||||
onHoverText = '',
|
||||
intercomMessageDisabled = false,
|
||||
}: FacingIssueBtnProps): JSX.Element | null {
|
||||
const handleFacingIssuesClick = (): void => {
|
||||
logEvent(eventName, attributes);
|
||||
|
||||
if (window.Intercom) {
|
||||
if (window.Intercom && !intercomMessageDisabled) {
|
||||
window.Intercom('showNewMessage', defaultTo(message, ''));
|
||||
}
|
||||
};
|
||||
@@ -62,6 +64,7 @@ FacingIssueBtn.defaultProps = {
|
||||
buttonText: '',
|
||||
className: '',
|
||||
onHoverText: '',
|
||||
intercomMessageDisabled: false,
|
||||
};
|
||||
|
||||
export default FacingIssueBtn;
|
||||
|
||||
@@ -19,6 +19,5 @@ export enum FeatureKeys {
|
||||
OSS = 'OSS',
|
||||
ONBOARDING = 'ONBOARDING',
|
||||
CHAT_SUPPORT = 'CHAT_SUPPORT',
|
||||
PLANNED_MAINTENANCE = 'PLANNED_MAINTENANCE',
|
||||
GATEWAY = 'GATEWAY',
|
||||
}
|
||||
|
||||
@@ -23,6 +23,10 @@ export const metricQueryFunctionOptions: SelectOption<string, string>[] = [
|
||||
value: QueryFunctionsTypes.ABSOLUTE,
|
||||
label: 'Absolute',
|
||||
},
|
||||
{
|
||||
value: QueryFunctionsTypes.RUNNING_DIFF,
|
||||
label: 'Running Diff',
|
||||
},
|
||||
{
|
||||
value: QueryFunctionsTypes.LOG_2,
|
||||
label: 'Log2',
|
||||
@@ -103,6 +107,9 @@ export const queryFunctionsTypesConfig: QueryFunctionConfigType = {
|
||||
absolute: {
|
||||
showInput: false,
|
||||
},
|
||||
runningDiff: {
|
||||
showInput: false,
|
||||
},
|
||||
log2: {
|
||||
showInput: false,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import AlertChannels from 'container/AllAlertChannels';
|
||||
import { allAlertChannels } from 'mocks-server/__mockdata__/alerts';
|
||||
import { act, fireEvent, render, screen, waitFor } from 'tests/test-utils';
|
||||
|
||||
jest.mock('hooks/useFetch', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn().mockImplementation(() => ({
|
||||
payload: allAlertChannels,
|
||||
})),
|
||||
}));
|
||||
|
||||
const successNotification = jest.fn();
|
||||
jest.mock('hooks/useNotifications', () => ({
|
||||
__esModule: true,
|
||||
useNotifications: jest.fn(() => ({
|
||||
notifications: {
|
||||
success: successNotification,
|
||||
error: jest.fn(),
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('Alert Channels Settings List page', () => {
|
||||
beforeEach(() => {
|
||||
render(<AlertChannels />);
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
describe('Should display the Alert Channels page properly', () => {
|
||||
it('Should check if "The alerts will be sent to all the configured channels." is visible ', () => {
|
||||
expect(screen.getByText('sending_channels_note')).toBeInTheDocument();
|
||||
});
|
||||
it('Should check if "New Alert Channel" Button is visble ', () => {
|
||||
expect(screen.getByText('button_new_channel')).toBeInTheDocument();
|
||||
});
|
||||
it('Should check if the help icon is visible and displays "tooltip_notification_channels ', async () => {
|
||||
const helpIcon = screen.getByLabelText('question-circle');
|
||||
|
||||
fireEvent.mouseOver(helpIcon);
|
||||
|
||||
await waitFor(() => {
|
||||
const tooltip = screen.getByText('tooltip_notification_channels');
|
||||
expect(tooltip).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('Should check if the channels table is properly displayed', () => {
|
||||
it('Should check if the table columns are properly displayed', () => {
|
||||
expect(screen.getByText('column_channel_name')).toBeInTheDocument();
|
||||
expect(screen.getByText('column_channel_type')).toBeInTheDocument();
|
||||
expect(screen.getByText('column_channel_action')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should check if the data in the table is displayed properly', () => {
|
||||
expect(screen.getByText('Dummy-Channel')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('slack')[0]).toBeInTheDocument();
|
||||
expect(screen.getAllByText('column_channel_edit')[0]).toBeInTheDocument();
|
||||
expect(screen.getAllByText('Delete')[0]).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should check if clicking on Delete displays Success Toast "Channel Deleted Successfully"', async () => {
|
||||
const deleteButton = screen.getAllByRole('button', { name: 'Delete' })[0];
|
||||
expect(deleteButton).toBeInTheDocument();
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(deleteButton);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(successNotification).toBeCalledWith({
|
||||
message: 'Success',
|
||||
description: 'channel_delete_success',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
import AlertChannels from 'container/AllAlertChannels';
|
||||
import { allAlertChannels } from 'mocks-server/__mockdata__/alerts';
|
||||
import { fireEvent, render, screen, waitFor } from 'tests/test-utils';
|
||||
|
||||
jest.mock('hooks/useFetch', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn().mockImplementation(() => ({
|
||||
payload: allAlertChannels,
|
||||
})),
|
||||
}));
|
||||
|
||||
const successNotification = jest.fn();
|
||||
jest.mock('hooks/useNotifications', () => ({
|
||||
__esModule: true,
|
||||
useNotifications: jest.fn(() => ({
|
||||
notifications: {
|
||||
success: successNotification,
|
||||
error: jest.fn(),
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/useComponentPermission', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn().mockImplementation(() => [false]),
|
||||
}));
|
||||
|
||||
describe('Alert Channels Settings List page (Normal User)', () => {
|
||||
beforeEach(() => {
|
||||
render(<AlertChannels />);
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
describe('Should display the Alert Channels page properly', () => {
|
||||
it('Should check if "The alerts will be sent to all the configured channels." is visible ', () => {
|
||||
expect(screen.getByText('sending_channels_note')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should check if "New Alert Channel" Button is visble and disabled', () => {
|
||||
const newAlertButton = screen.getByRole('button', {
|
||||
name: 'plus button_new_channel',
|
||||
});
|
||||
expect(newAlertButton).toBeInTheDocument();
|
||||
expect(newAlertButton).toBeDisabled();
|
||||
});
|
||||
it('Should check if the help icon is visible and displays "tooltip_notification_channels ', async () => {
|
||||
const helpIcon = screen.getByLabelText('question-circle');
|
||||
|
||||
fireEvent.mouseOver(helpIcon);
|
||||
|
||||
await waitFor(() => {
|
||||
const tooltip = screen.getByText('tooltip_notification_channels');
|
||||
expect(tooltip).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('Should check if the channels table is properly displayed', () => {
|
||||
it('Should check if the table columns are properly displayed', () => {
|
||||
expect(screen.getByText('column_channel_name')).toBeInTheDocument();
|
||||
expect(screen.getByText('column_channel_type')).toBeInTheDocument();
|
||||
expect(screen.queryByText('column_channel_action')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should check if the data in the table is displayed properly', () => {
|
||||
expect(screen.getByText('Dummy-Channel')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('slack')[0]).toBeInTheDocument();
|
||||
expect(screen.queryByText('column_channel_edit')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Delete')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,424 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
|
||||
import CreateAlertChannels from 'container/CreateAlertChannels';
|
||||
import { ChannelType } from 'container/CreateAlertChannels/config';
|
||||
import {
|
||||
opsGenieDescriptionDefaultValue,
|
||||
opsGenieMessageDefaultValue,
|
||||
opsGeniePriorityDefaultValue,
|
||||
pagerDutyAdditionalDetailsDefaultValue,
|
||||
pagerDutyDescriptionDefaultVaule,
|
||||
pagerDutySeverityTextDefaultValue,
|
||||
slackDescriptionDefaultValue,
|
||||
slackTitleDefaultValue,
|
||||
} from 'mocks-server/__mockdata__/alerts';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { fireEvent, render, screen, waitFor } from 'tests/test-utils';
|
||||
|
||||
import { testLabelInputAndHelpValue } from './testUtils';
|
||||
|
||||
const successNotification = jest.fn();
|
||||
const errorNotification = jest.fn();
|
||||
jest.mock('hooks/useNotifications', () => ({
|
||||
__esModule: true,
|
||||
useNotifications: jest.fn(() => ({
|
||||
notifications: {
|
||||
success: successNotification,
|
||||
error: errorNotification,
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/useFeatureFlag', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn().mockImplementation(() => ({
|
||||
active: true,
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('Create Alert Channel', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
describe('Should check if the new alert channel is properly displayed with the cascading fields of slack channel ', () => {
|
||||
beforeEach(() => {
|
||||
render(<CreateAlertChannels preType={ChannelType.Slack} />);
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('Should check if the title is "New Notification Channels"', () => {
|
||||
expect(screen.getByText('page_title_create')).toBeInTheDocument();
|
||||
});
|
||||
it('Should check if the name label and textbox are displayed properly ', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_channel_name',
|
||||
testId: 'channel-name-textbox',
|
||||
});
|
||||
});
|
||||
it('Should check if Send resolved alerts label and checkbox are displayed properly ', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_send_resolved',
|
||||
testId: 'field-send-resolved-checkbox',
|
||||
});
|
||||
});
|
||||
it('Should check if channel type label and dropdown are displayed properly', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_channel_type',
|
||||
testId: 'channel-type-select',
|
||||
});
|
||||
});
|
||||
// Default Channel type (Slack) fields
|
||||
it('Should check if the selected item in the type dropdown has text "Slack"', () => {
|
||||
expect(screen.getByText('Slack')).toBeInTheDocument();
|
||||
});
|
||||
it('Should check if Webhook URL label and input are displayed properly ', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_webhook_url',
|
||||
testId: 'webhook-url-textbox',
|
||||
});
|
||||
});
|
||||
it('Should check if Recepient label, input, and help text are displayed properly ', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_slack_recipient',
|
||||
testId: 'slack-channel-textbox',
|
||||
helpText: 'slack_channel_help',
|
||||
});
|
||||
});
|
||||
|
||||
it('Should check if Title label and text area are displayed properly ', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_slack_title',
|
||||
testId: 'title-textarea',
|
||||
});
|
||||
});
|
||||
it('Should check if Title contains template', () => {
|
||||
const titleTextArea = screen.getByTestId('title-textarea');
|
||||
|
||||
expect(titleTextArea).toHaveTextContent(slackTitleDefaultValue);
|
||||
});
|
||||
it('Should check if Description label and text area are displayed properly ', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_slack_description',
|
||||
testId: 'description-textarea',
|
||||
});
|
||||
});
|
||||
it('Should check if Description contains template', () => {
|
||||
const descriptionTextArea = screen.getByTestId('description-textarea');
|
||||
|
||||
expect(descriptionTextArea).toHaveTextContent(slackDescriptionDefaultValue);
|
||||
});
|
||||
it('Should check if the form buttons are displayed properly (Save, Test, Back)', () => {
|
||||
expect(screen.getByText('button_save_channel')).toBeInTheDocument();
|
||||
expect(screen.getByText('button_test_channel')).toBeInTheDocument();
|
||||
expect(screen.getByText('button_return')).toBeInTheDocument();
|
||||
});
|
||||
it('Should check if saving the form without filling the name displays "Something went wrong"', async () => {
|
||||
const saveButton = screen.getByRole('button', {
|
||||
name: 'button_save_channel',
|
||||
});
|
||||
|
||||
fireEvent.click(saveButton);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(errorNotification).toHaveBeenCalledWith({
|
||||
description: 'Something went wrong',
|
||||
message: 'Error',
|
||||
}),
|
||||
);
|
||||
});
|
||||
it('Should check if clicking on Test button shows "An alert has been sent to this channel" success message if testing passes', async () => {
|
||||
server.use(
|
||||
rest.post('http://localhost/api/v1/testChannel', (req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
status: 'success',
|
||||
data: 'test alert sent',
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
const testButton = screen.getByRole('button', {
|
||||
name: 'button_test_channel',
|
||||
});
|
||||
|
||||
fireEvent.click(testButton);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(successNotification).toHaveBeenCalledWith({
|
||||
message: 'Success',
|
||||
description: 'channel_test_done',
|
||||
}),
|
||||
);
|
||||
});
|
||||
it('Should check if clicking on Test button shows "Something went wrong" error message if testing fails', async () => {
|
||||
const testButton = screen.getByRole('button', {
|
||||
name: 'button_test_channel',
|
||||
});
|
||||
|
||||
fireEvent.click(testButton);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(errorNotification).toHaveBeenCalledWith({
|
||||
message: 'Error',
|
||||
description: 'channel_test_failed',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('New Alert Channel Cascading Fields Based on Channel Type', () => {
|
||||
describe('Webhook', () => {
|
||||
beforeEach(() => {
|
||||
render(<CreateAlertChannels preType={ChannelType.Webhook} />);
|
||||
});
|
||||
|
||||
it('Should check if the selected item in the type dropdown has text "Webhook"', () => {
|
||||
expect(screen.getByText('Webhook')).toBeInTheDocument();
|
||||
});
|
||||
it('Should check if Webhook URL label and input are displayed properly ', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_webhook_url',
|
||||
testId: 'webhook-url-textbox',
|
||||
});
|
||||
});
|
||||
it('Should check if Webhook User Name label, input, and help text are displayed properly ', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_webhook_username',
|
||||
testId: 'webhook-username-textbox',
|
||||
helpText: 'help_webhook_username',
|
||||
});
|
||||
});
|
||||
it('Should check if Password label and textbox, and help text are displayed properly', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'Password (optional)',
|
||||
testId: 'webhook-password-textbox',
|
||||
helpText: 'help_webhook_password',
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('PagerDuty', () => {
|
||||
beforeEach(() => {
|
||||
render(<CreateAlertChannels preType={ChannelType.Pagerduty} />);
|
||||
});
|
||||
|
||||
it('Should check if the selected item in the type dropdown has text "Pagerduty"', () => {
|
||||
expect(screen.getByText('Pagerduty')).toBeInTheDocument();
|
||||
});
|
||||
it('Should check if Routing key label, required, and textbox are displayed properly', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_pager_routing_key',
|
||||
testId: 'pager-routing-key-textbox',
|
||||
});
|
||||
});
|
||||
it('Should check if Description label, required, info (Shows up as description in pagerduty), and text area are displayed properly', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_pager_description',
|
||||
testId: 'pager-description-textarea',
|
||||
helpText: 'help_pager_description',
|
||||
});
|
||||
});
|
||||
it('Should check if the description contains default template', () => {
|
||||
const descriptionTextArea = screen.getByTestId(
|
||||
'pager-description-textarea',
|
||||
);
|
||||
|
||||
expect(descriptionTextArea).toHaveTextContent(
|
||||
pagerDutyDescriptionDefaultVaule,
|
||||
);
|
||||
});
|
||||
it('Should check if Severity label, info (help_pager_severity), and textbox are displayed properly', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_pager_severity',
|
||||
testId: 'pager-severity-textbox',
|
||||
helpText: 'help_pager_severity',
|
||||
});
|
||||
});
|
||||
it('Should check if Severity contains the default template', () => {
|
||||
const severityTextbox = screen.getByTestId('pager-severity-textbox');
|
||||
|
||||
expect(severityTextbox).toHaveValue(pagerDutySeverityTextDefaultValue);
|
||||
});
|
||||
it('Should check if Additional Information label, text area, and help text (help_pager_details) are displayed properly', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_pager_details',
|
||||
testId: 'pager-additional-details-textarea',
|
||||
helpText: 'help_pager_details',
|
||||
});
|
||||
});
|
||||
it('Should check if Additional Information contains the default template', () => {
|
||||
const detailsTextArea = screen.getByTestId(
|
||||
'pager-additional-details-textarea',
|
||||
);
|
||||
|
||||
expect(detailsTextArea).toHaveValue(pagerDutyAdditionalDetailsDefaultValue);
|
||||
});
|
||||
it('Should check if Group label, text area, and info (help_pager_group) are displayed properly', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_pager_group',
|
||||
testId: 'pager-group-textarea',
|
||||
helpText: 'help_pager_group',
|
||||
});
|
||||
});
|
||||
it('Should check if Class label, text area, and info (help_pager_class) are displayed properly', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_pager_class',
|
||||
testId: 'pager-class-textarea',
|
||||
helpText: 'help_pager_class',
|
||||
});
|
||||
});
|
||||
it('Should check if Client label, text area, and info (Shows up as event source in Pagerduty) are displayed properly', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_pager_client',
|
||||
testId: 'pager-client-textarea',
|
||||
helpText: 'help_pager_client',
|
||||
});
|
||||
});
|
||||
it('Should check if Client input contains the default value "SigNoz Alert Manager"', () => {
|
||||
const clientTextArea = screen.getByTestId('pager-client-textarea');
|
||||
|
||||
expect(clientTextArea).toHaveValue('SigNoz Alert Manager');
|
||||
});
|
||||
it('Should check if Client URL label, text area, and info (Shows up as event source link in Pagerduty) are displayed properly', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_pager_client_url',
|
||||
testId: 'pager-client-url-textarea',
|
||||
helpText: 'help_pager_client_url',
|
||||
});
|
||||
});
|
||||
it('Should check if Client URL contains the default value "https://enter-signoz-host-n-port-here/alerts"', () => {
|
||||
const clientUrlTextArea = screen.getByTestId('pager-client-url-textarea');
|
||||
|
||||
expect(clientUrlTextArea).toHaveValue(
|
||||
'https://enter-signoz-host-n-port-here/alerts',
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('Opsgenie', () => {
|
||||
beforeEach(() => {
|
||||
render(<CreateAlertChannels preType={ChannelType.Opsgenie} />);
|
||||
});
|
||||
|
||||
it('Should check if the selected item in the type dropdown has text "Opsgenie"', () => {
|
||||
expect(screen.getByText('Opsgenie')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should check if API key label, required, and textbox are displayed properly', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_opsgenie_api_key',
|
||||
testId: 'opsgenie-api-key-textbox',
|
||||
required: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('Should check if Message label, required, info (Shows up as message in opsgenie), and text area are displayed properly', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_opsgenie_message',
|
||||
testId: 'opsgenie-message-textarea',
|
||||
helpText: 'help_opsgenie_message',
|
||||
required: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('Should check if Message contains the default template ', () => {
|
||||
const messageTextArea = screen.getByTestId('opsgenie-message-textarea');
|
||||
|
||||
expect(messageTextArea).toHaveValue(opsGenieMessageDefaultValue);
|
||||
});
|
||||
|
||||
it('Should check if Description label, required, info (Shows up as description in opsgenie), and text area are displayed properly `{{ if gt (len .Alerts.Firing) 0 -}}', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_opsgenie_description',
|
||||
testId: 'opsgenie-description-textarea',
|
||||
helpText: 'help_opsgenie_description',
|
||||
required: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('Should check if Description label, required, info (Shows up as description in opsgenie), and text area are displayed properly `{{ if gt (len .Alerts.Firing) 0 -}}', () => {
|
||||
const descriptionTextArea = screen.getByTestId(
|
||||
'opsgenie-description-textarea',
|
||||
);
|
||||
|
||||
expect(descriptionTextArea).toHaveTextContent(
|
||||
opsGenieDescriptionDefaultValue,
|
||||
);
|
||||
});
|
||||
|
||||
it('Should check if Priority label, required, info (help_opsgenie_priority), and text area are displayed properly', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_opsgenie_priority',
|
||||
testId: 'opsgenie-priority-textarea',
|
||||
helpText: 'help_opsgenie_priority',
|
||||
required: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('Should check if Message contains the default template', () => {
|
||||
const priorityTextArea = screen.getByTestId('opsgenie-priority-textarea');
|
||||
|
||||
expect(priorityTextArea).toHaveValue(opsGeniePriorityDefaultValue);
|
||||
});
|
||||
});
|
||||
describe('Opsgenie', () => {
|
||||
beforeEach(() => {
|
||||
render(<CreateAlertChannels preType={ChannelType.Email} />);
|
||||
});
|
||||
|
||||
it('Should check if the selected item in the type dropdown has text "Email"', () => {
|
||||
expect(screen.getByText('Email')).toBeInTheDocument();
|
||||
});
|
||||
it('Should check if API key label, required, info(help_email_to), and textbox are displayed properly', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_email_to',
|
||||
testId: 'email-to-textbox',
|
||||
helpText: 'help_email_to',
|
||||
required: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('Microsoft Teams', () => {
|
||||
beforeEach(() => {
|
||||
render(<CreateAlertChannels preType={ChannelType.MsTeams} />);
|
||||
});
|
||||
|
||||
it('Should check if the selected item in the type dropdown has text "msteams"', () => {
|
||||
expect(screen.getByText('msteams')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should check if Webhook URL label and input are displayed properly ', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_webhook_url',
|
||||
testId: 'webhook-url-textbox',
|
||||
});
|
||||
});
|
||||
|
||||
it('Should check if Title label and text area are displayed properly ', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_slack_title',
|
||||
testId: 'title-textarea',
|
||||
});
|
||||
});
|
||||
|
||||
it('Should check if Title contains template', () => {
|
||||
const titleTextArea = screen.getByTestId('title-textarea');
|
||||
|
||||
expect(titleTextArea).toHaveTextContent(slackTitleDefaultValue);
|
||||
});
|
||||
it('Should check if Description label and text area are displayed properly ', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_slack_description',
|
||||
testId: 'description-textarea',
|
||||
});
|
||||
});
|
||||
|
||||
it('Should check if Description contains template', () => {
|
||||
const descriptionTextArea = screen.getByTestId('description-textarea');
|
||||
|
||||
expect(descriptionTextArea).toHaveTextContent(slackDescriptionDefaultValue);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,348 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
|
||||
import { SIGNOZ_UPGRADE_PLAN_URL } from 'constants/app';
|
||||
import CreateAlertChannels from 'container/CreateAlertChannels';
|
||||
import { ChannelType } from 'container/CreateAlertChannels/config';
|
||||
import {
|
||||
opsGenieDescriptionDefaultValue,
|
||||
opsGenieMessageDefaultValue,
|
||||
opsGeniePriorityDefaultValue,
|
||||
pagerDutyAdditionalDetailsDefaultValue,
|
||||
pagerDutyDescriptionDefaultVaule,
|
||||
pagerDutySeverityTextDefaultValue,
|
||||
slackDescriptionDefaultValue,
|
||||
slackTitleDefaultValue,
|
||||
} from 'mocks-server/__mockdata__/alerts';
|
||||
import { render, screen } from 'tests/test-utils';
|
||||
|
||||
import { testLabelInputAndHelpValue } from './testUtils';
|
||||
|
||||
describe('Create Alert Channel (Normal User)', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
describe('Should check if the new alert channel is properly displayed with the cascading fields of slack channel ', () => {
|
||||
beforeEach(() => {
|
||||
render(<CreateAlertChannels preType={ChannelType.Slack} />);
|
||||
});
|
||||
it('Should check if the title is "New Notification Channels"', () => {
|
||||
expect(screen.getByText('page_title_create')).toBeInTheDocument();
|
||||
});
|
||||
it('Should check if the name label and textbox are displayed properly ', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_channel_name',
|
||||
testId: 'channel-name-textbox',
|
||||
});
|
||||
});
|
||||
it('Should check if Send resolved alerts label and checkbox are displayed properly ', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_send_resolved',
|
||||
testId: 'field-send-resolved-checkbox',
|
||||
});
|
||||
});
|
||||
it('Should check if channel type label and dropdown are displayed properly', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_channel_type',
|
||||
testId: 'channel-type-select',
|
||||
});
|
||||
});
|
||||
// Default Channel type (Slack) fields
|
||||
it('Should check if the selected item in the type dropdown has text "Slack"', () => {
|
||||
expect(screen.getByText('Slack')).toBeInTheDocument();
|
||||
});
|
||||
it('Should check if Webhook URL label and input are displayed properly ', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_webhook_url',
|
||||
testId: 'webhook-url-textbox',
|
||||
});
|
||||
});
|
||||
it('Should check if Recepient label, input, and help text are displayed properly ', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_slack_recipient',
|
||||
testId: 'slack-channel-textbox',
|
||||
helpText: 'slack_channel_help',
|
||||
});
|
||||
});
|
||||
|
||||
it('Should check if Title label and text area are displayed properly ', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_slack_title',
|
||||
testId: 'title-textarea',
|
||||
});
|
||||
});
|
||||
it('Should check if Title contains template', () => {
|
||||
const titleTextArea = screen.getByTestId('title-textarea');
|
||||
|
||||
expect(titleTextArea).toHaveTextContent(slackTitleDefaultValue);
|
||||
});
|
||||
it('Should check if Description label and text area are displayed properly ', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_slack_description',
|
||||
testId: 'description-textarea',
|
||||
});
|
||||
});
|
||||
it('Should check if Description contains template', () => {
|
||||
const descriptionTextArea = screen.getByTestId('description-textarea');
|
||||
|
||||
expect(descriptionTextArea).toHaveTextContent(slackDescriptionDefaultValue);
|
||||
});
|
||||
it('Should check if the form buttons are displayed properly (Save, Test, Back)', () => {
|
||||
expect(screen.getByText('button_save_channel')).toBeInTheDocument();
|
||||
expect(screen.getByText('button_test_channel')).toBeInTheDocument();
|
||||
expect(screen.getByText('button_return')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
describe('New Alert Channel Cascading Fields Based on Channel Type', () => {
|
||||
describe('Webhook', () => {
|
||||
beforeEach(() => {
|
||||
render(<CreateAlertChannels preType={ChannelType.Webhook} />);
|
||||
});
|
||||
|
||||
it('Should check if the selected item in the type dropdown has text "Webhook"', () => {
|
||||
expect(screen.getByText('Webhook')).toBeInTheDocument();
|
||||
});
|
||||
it('Should check if Webhook URL label and input are displayed properly ', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_webhook_url',
|
||||
testId: 'webhook-url-textbox',
|
||||
});
|
||||
});
|
||||
it('Should check if Webhook User Name label, input, and help text are displayed properly ', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_webhook_username',
|
||||
testId: 'webhook-username-textbox',
|
||||
helpText: 'help_webhook_username',
|
||||
});
|
||||
});
|
||||
it('Should check if Password label and textbox, and help text are displayed properly', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'Password (optional)',
|
||||
testId: 'webhook-password-textbox',
|
||||
helpText: 'help_webhook_password',
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('PagerDuty', () => {
|
||||
beforeEach(() => {
|
||||
render(<CreateAlertChannels preType={ChannelType.Pagerduty} />);
|
||||
});
|
||||
|
||||
it('Should check if the selected item in the type dropdown has text "Pagerduty"', () => {
|
||||
expect(screen.getByText('Pagerduty')).toBeInTheDocument();
|
||||
});
|
||||
it('Should check if Routing key label, required, and textbox are displayed properly', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_pager_routing_key',
|
||||
testId: 'pager-routing-key-textbox',
|
||||
});
|
||||
});
|
||||
it('Should check if Description label, required, info (Shows up as description in pagerduty), and text area are displayed properly', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_pager_description',
|
||||
testId: 'pager-description-textarea',
|
||||
helpText: 'help_pager_description',
|
||||
});
|
||||
});
|
||||
it('Should check if the description contains default template', () => {
|
||||
const descriptionTextArea = screen.getByTestId(
|
||||
'pager-description-textarea',
|
||||
);
|
||||
|
||||
expect(descriptionTextArea).toHaveTextContent(
|
||||
pagerDutyDescriptionDefaultVaule,
|
||||
);
|
||||
});
|
||||
it('Should check if Severity label, info (help_pager_severity), and textbox are displayed properly', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_pager_severity',
|
||||
testId: 'pager-severity-textbox',
|
||||
helpText: 'help_pager_severity',
|
||||
});
|
||||
});
|
||||
it('Should check if Severity contains the default template', () => {
|
||||
const severityTextbox = screen.getByTestId('pager-severity-textbox');
|
||||
|
||||
expect(severityTextbox).toHaveValue(pagerDutySeverityTextDefaultValue);
|
||||
});
|
||||
it('Should check if Additional Information label, text area, and help text (help_pager_details) are displayed properly', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_pager_details',
|
||||
testId: 'pager-additional-details-textarea',
|
||||
helpText: 'help_pager_details',
|
||||
});
|
||||
});
|
||||
it('Should check if Additional Information contains the default template', () => {
|
||||
const detailsTextArea = screen.getByTestId(
|
||||
'pager-additional-details-textarea',
|
||||
);
|
||||
|
||||
expect(detailsTextArea).toHaveValue(pagerDutyAdditionalDetailsDefaultValue);
|
||||
});
|
||||
it('Should check if Group label, text area, and info (help_pager_group) are displayed properly', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_pager_group',
|
||||
testId: 'pager-group-textarea',
|
||||
helpText: 'help_pager_group',
|
||||
});
|
||||
});
|
||||
it('Should check if Class label, text area, and info (help_pager_class) are displayed properly', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_pager_class',
|
||||
testId: 'pager-class-textarea',
|
||||
helpText: 'help_pager_class',
|
||||
});
|
||||
});
|
||||
it('Should check if Client label, text area, and info (Shows up as event source in Pagerduty) are displayed properly', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_pager_client',
|
||||
testId: 'pager-client-textarea',
|
||||
helpText: 'help_pager_client',
|
||||
});
|
||||
});
|
||||
it('Should check if Client input contains the default value "SigNoz Alert Manager"', () => {
|
||||
const clientTextArea = screen.getByTestId('pager-client-textarea');
|
||||
|
||||
expect(clientTextArea).toHaveValue('SigNoz Alert Manager');
|
||||
});
|
||||
it('Should check if Client URL label, text area, and info (Shows up as event source link in Pagerduty) are displayed properly', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_pager_client_url',
|
||||
testId: 'pager-client-url-textarea',
|
||||
helpText: 'help_pager_client_url',
|
||||
});
|
||||
});
|
||||
it('Should check if Client URL contains the default value "https://enter-signoz-host-n-port-here/alerts"', () => {
|
||||
const clientUrlTextArea = screen.getByTestId('pager-client-url-textarea');
|
||||
|
||||
expect(clientUrlTextArea).toHaveValue(
|
||||
'https://enter-signoz-host-n-port-here/alerts',
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('Opsgenie', () => {
|
||||
beforeEach(() => {
|
||||
render(<CreateAlertChannels preType={ChannelType.Opsgenie} />);
|
||||
});
|
||||
|
||||
it('Should check if the selected item in the type dropdown has text "Opsgenie"', () => {
|
||||
expect(screen.getByText('Opsgenie')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should check if API key label, required, and textbox are displayed properly', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_opsgenie_api_key',
|
||||
testId: 'opsgenie-api-key-textbox',
|
||||
required: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('Should check if Message label, required, info (Shows up as message in opsgenie), and text area are displayed properly', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_opsgenie_message',
|
||||
testId: 'opsgenie-message-textarea',
|
||||
helpText: 'help_opsgenie_message',
|
||||
required: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('Should check if Message contains the default template ', () => {
|
||||
const messageTextArea = screen.getByTestId('opsgenie-message-textarea');
|
||||
|
||||
expect(messageTextArea).toHaveValue(opsGenieMessageDefaultValue);
|
||||
});
|
||||
|
||||
it('Should check if Description label, required, info (Shows up as description in opsgenie), and text area are displayed properly `{{ if gt (len .Alerts.Firing) 0 -}}', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_opsgenie_description',
|
||||
testId: 'opsgenie-description-textarea',
|
||||
helpText: 'help_opsgenie_description',
|
||||
required: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('Should check if Description label, required, info (Shows up as description in opsgenie), and text area are displayed properly `{{ if gt (len .Alerts.Firing) 0 -}}', () => {
|
||||
const descriptionTextArea = screen.getByTestId(
|
||||
'opsgenie-description-textarea',
|
||||
);
|
||||
|
||||
expect(descriptionTextArea).toHaveTextContent(
|
||||
opsGenieDescriptionDefaultValue,
|
||||
);
|
||||
});
|
||||
|
||||
it('Should check if Priority label, required, info (help_opsgenie_priority), and text area are displayed properly', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_opsgenie_priority',
|
||||
testId: 'opsgenie-priority-textarea',
|
||||
helpText: 'help_opsgenie_priority',
|
||||
required: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('Should check if Message contains the default template', () => {
|
||||
const priorityTextArea = screen.getByTestId('opsgenie-priority-textarea');
|
||||
|
||||
expect(priorityTextArea).toHaveValue(opsGeniePriorityDefaultValue);
|
||||
});
|
||||
});
|
||||
describe('Opsgenie', () => {
|
||||
beforeEach(() => {
|
||||
render(<CreateAlertChannels preType={ChannelType.Email} />);
|
||||
});
|
||||
|
||||
it('Should check if the selected item in the type dropdown has text "Email"', () => {
|
||||
expect(screen.getByText('Email')).toBeInTheDocument();
|
||||
});
|
||||
it('Should check if API key label, required, info(help_email_to), and textbox are displayed properly', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_email_to',
|
||||
testId: 'email-to-textbox',
|
||||
helpText: 'help_email_to',
|
||||
required: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('Microsoft Teams', () => {
|
||||
beforeEach(() => {
|
||||
render(<CreateAlertChannels preType={ChannelType.MsTeams} />);
|
||||
});
|
||||
|
||||
it('Should check if the selected item in the type dropdown has text "Microsoft Teams (Supported in Paid Plans Only)"', () => {
|
||||
expect(
|
||||
screen.getByText('Microsoft Teams (Supported in Paid Plans Only)'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should check if the upgrade plan message is shown', () => {
|
||||
expect(screen.getByText('Upgrade to a Paid Plan')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(/This feature is available for paid plans only./),
|
||||
).toBeInTheDocument();
|
||||
const link = screen.getByRole('link', { name: 'Click here' });
|
||||
expect(link).toBeInTheDocument();
|
||||
expect(link).toHaveAttribute('href', SIGNOZ_UPGRADE_PLAN_URL);
|
||||
expect(screen.getByText(/to Upgrade/)).toBeInTheDocument();
|
||||
});
|
||||
it('Should check if the form buttons are displayed properly (Save, Test, Back)', () => {
|
||||
expect(
|
||||
screen.getByRole('button', { name: 'button_save_channel' }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('button', { name: 'button_test_channel' }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('button', { name: 'button_return' }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
it('Should check if save and test buttons are disabled', () => {
|
||||
expect(
|
||||
screen.getByRole('button', { name: 'button_save_channel' }),
|
||||
).toBeDisabled();
|
||||
expect(
|
||||
screen.getByRole('button', { name: 'button_test_channel' }),
|
||||
).toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,118 @@
|
||||
import EditAlertChannels from 'container/EditAlertChannels';
|
||||
import {
|
||||
editAlertChannelInitialValue,
|
||||
editSlackDescriptionDefaultValue,
|
||||
slackTitleDefaultValue,
|
||||
} from 'mocks-server/__mockdata__/alerts';
|
||||
import { render, screen } from 'tests/test-utils';
|
||||
|
||||
import { testLabelInputAndHelpValue } from './testUtils';
|
||||
|
||||
const successNotification = jest.fn();
|
||||
const errorNotification = jest.fn();
|
||||
jest.mock('hooks/useNotifications', () => ({
|
||||
__esModule: true,
|
||||
useNotifications: jest.fn(() => ({
|
||||
notifications: {
|
||||
success: successNotification,
|
||||
error: errorNotification,
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/useFeatureFlag', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn().mockImplementation(() => ({
|
||||
active: true,
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('Should check if the edit alert channel is properly displayed ', () => {
|
||||
beforeEach(() => {
|
||||
render(<EditAlertChannels initialValue={editAlertChannelInitialValue} />);
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('Should check if the title is "Edit Notification Channels"', () => {
|
||||
expect(screen.getByText('page_title_edit')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should check if the name label and textbox are displayed properly ', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_channel_name',
|
||||
testId: 'channel-name-textbox',
|
||||
value: 'Dummy-Channel',
|
||||
});
|
||||
});
|
||||
it('Should check if Send resolved alerts label and checkbox are displayed properly and the checkbox is checked ', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_send_resolved',
|
||||
testId: 'field-send-resolved-checkbox',
|
||||
});
|
||||
expect(screen.getByTestId('field-send-resolved-checkbox')).toBeChecked();
|
||||
});
|
||||
|
||||
it('Should check if channel type label and dropdown are displayed properly', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_channel_type',
|
||||
testId: 'channel-type-select',
|
||||
});
|
||||
});
|
||||
|
||||
it('Should check if the selected item in the type dropdown has text "Slack"', () => {
|
||||
expect(screen.getByText('Slack')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should check if Webhook URL label and input are displayed properly ', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_webhook_url',
|
||||
testId: 'webhook-url-textbox',
|
||||
value:
|
||||
'https://discord.com/api/webhooks/dummy_webhook_id/dummy_webhook_token/slack',
|
||||
});
|
||||
});
|
||||
|
||||
it('Should check if Recepient label, input, and help text are displayed properly ', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_slack_recipient',
|
||||
testId: 'slack-channel-textbox',
|
||||
helpText: 'slack_channel_help',
|
||||
value: '#dummy_channel',
|
||||
});
|
||||
});
|
||||
|
||||
it('Should check if Title label and text area are displayed properly ', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_slack_title',
|
||||
testId: 'title-textarea',
|
||||
});
|
||||
});
|
||||
|
||||
it('Should check if Title contains template', () => {
|
||||
const titleTextArea = screen.getByTestId('title-textarea');
|
||||
|
||||
expect(titleTextArea).toHaveTextContent(slackTitleDefaultValue);
|
||||
});
|
||||
|
||||
it('Should check if Description label and text area are displayed properly ', () => {
|
||||
testLabelInputAndHelpValue({
|
||||
labelText: 'field_slack_description',
|
||||
testId: 'description-textarea',
|
||||
});
|
||||
});
|
||||
|
||||
it('Should check if Description contains template', () => {
|
||||
const descriptionTextArea = screen.getByTestId('description-textarea');
|
||||
|
||||
expect(descriptionTextArea).toHaveTextContent(
|
||||
editSlackDescriptionDefaultValue,
|
||||
);
|
||||
});
|
||||
|
||||
it('Should check if the form buttons are displayed properly (Save, Test, Back)', () => {
|
||||
expect(screen.getByText('button_save_channel')).toBeInTheDocument();
|
||||
expect(screen.getByText('button_test_channel')).toBeInTheDocument();
|
||||
expect(screen.getByText('button_return')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import { screen } from 'tests/test-utils';
|
||||
|
||||
export const testLabelInputAndHelpValue = ({
|
||||
labelText,
|
||||
testId,
|
||||
helpText,
|
||||
required = false,
|
||||
value,
|
||||
}: {
|
||||
labelText: string;
|
||||
testId: string;
|
||||
helpText?: string;
|
||||
required?: boolean;
|
||||
value?: string;
|
||||
}): void => {
|
||||
const label = screen.getByText(labelText);
|
||||
expect(label).toBeInTheDocument();
|
||||
|
||||
const input = screen.getByTestId(testId);
|
||||
expect(input).toBeInTheDocument();
|
||||
|
||||
if (helpText !== undefined) {
|
||||
expect(screen.getByText(helpText)).toBeInTheDocument();
|
||||
}
|
||||
if (required) {
|
||||
expect(input).toBeRequired();
|
||||
}
|
||||
if (value) {
|
||||
expect(input).toHaveValue(value);
|
||||
}
|
||||
};
|
||||
@@ -19,10 +19,10 @@ import { ColumnsType } from 'antd/es/table';
|
||||
import updateCreditCardApi from 'api/billing/checkout';
|
||||
import getUsage, { UsageResponsePayloadProps } from 'api/billing/getUsage';
|
||||
import manageCreditCardApi from 'api/billing/manage';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
||||
import useAxiosError from 'hooks/useAxiosError';
|
||||
import useLicense from 'hooks/useLicense';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
@@ -137,8 +137,6 @@ export default function BillingContainer(): JSX.Element {
|
||||
Partial<UsageResponsePayloadProps>
|
||||
>({});
|
||||
|
||||
const { trackEvent } = useAnalytics();
|
||||
|
||||
const { isFetching, data: licensesData, error: licenseError } = useLicense();
|
||||
|
||||
const { user, org } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
@@ -316,7 +314,7 @@ export default function BillingContainer(): JSX.Element {
|
||||
|
||||
const handleBilling = useCallback(async () => {
|
||||
if (isFreeTrial && !licensesData?.payload?.trialConvertedToSubscription) {
|
||||
trackEvent('Billing : Upgrade Plan', {
|
||||
logEvent('Billing : Upgrade Plan', {
|
||||
user: pick(user, ['email', 'userId', 'name']),
|
||||
org,
|
||||
});
|
||||
@@ -327,7 +325,7 @@ export default function BillingContainer(): JSX.Element {
|
||||
cancelURL: window.location.href,
|
||||
});
|
||||
} else {
|
||||
trackEvent('Billing : Manage Billing', {
|
||||
logEvent('Billing : Manage Billing', {
|
||||
user: pick(user, ['email', 'userId', 'name']),
|
||||
org,
|
||||
});
|
||||
|
||||
@@ -27,6 +27,7 @@ function EmailForm({ setSelectedConfig }: EmailFormProps): JSX.Element {
|
||||
<Input
|
||||
onChange={handleInputChange('to')}
|
||||
placeholder={t('placeholder_email_to')}
|
||||
data-testid="email-to-textbox"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ function MsTeams({ setSelectedConfig }: MsTeamsProps): JSX.Element {
|
||||
webhook_url: event.target.value,
|
||||
}));
|
||||
}}
|
||||
data-testid="webhook-url-textbox"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
@@ -30,6 +31,7 @@ function MsTeams({ setSelectedConfig }: MsTeamsProps): JSX.Element {
|
||||
title: event.target.value,
|
||||
}))
|
||||
}
|
||||
data-testid="title-textarea"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
@@ -41,6 +43,7 @@ function MsTeams({ setSelectedConfig }: MsTeamsProps): JSX.Element {
|
||||
text: event.target.value,
|
||||
}))
|
||||
}
|
||||
data-testid="description-textarea"
|
||||
placeholder={t('placeholder_slack_description')}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
@@ -20,7 +20,10 @@ function OpsgenieForm({ setSelectedConfig }: OpsgenieFormProps): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<Form.Item name="api_key" label={t('field_opsgenie_api_key')} required>
|
||||
<Input onChange={handleInputChange('api_key')} />
|
||||
<Input
|
||||
onChange={handleInputChange('api_key')}
|
||||
data-testid="opsgenie-api-key-textbox"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
@@ -33,6 +36,7 @@ function OpsgenieForm({ setSelectedConfig }: OpsgenieFormProps): JSX.Element {
|
||||
rows={4}
|
||||
onChange={handleInputChange('message')}
|
||||
placeholder={t('placeholder_opsgenie_message')}
|
||||
data-testid="opsgenie-message-textarea"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
@@ -46,6 +50,7 @@ function OpsgenieForm({ setSelectedConfig }: OpsgenieFormProps): JSX.Element {
|
||||
rows={4}
|
||||
onChange={handleInputChange('description')}
|
||||
placeholder={t('placeholder_opsgenie_description')}
|
||||
data-testid="opsgenie-description-textarea"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
@@ -59,6 +64,7 @@ function OpsgenieForm({ setSelectedConfig }: OpsgenieFormProps): JSX.Element {
|
||||
rows={4}
|
||||
onChange={handleInputChange('priority')}
|
||||
placeholder={t('placeholder_opsgenie_priority')}
|
||||
data-testid="opsgenie-priority-textarea"
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
|
||||
@@ -18,6 +18,7 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
|
||||
routing_key: event.target.value,
|
||||
}));
|
||||
}}
|
||||
data-testid="pager-routing-key-textbox"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
@@ -36,6 +37,7 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
|
||||
}))
|
||||
}
|
||||
placeholder={t('placeholder_pager_description')}
|
||||
data-testid="pager-description-textarea"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
@@ -51,6 +53,7 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
|
||||
severity: event.target.value,
|
||||
}))
|
||||
}
|
||||
data-testid="pager-severity-textbox"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
@@ -67,6 +70,7 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
|
||||
details: event.target.value,
|
||||
}))
|
||||
}
|
||||
data-testid="pager-additional-details-textarea"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
@@ -97,6 +101,7 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
|
||||
group: event.target.value,
|
||||
}))
|
||||
}
|
||||
data-testid="pager-group-textarea"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
@@ -112,6 +117,7 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
|
||||
class: event.target.value,
|
||||
}))
|
||||
}
|
||||
data-testid="pager-class-textarea"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
@@ -126,6 +132,7 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
|
||||
client: event.target.value,
|
||||
}))
|
||||
}
|
||||
data-testid="pager-client-textarea"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
@@ -141,6 +148,7 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
|
||||
client_url: event.target.value,
|
||||
}))
|
||||
}
|
||||
data-testid="pager-client-url-textarea"
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
|
||||
@@ -19,6 +19,7 @@ function Slack({ setSelectedConfig }: SlackProps): JSX.Element {
|
||||
api_url: event.target.value,
|
||||
}));
|
||||
}}
|
||||
data-testid="webhook-url-textbox"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
@@ -34,11 +35,13 @@ function Slack({ setSelectedConfig }: SlackProps): JSX.Element {
|
||||
channel: event.target.value,
|
||||
}))
|
||||
}
|
||||
data-testid="slack-channel-textbox"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="title" label={t('field_slack_title')}>
|
||||
<TextArea
|
||||
data-testid="title-textarea"
|
||||
rows={4}
|
||||
// value={`[{{ .Status | toUpper }}{{ if eq .Status \"firing\" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .CommonLabels.alertname }} for {{ .CommonLabels.job }}\n{{- if gt (len .CommonLabels) (len .GroupLabels) -}}\n{{\" \"}}(\n{{- with .CommonLabels.Remove .GroupLabels.Names }}\n {{- range $index, $label := .SortedPairs -}}\n {{ if $index }}, {{ end }}\n {{- $label.Name }}=\"{{ $label.Value -}}\"\n {{- end }}\n{{- end -}}\n)\n{{- end }}`}
|
||||
onChange={(event): void =>
|
||||
@@ -59,6 +62,7 @@ function Slack({ setSelectedConfig }: SlackProps): JSX.Element {
|
||||
}))
|
||||
}
|
||||
placeholder={t('placeholder_slack_description')}
|
||||
data-testid="description-textarea"
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
|
||||
@@ -17,6 +17,7 @@ function WebhookSettings({ setSelectedConfig }: WebhookProps): JSX.Element {
|
||||
api_url: event.target.value,
|
||||
}));
|
||||
}}
|
||||
data-testid="webhook-url-textbox"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
@@ -31,6 +32,7 @@ function WebhookSettings({ setSelectedConfig }: WebhookProps): JSX.Element {
|
||||
username: event.target.value,
|
||||
}));
|
||||
}}
|
||||
data-testid="webhook-username-textbox"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
@@ -46,6 +48,7 @@ function WebhookSettings({ setSelectedConfig }: WebhookProps): JSX.Element {
|
||||
password: event.target.value,
|
||||
}));
|
||||
}}
|
||||
data-testid="webhook-password-textbox"
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
|
||||
@@ -85,6 +85,7 @@ function FormAlertChannels({
|
||||
<Form initialValues={initialValue} layout="vertical" form={formInstance}>
|
||||
<Form.Item label={t('field_channel_name')} labelAlign="left" name="name">
|
||||
<Input
|
||||
data-testid="channel-name-textbox"
|
||||
disabled={editing}
|
||||
onChange={(event): void => {
|
||||
setSelectedConfig((state) => ({
|
||||
@@ -102,6 +103,7 @@ function FormAlertChannels({
|
||||
>
|
||||
<Switch
|
||||
defaultChecked={initialValue?.send_resolved}
|
||||
data-testid="field-send-resolved-checkbox"
|
||||
onChange={(value): void => {
|
||||
setSelectedConfig((state) => ({
|
||||
...state,
|
||||
@@ -112,24 +114,37 @@ function FormAlertChannels({
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t('field_channel_type')} labelAlign="left" name="type">
|
||||
<Select disabled={editing} onChange={onTypeChangeHandler} value={type}>
|
||||
<Select.Option value="slack" key="slack">
|
||||
<Select
|
||||
disabled={editing}
|
||||
onChange={onTypeChangeHandler}
|
||||
value={type}
|
||||
data-testid="channel-type-select"
|
||||
>
|
||||
<Select.Option value="slack" key="slack" data-testid="select-option">
|
||||
Slack
|
||||
</Select.Option>
|
||||
<Select.Option value="webhook" key="webhook">
|
||||
<Select.Option value="webhook" key="webhook" data-testid="select-option">
|
||||
Webhook
|
||||
</Select.Option>
|
||||
<Select.Option value="pagerduty" key="pagerduty">
|
||||
<Select.Option
|
||||
value="pagerduty"
|
||||
key="pagerduty"
|
||||
data-testid="select-option"
|
||||
>
|
||||
Pagerduty
|
||||
</Select.Option>
|
||||
<Select.Option value="opsgenie" key="opsgenie">
|
||||
<Select.Option
|
||||
value="opsgenie"
|
||||
key="opsgenie"
|
||||
data-testid="select-option"
|
||||
>
|
||||
Opsgenie
|
||||
</Select.Option>
|
||||
<Select.Option value="email" key="email">
|
||||
<Select.Option value="email" key="email" data-testid="select-option">
|
||||
Email
|
||||
</Select.Option>
|
||||
{!isOssFeature?.active && (
|
||||
<Select.Option value="msteams" key="msteams">
|
||||
<Select.Option value="msteams" key="msteams" data-testid="select-option">
|
||||
<div>
|
||||
Microsoft Teams {!isUserOnEEPlan && '(Supported in Paid Plans Only)'}{' '}
|
||||
</div>
|
||||
|
||||
@@ -125,10 +125,9 @@ function GridCardGraph({
|
||||
offset: 0,
|
||||
limit: updatedQuery.builder.queryData[0].limit || 0,
|
||||
},
|
||||
// we do not need select columns in case of logs
|
||||
selectColumns:
|
||||
initialDataSource === DataSource.LOGS
|
||||
? widget.selectedLogFields
|
||||
: widget.selectedTracesFields,
|
||||
initialDataSource === DataSource.TRACES && widget.selectedTracesFields,
|
||||
},
|
||||
fillGaps: widget.fillSpans,
|
||||
};
|
||||
|
||||
@@ -940,3 +940,50 @@
|
||||
border-color: var(--bg-vanilla-300) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.mt-8 {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.mt-12 {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.mt-24 {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.mb-24 {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.ingestion-setup-details-links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 24px;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
background: rgba(113, 144, 249, 0.1);
|
||||
color: var(--bg-robin-300, #95acfb);
|
||||
|
||||
.learn-more {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-decoration: underline;
|
||||
|
||||
color: var(--bg-robin-300, #95acfb);
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.ingestion-setup-details-links {
|
||||
background: rgba(113, 144, 249, 0.1);
|
||||
color: var(--bg-robin-500);
|
||||
|
||||
.learn-more {
|
||||
color: var(--bg-robin-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,11 +34,14 @@ import dayjs, { Dayjs } from 'dayjs';
|
||||
import { useGetAllIngestionsKeys } from 'hooks/IngestionKeys/useGetAllIngestionKeys';
|
||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { isNil } from 'lodash-es';
|
||||
import {
|
||||
ArrowUpRight,
|
||||
CalendarClock,
|
||||
Check,
|
||||
Copy,
|
||||
Infinity,
|
||||
Info,
|
||||
Minus,
|
||||
PenLine,
|
||||
Plus,
|
||||
@@ -603,243 +606,250 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
|
||||
<div className="limits-data">
|
||||
<div className="signals">
|
||||
{SIGNALS.map((signal) => (
|
||||
<div className="signal" key={signal}>
|
||||
<div className="header">
|
||||
<div className="signal-name">{signal}</div>
|
||||
<div className="actions">
|
||||
{hasLimits(signal) ? (
|
||||
<>
|
||||
{SIGNALS.map((signal) => {
|
||||
const hasValidDayLimit = !isNil(limits[signal]?.config?.day?.size);
|
||||
const hasValidSecondLimit = !isNil(
|
||||
limits[signal]?.config?.second?.size,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="signal" key={signal}>
|
||||
<div className="header">
|
||||
<div className="signal-name">{signal}</div>
|
||||
<div className="actions">
|
||||
{hasLimits(signal) ? (
|
||||
<>
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
icon={<PenLine size={14} />}
|
||||
disabled={!!(activeAPIKey?.id === APIKey.id && activeSignal)}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
enableEditLimitMode(APIKey, limits[signal]);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
icon={<Trash2 color={Color.BG_CHERRY_500} size={14} />}
|
||||
disabled={!!(activeAPIKey?.id === APIKey.id && activeSignal)}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
showDeleteLimitModal(APIKey, limits[signal]);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
icon={<PenLine size={14} />}
|
||||
className="periscope-btn"
|
||||
size="small"
|
||||
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, limits[signal]);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
icon={<Trash2 color={Color.BG_CHERRY_500} size={14} />}
|
||||
disabled={!!(activeAPIKey?.id === APIKey.id && activeSignal)}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
showDeleteLimitModal(APIKey, limits[signal]);
|
||||
enableEditLimitMode(APIKey, {
|
||||
id: signal,
|
||||
signal,
|
||||
config: {},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
className="periscope-btn"
|
||||
size="small"
|
||||
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();
|
||||
>
|
||||
Limits
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
enableEditLimitMode(APIKey, {
|
||||
id: signal,
|
||||
signal,
|
||||
config: {},
|
||||
});
|
||||
<div className="signal-limit-values">
|
||||
{activeAPIKey?.id === APIKey.id &&
|
||||
activeSignal?.signal === signal &&
|
||||
isEditAddLimitOpen ? (
|
||||
<Form
|
||||
name="edit-ingestion-key-limit-form"
|
||||
key="addEditLimitForm"
|
||||
form={addEditLimitForm}
|
||||
autoComplete="off"
|
||||
initialValues={{
|
||||
dailyLimit: bytesToGb(limits[signal]?.config?.day?.size),
|
||||
secondsLimit: bytesToGb(limits[signal]?.config?.second?.size),
|
||||
}}
|
||||
className="edit-ingestion-key-limit-form"
|
||||
>
|
||||
Limits
|
||||
</Button>
|
||||
<div className="signal-limit-edit-mode">
|
||||
<div className="daily-limit">
|
||||
<div className="heading">
|
||||
<div className="title"> Daily limit </div>
|
||||
<div className="subtitle">
|
||||
Add a limit for data ingested daily{' '}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="size">
|
||||
<Form.Item name="dailyLimit">
|
||||
<InputNumber
|
||||
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>
|
||||
</div>
|
||||
|
||||
<div className="second-limit">
|
||||
<div className="heading">
|
||||
<div className="title"> Per Second limit </div>
|
||||
<div className="subtitle">
|
||||
{' '}
|
||||
Add a limit for data ingested every second{' '}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="size">
|
||||
<Form.Item name="secondsLimit">
|
||||
<InputNumber
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{activeAPIKey?.id === APIKey.id &&
|
||||
activeSignal.signal === signal &&
|
||||
!isLoadingLimitForKey &&
|
||||
hasCreateLimitForIngestionKeyError &&
|
||||
createLimitForIngestionKeyError &&
|
||||
createLimitForIngestionKeyError?.error && (
|
||||
<div className="error">
|
||||
{createLimitForIngestionKeyError?.error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeAPIKey?.id === APIKey.id &&
|
||||
activeSignal.signal === signal &&
|
||||
!isLoadingLimitForKey &&
|
||||
hasUpdateLimitForIngestionKeyError &&
|
||||
updateLimitForIngestionKeyError && (
|
||||
<div className="error">
|
||||
{updateLimitForIngestionKeyError?.error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeAPIKey?.id === APIKey.id &&
|
||||
activeSignal.signal === signal &&
|
||||
isEditAddLimitOpen && (
|
||||
<div className="signal-limit-save-discard">
|
||||
<Button
|
||||
type="primary"
|
||||
className="periscope-btn primary"
|
||||
size="small"
|
||||
disabled={
|
||||
isLoadingLimitForKey || isLoadingUpdatedLimitForKey
|
||||
}
|
||||
loading={
|
||||
isLoadingLimitForKey || isLoadingUpdatedLimitForKey
|
||||
}
|
||||
onClick={(): void => {
|
||||
if (!hasLimits(signal)) {
|
||||
handleAddLimit(APIKey, signal);
|
||||
} else {
|
||||
handleUpdateLimit(APIKey, limits[signal]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
type="default"
|
||||
className="periscope-btn"
|
||||
size="small"
|
||||
disabled={
|
||||
isLoadingLimitForKey || isLoadingUpdatedLimitForKey
|
||||
}
|
||||
onClick={handleDiscardSaveLimit}
|
||||
>
|
||||
Discard
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Form>
|
||||
) : (
|
||||
<div className="signal-limit-view-mode">
|
||||
<div className="signal-limit-value">
|
||||
<div className="limit-type">
|
||||
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
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="signal-limit-values">
|
||||
{activeAPIKey?.id === APIKey.id &&
|
||||
activeSignal?.signal === signal &&
|
||||
isEditAddLimitOpen ? (
|
||||
<Form
|
||||
name="edit-ingestion-key-limit-form"
|
||||
key="addEditLimitForm"
|
||||
form={addEditLimitForm}
|
||||
autoComplete="off"
|
||||
initialValues={{
|
||||
dailyLimit: bytesToGb(limits[signal]?.config?.day?.size),
|
||||
secondsLimit: bytesToGb(limits[signal]?.config?.second?.size),
|
||||
}}
|
||||
className="edit-ingestion-key-limit-form"
|
||||
>
|
||||
<div className="signal-limit-edit-mode">
|
||||
<div className="daily-limit">
|
||||
<div className="heading">
|
||||
<div className="title"> Daily limit </div>
|
||||
<div className="subtitle">
|
||||
Add a limit for data ingested daily{' '}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="size">
|
||||
<Form.Item name="dailyLimit">
|
||||
<InputNumber
|
||||
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>
|
||||
</div>
|
||||
|
||||
<div className="second-limit">
|
||||
<div className="heading">
|
||||
<div className="title"> Per Second limit </div>
|
||||
<div className="subtitle">
|
||||
{' '}
|
||||
Add a limit for data ingested every second{' '}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="size">
|
||||
<Form.Item name="secondsLimit">
|
||||
<InputNumber
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{activeAPIKey?.id === APIKey.id &&
|
||||
activeSignal.signal === signal &&
|
||||
!isLoadingLimitForKey &&
|
||||
hasCreateLimitForIngestionKeyError &&
|
||||
createLimitForIngestionKeyError &&
|
||||
createLimitForIngestionKeyError?.error && (
|
||||
<div className="error">
|
||||
{createLimitForIngestionKeyError?.error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeAPIKey?.id === APIKey.id &&
|
||||
activeSignal.signal === signal &&
|
||||
!isLoadingLimitForKey &&
|
||||
hasUpdateLimitForIngestionKeyError &&
|
||||
updateLimitForIngestionKeyError && (
|
||||
<div className="error">
|
||||
{updateLimitForIngestionKeyError?.error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeAPIKey?.id === APIKey.id &&
|
||||
activeSignal.signal === signal &&
|
||||
isEditAddLimitOpen && (
|
||||
<div className="signal-limit-save-discard">
|
||||
<Button
|
||||
type="primary"
|
||||
className="periscope-btn primary"
|
||||
size="small"
|
||||
disabled={
|
||||
isLoadingLimitForKey || isLoadingUpdatedLimitForKey
|
||||
}
|
||||
loading={
|
||||
isLoadingLimitForKey || isLoadingUpdatedLimitForKey
|
||||
}
|
||||
onClick={(): void => {
|
||||
if (!hasLimits(signal)) {
|
||||
handleAddLimit(APIKey, signal);
|
||||
} else {
|
||||
handleUpdateLimit(APIKey, limits[signal]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
type="default"
|
||||
className="periscope-btn"
|
||||
size="small"
|
||||
disabled={
|
||||
isLoadingLimitForKey || isLoadingUpdatedLimitForKey
|
||||
}
|
||||
onClick={handleDiscardSaveLimit}
|
||||
>
|
||||
Discard
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Form>
|
||||
) : (
|
||||
<div className="signal-limit-view-mode">
|
||||
<div className="signal-limit-value">
|
||||
<div className="limit-type">
|
||||
Daily <Minus size={16} />{' '}
|
||||
</div>
|
||||
|
||||
<div className="limit-value">
|
||||
{limits[signal]?.config?.day?.size ? (
|
||||
<>
|
||||
{getYAxisFormattedValue(
|
||||
(limits[signal]?.metric?.day?.size || 0).toString(),
|
||||
'bytes',
|
||||
)}{' '}
|
||||
/{' '}
|
||||
{getYAxisFormattedValue(
|
||||
(limits[signal]?.config?.day?.size || 0).toString(),
|
||||
'bytes',
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Infinity size={16} /> NO LIMIT
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="signal-limit-value">
|
||||
<div className="limit-type">
|
||||
Seconds <Minus size={16} />
|
||||
</div>
|
||||
|
||||
<div className="limit-value">
|
||||
{limits[signal]?.config?.second?.size ? (
|
||||
<>
|
||||
{getYAxisFormattedValue(
|
||||
(limits[signal]?.metric?.second?.size || 0).toString(),
|
||||
'bytes',
|
||||
)}{' '}
|
||||
/{' '}
|
||||
{getYAxisFormattedValue(
|
||||
(limits[signal]?.config?.second?.size || 0).toString(),
|
||||
'bytes',
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Infinity size={16} /> NO LIMIT
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -875,10 +885,35 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
return (
|
||||
<div className="ingestion-key-container">
|
||||
<div className="ingestion-key-content">
|
||||
<div className="ingestion-setup-details-links">
|
||||
<Info size={14} />
|
||||
|
||||
<span>
|
||||
Find your ingestion URL and learn more about sending data to SigNoz{' '}
|
||||
<a
|
||||
href="https://signoz.io/docs/ingestion/signoz-cloud/overview/"
|
||||
target="_blank"
|
||||
className="learn-more"
|
||||
rel="noreferrer"
|
||||
>
|
||||
here <ArrowUpRight size={14} />
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<header>
|
||||
<Typography.Title className="title"> Ingestion Keys </Typography.Title>
|
||||
<Typography.Text className="subtitle">
|
||||
Create and manage ingestion keys for the SigNoz Cloud
|
||||
Create and manage ingestion keys for the SigNoz Cloud{' '}
|
||||
<a
|
||||
href="https://signoz.io/docs/ingestion/signoz-cloud/keys/"
|
||||
target="_blank"
|
||||
className="learn-more"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{' '}
|
||||
Learn more <ArrowUpRight size={14} />
|
||||
</a>
|
||||
</Typography.Text>
|
||||
</header>
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { render, screen } from 'tests/test-utils';
|
||||
|
||||
import MultiIngestionSettings from '../MultiIngestionSettings';
|
||||
|
||||
describe('MultiIngestionSettings Page', () => {
|
||||
beforeEach(() => {
|
||||
render(<MultiIngestionSettings />);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders MultiIngestionSettings page without crashing', () => {
|
||||
expect(
|
||||
screen.getByText(
|
||||
'Find your ingestion URL and learn more about sending data to SigNoz',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('Ingestion Keys')).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
screen.getByText('Create and manage ingestion keys for the SigNoz Cloud'),
|
||||
).toBeInTheDocument();
|
||||
|
||||
const overviewLink = screen.getByRole('link', { name: /here/i });
|
||||
expect(overviewLink).toHaveAttribute(
|
||||
'href',
|
||||
'https://signoz.io/docs/ingestion/signoz-cloud/overview/',
|
||||
);
|
||||
expect(overviewLink).toHaveAttribute('target', '_blank');
|
||||
expect(overviewLink).toHaveClass('learn-more');
|
||||
expect(overviewLink).toHaveAttribute('rel', 'noreferrer');
|
||||
|
||||
const aboutKeyslink = screen.getByRole('link', { name: /Learn more/i });
|
||||
expect(aboutKeyslink).toHaveAttribute(
|
||||
'href',
|
||||
'https://signoz.io/docs/ingestion/signoz-cloud/keys/',
|
||||
);
|
||||
expect(aboutKeyslink).toHaveAttribute('target', '_blank');
|
||||
expect(aboutKeyslink).toHaveClass('learn-more');
|
||||
expect(aboutKeyslink).toHaveAttribute('rel', 'noreferrer');
|
||||
});
|
||||
});
|
||||
@@ -43,6 +43,10 @@
|
||||
background: var(--bg-ink-400);
|
||||
cursor: pointer;
|
||||
|
||||
.dashboard-title {
|
||||
color: var(--bg-vanilla-100);
|
||||
}
|
||||
|
||||
.title-with-action {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -1048,6 +1052,10 @@
|
||||
border: 1px solid var(--bg-vanilla-200);
|
||||
background: var(--bg-vanilla-100);
|
||||
|
||||
.dashboard-title {
|
||||
color: var(--bg-slate-300);
|
||||
}
|
||||
|
||||
.title-with-action {
|
||||
.dashboard-title {
|
||||
.ant-typography {
|
||||
|
||||
@@ -66,7 +66,7 @@ import {
|
||||
} from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { generatePath } from 'react-router-dom';
|
||||
import { generatePath, Link } from 'react-router-dom';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
@@ -455,7 +455,9 @@ function DashboardsList(): JSX.Element {
|
||||
alt="dashboard-image"
|
||||
/>
|
||||
<Typography.Text data-testid={`dashboard-title-${index}`}>
|
||||
{dashboard.name}
|
||||
<Link to={getLink()} className="dashboard-title">
|
||||
{dashboard.name}
|
||||
</Link>
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
@@ -653,8 +655,9 @@ function DashboardsList(): JSX.Element {
|
||||
}}
|
||||
eventName="Dashboard: Facing Issues in dashboard"
|
||||
message={dashboardListMessage}
|
||||
buttonText="Facing issues with dashboards?"
|
||||
buttonText="Need help with dashboards?"
|
||||
onHoverText="Click here to get help with dashboards"
|
||||
intercomMessageDisabled
|
||||
/>
|
||||
</Flex>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import {
|
||||
@@ -46,6 +47,8 @@ export const useContextLogData = ({
|
||||
} => {
|
||||
const [logs, setLogs] = useState<ILog[]>([]);
|
||||
|
||||
const [lastLog, setLastLog] = useState<ILog>(log);
|
||||
|
||||
const orderByTimestamp = useMemo(() => getOrderByTimestamp(order), [order]);
|
||||
|
||||
const logsMorePageSize = useMemo(() => (page - 1) * LOGS_MORE_PAGE_SIZE, [
|
||||
@@ -71,11 +74,11 @@ export const useContextLogData = ({
|
||||
getRequestData({
|
||||
stagedQueryData: currentStagedQueryData,
|
||||
query,
|
||||
log,
|
||||
log: lastLog,
|
||||
orderByTimestamp,
|
||||
page,
|
||||
}),
|
||||
[currentStagedQueryData, page, log, query, orderByTimestamp],
|
||||
[currentStagedQueryData, query, lastLog, orderByTimestamp, page],
|
||||
);
|
||||
|
||||
const [requestData, setRequestData] = useState<Query | null>(
|
||||
@@ -95,8 +98,10 @@ export const useContextLogData = ({
|
||||
if (order === ORDERBY_FILTERS.ASC) {
|
||||
const reversedCurrentLogs = currentLogs.reverse();
|
||||
setLogs([...reversedCurrentLogs]);
|
||||
setLastLog(reversedCurrentLogs[0]);
|
||||
} else {
|
||||
setLogs([...currentLogs]);
|
||||
setLastLog(currentLogs[currentLogs.length - 1]);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -118,7 +123,7 @@ export const useContextLogData = ({
|
||||
const newRequestData = getRequestData({
|
||||
stagedQueryData: currentStagedQueryData,
|
||||
query,
|
||||
log,
|
||||
log: lastLog,
|
||||
orderByTimestamp,
|
||||
page: page + 1,
|
||||
pageSize: LOGS_MORE_PAGE_SIZE,
|
||||
@@ -131,6 +136,7 @@ export const useContextLogData = ({
|
||||
query,
|
||||
page,
|
||||
order,
|
||||
lastLog,
|
||||
currentStagedQueryData,
|
||||
isDisabledFetch,
|
||||
orderByTimestamp,
|
||||
@@ -142,7 +148,7 @@ export const useContextLogData = ({
|
||||
const newRequestData = getRequestData({
|
||||
stagedQueryData: currentStagedQueryData,
|
||||
query,
|
||||
log,
|
||||
log: lastLog,
|
||||
orderByTimestamp,
|
||||
page: 1,
|
||||
});
|
||||
|
||||
@@ -188,6 +188,7 @@ export const aggregateAttributesResourcesToString = (logData: ILog): string => {
|
||||
attributes: {},
|
||||
resources: {},
|
||||
severity_text: logData.severity_text,
|
||||
severity_number: logData.severity_number,
|
||||
};
|
||||
|
||||
Object.keys(logData).forEach((key) => {
|
||||
|
||||
112
frontend/src/container/Login/__tests__/Login.test.tsx
Normal file
112
frontend/src/container/Login/__tests__/Login.test.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import Login from 'container/Login';
|
||||
import { act, fireEvent, render, screen, waitFor } from 'tests/test-utils';
|
||||
|
||||
const errorNotification = jest.fn();
|
||||
jest.mock('hooks/useNotifications', () => ({
|
||||
__esModule: true,
|
||||
useNotifications: jest.fn(() => ({
|
||||
notifications: {
|
||||
error: errorNotification,
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('Login Flow', () => {
|
||||
test('Login form is rendered correctly', async () => {
|
||||
render(<Login ssoerror="" jwt="" refreshjwt="" userId="" withPassword="" />);
|
||||
|
||||
const headingElement = screen.getByRole('heading', {
|
||||
name: 'login_page_title',
|
||||
});
|
||||
expect(headingElement).toBeInTheDocument();
|
||||
|
||||
const textboxElement = screen.getByRole('textbox');
|
||||
expect(textboxElement).toBeInTheDocument();
|
||||
|
||||
const buttonElement = screen.getByRole('button', {
|
||||
name: 'button_initiate_login',
|
||||
});
|
||||
expect(buttonElement).toBeInTheDocument();
|
||||
|
||||
const noAccountPromptElement = screen.getByText('prompt_no_account');
|
||||
expect(noAccountPromptElement).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test(`Display "invalid_email" if email is not provided`, async () => {
|
||||
render(<Login ssoerror="" jwt="" refreshjwt="" userId="" withPassword="" />);
|
||||
|
||||
const buttonElement = screen.getByText('button_initiate_login');
|
||||
fireEvent.click(buttonElement);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(errorNotification).toHaveBeenCalledWith({
|
||||
message: 'invalid_email',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('Display invalid_config if invalid email is provided and next clicked', async () => {
|
||||
render(<Login ssoerror="" jwt="" refreshjwt="" userId="" withPassword="" />);
|
||||
|
||||
const textboxElement = screen.getByRole('textbox');
|
||||
fireEvent.change(textboxElement, {
|
||||
target: { value: 'failEmail@signoz.io' },
|
||||
});
|
||||
|
||||
const buttonElement = screen.getByRole('button', {
|
||||
name: 'button_initiate_login',
|
||||
});
|
||||
fireEvent.click(buttonElement);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(errorNotification).toHaveBeenCalledWith({
|
||||
message: 'invalid_config',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('providing shaheer@signoz.io as email and pressing next, should make the login_with_sso button visible', async () => {
|
||||
render(<Login ssoerror="" jwt="" refreshjwt="" userId="" withPassword="" />);
|
||||
act(() => {
|
||||
fireEvent.change(screen.getByTestId('email'), {
|
||||
target: { value: 'shaheer@signoz.io' },
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByTestId('initiate_login'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('login_with_sso')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('Display email, password, forgot password if password=Y', () => {
|
||||
render(<Login ssoerror="" jwt="" refreshjwt="" userId="" withPassword="Y" />);
|
||||
|
||||
const emailTextBox = screen.getByTestId('email');
|
||||
expect(emailTextBox).toBeInTheDocument();
|
||||
|
||||
const passwordTextBox = screen.getByTestId('password');
|
||||
expect(passwordTextBox).toBeInTheDocument();
|
||||
|
||||
const forgotPasswordLink = screen.getByText('forgot_password');
|
||||
expect(forgotPasswordLink).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Display tooltip with "prompt_forgot_password" if forgot password is clicked while password=Y', async () => {
|
||||
render(<Login ssoerror="" jwt="" refreshjwt="" userId="" withPassword="Y" />);
|
||||
const forgotPasswordLink = screen.getByText('forgot_password');
|
||||
|
||||
act(() => {
|
||||
fireEvent.mouseOver(forgotPasswordLink);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const forgotPasswordTooltip = screen.getByRole('tooltip', {
|
||||
name: 'prompt_forgot_password',
|
||||
});
|
||||
expect(forgotPasswordLink).toBeInTheDocument();
|
||||
expect(forgotPasswordTooltip).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -163,8 +163,15 @@ function Login({
|
||||
response.payload.accessJwt,
|
||||
response.payload.refreshJwt,
|
||||
);
|
||||
if (history?.location?.state) {
|
||||
const historyState = history?.location?.state as any;
|
||||
|
||||
history.push(ROUTES.APPLICATION);
|
||||
if (historyState?.from) {
|
||||
history.push(historyState?.from);
|
||||
} else {
|
||||
history.push(ROUTES.APPLICATION);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
notifications.error({
|
||||
message: response.error || t('unexpected_error'),
|
||||
@@ -213,6 +220,7 @@ function Login({
|
||||
<Input
|
||||
type="email"
|
||||
id="loginEmail"
|
||||
data-testid="email"
|
||||
required
|
||||
placeholder={t('placeholder_email')}
|
||||
autoFocus
|
||||
@@ -224,7 +232,12 @@ function Login({
|
||||
<ParentContainer>
|
||||
<Label htmlFor="Password">{t('label_password')}</Label>
|
||||
<FormContainer.Item name="password">
|
||||
<Input.Password required id="currentPassword" disabled={isLoading} />
|
||||
<Input.Password
|
||||
required
|
||||
id="currentPassword"
|
||||
data-testid="password"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</FormContainer.Item>
|
||||
<Tooltip title={t('prompt_forgot_password')}>
|
||||
<Typography.Link>{t('forgot_password')}</Typography.Link>
|
||||
@@ -243,6 +256,7 @@ function Login({
|
||||
loading={precheckInProcess}
|
||||
type="primary"
|
||||
onClick={onNextHandler}
|
||||
data-testid="initiate_login"
|
||||
>
|
||||
{t('button_initiate_login')}
|
||||
</Button>
|
||||
|
||||
@@ -20,12 +20,14 @@ import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||
import { defaultTo } from 'lodash-es';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import { UpdateTimeInterval } from 'store/actions';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { GraphTitle, SERVICE_CHART_ID } from '../constant';
|
||||
@@ -52,6 +54,11 @@ import {
|
||||
function Application(): JSX.Element {
|
||||
const { servicename: encodedServiceName } = useParams<IServiceName>();
|
||||
const servicename = decodeURIComponent(encodedServiceName);
|
||||
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const [selectedTimeStamp, setSelectedTimeStamp] = useState<number>(0);
|
||||
const { search, pathname } = useLocation();
|
||||
const { queries } = useResourceAttribute();
|
||||
@@ -105,8 +112,13 @@ function Application(): JSX.Element {
|
||||
isLoading: topLevelOperationsIsLoading,
|
||||
isError: topLevelOperationsIsError,
|
||||
} = useQuery<ServiceDataProps>({
|
||||
queryKey: [servicename],
|
||||
queryFn: getTopLevelOperations,
|
||||
queryKey: [servicename, minTime, maxTime],
|
||||
queryFn: (): Promise<ServiceDataProps> =>
|
||||
getTopLevelOperations({
|
||||
service: servicename || '',
|
||||
start: minTime,
|
||||
end: maxTime,
|
||||
}),
|
||||
});
|
||||
|
||||
const selectedTraceTags: string = JSON.stringify(
|
||||
|
||||
@@ -164,6 +164,12 @@ function TopOperationsTable({
|
||||
|
||||
const downloadableData = convertedTracesToDownloadData(data);
|
||||
|
||||
const paginationConfig = {
|
||||
pageSize: 10,
|
||||
showSizeChanger: false,
|
||||
hideOnSinglePage: true,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="top-operation">
|
||||
<div className="top-operation--download">
|
||||
@@ -181,6 +187,7 @@ function TopOperationsTable({
|
||||
tableLayout="fixed"
|
||||
dataSource={data}
|
||||
rowKey="name"
|
||||
pagination={paginationConfig}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -90,18 +90,23 @@ function PasswordContainer(): JSX.Element {
|
||||
return (
|
||||
<Card>
|
||||
<Space direction="vertical" size="small">
|
||||
<Typography.Title level={4} style={{ marginTop: 0 }}>
|
||||
<Typography.Title
|
||||
level={4}
|
||||
style={{ marginTop: 0 }}
|
||||
data-testid="change-password-header"
|
||||
>
|
||||
{t('change_password', {
|
||||
ns: 'settings',
|
||||
})}
|
||||
</Typography.Title>
|
||||
<Space direction="vertical">
|
||||
<Typography>
|
||||
<Typography data-testid="current-password-label">
|
||||
{t('current_password', {
|
||||
ns: 'settings',
|
||||
})}
|
||||
</Typography>
|
||||
<Password
|
||||
data-testid="current-password-textbox"
|
||||
disabled={isLoading}
|
||||
placeholder={defaultPlaceHolder}
|
||||
onChange={(event): void => {
|
||||
@@ -111,12 +116,13 @@ function PasswordContainer(): JSX.Element {
|
||||
/>
|
||||
</Space>
|
||||
<Space direction="vertical">
|
||||
<Typography>
|
||||
<Typography data-testid="new-password-label">
|
||||
{t('new_password', {
|
||||
ns: 'settings',
|
||||
})}
|
||||
</Typography>
|
||||
<Password
|
||||
data-testid="new-password-textbox"
|
||||
disabled={isLoading}
|
||||
placeholder={defaultPlaceHolder}
|
||||
onChange={(event): void => {
|
||||
@@ -129,6 +135,7 @@ function PasswordContainer(): JSX.Element {
|
||||
<Space>
|
||||
{isPasswordPolicyError && (
|
||||
<Typography.Paragraph
|
||||
data-testid="validation-message"
|
||||
style={{
|
||||
color: '#D89614',
|
||||
marginTop: '0.50rem',
|
||||
@@ -143,8 +150,13 @@ function PasswordContainer(): JSX.Element {
|
||||
loading={isLoading}
|
||||
onClick={onChangePasswordClickHandler}
|
||||
type="primary"
|
||||
data-testid="update-password-button"
|
||||
>
|
||||
<Save size={12} style={{ marginRight: '8px' }} />{' '}
|
||||
<Save
|
||||
size={12}
|
||||
style={{ marginRight: '8px' }}
|
||||
data-testid="update-password-icon"
|
||||
/>{' '}
|
||||
{t('change_password', {
|
||||
ns: 'settings',
|
||||
})}
|
||||
|
||||
@@ -86,8 +86,11 @@ function UserInfo(): JSX.Element {
|
||||
|
||||
<Flex gap={16}>
|
||||
<Space>
|
||||
<Typography className="userInfo-label">Name</Typography>
|
||||
<Typography className="userInfo-label" data-testid="name-label">
|
||||
Name
|
||||
</Typography>
|
||||
<NameInput
|
||||
data-testid="name-textbox"
|
||||
placeholder="Your Name"
|
||||
onChange={(event): void => {
|
||||
setChangedName(event.target.value);
|
||||
@@ -102,6 +105,7 @@ function UserInfo(): JSX.Element {
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
onClick={onClickUpdateHandler}
|
||||
data-testid="update-name-button"
|
||||
type="primary"
|
||||
>
|
||||
<PencilIcon size={12} /> Update
|
||||
@@ -109,13 +113,29 @@ function UserInfo(): JSX.Element {
|
||||
</Flex>
|
||||
|
||||
<Space>
|
||||
<Typography className="userInfo-label"> Email </Typography>
|
||||
<Input className="userInfo-value" value={user.email} disabled />
|
||||
<Typography className="userInfo-label" data-testid="email-label">
|
||||
{' '}
|
||||
Email{' '}
|
||||
</Typography>
|
||||
<Input
|
||||
className="userInfo-value"
|
||||
data-testid="email-textbox"
|
||||
value={user.email}
|
||||
disabled
|
||||
/>
|
||||
</Space>
|
||||
|
||||
<Space>
|
||||
<Typography className="userInfo-label"> Role </Typography>
|
||||
<Input className="userInfo-value" value={role || ''} disabled />
|
||||
<Typography className="userInfo-label" data-testid="role-label">
|
||||
{' '}
|
||||
Role{' '}
|
||||
</Typography>
|
||||
<Input
|
||||
className="userInfo-value"
|
||||
value={role || ''}
|
||||
disabled
|
||||
data-testid="role-textbox"
|
||||
/>
|
||||
</Space>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
219
frontend/src/container/MySettings/__tests__/MySettings.test.tsx
Normal file
219
frontend/src/container/MySettings/__tests__/MySettings.test.tsx
Normal file
@@ -0,0 +1,219 @@
|
||||
import MySettingsContainer from 'container/MySettings';
|
||||
import { act, fireEvent, render, screen, waitFor } from 'tests/test-utils';
|
||||
|
||||
const toggleThemeFunction = jest.fn();
|
||||
|
||||
jest.mock('hooks/useDarkMode', () => ({
|
||||
__esModule: true,
|
||||
useIsDarkMode: jest.fn(() => ({
|
||||
toggleTheme: toggleThemeFunction,
|
||||
})),
|
||||
default: jest.fn(() => ({
|
||||
toggleTheme: toggleThemeFunction,
|
||||
})),
|
||||
}));
|
||||
|
||||
const errorNotification = jest.fn();
|
||||
const successNotification = jest.fn();
|
||||
jest.mock('hooks/useNotifications', () => ({
|
||||
__esModule: true,
|
||||
useNotifications: jest.fn(() => ({
|
||||
notifications: {
|
||||
error: errorNotification,
|
||||
success: successNotification,
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
||||
enum ThemeOptions {
|
||||
Dark = 'Dark',
|
||||
Light = 'Light',
|
||||
}
|
||||
|
||||
describe('MySettings Flows', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
render(<MySettingsContainer />);
|
||||
});
|
||||
|
||||
describe('Dark/Light Theme Switch', () => {
|
||||
it('Should display Dark and Light theme buttons properly', async () => {
|
||||
expect(screen.getByText('Dark')).toBeInTheDocument();
|
||||
|
||||
const darkThemeIcon = screen.getByTestId('dark-theme-icon');
|
||||
expect(darkThemeIcon).toBeInTheDocument();
|
||||
expect(darkThemeIcon.tagName).toBe('svg');
|
||||
|
||||
expect(screen.getByText('Light')).toBeInTheDocument();
|
||||
const lightThemeIcon = screen.getByTestId('light-theme-icon');
|
||||
expect(lightThemeIcon).toBeInTheDocument();
|
||||
expect(lightThemeIcon.tagName).toBe('svg');
|
||||
});
|
||||
|
||||
it('Should activate Dark and Light buttons on click', async () => {
|
||||
const initialSelectedOption = screen.getByRole('radio', {
|
||||
name: ThemeOptions.Dark,
|
||||
});
|
||||
expect(initialSelectedOption).toBeChecked();
|
||||
|
||||
const newThemeOption = screen.getByRole('radio', {
|
||||
name: ThemeOptions.Light,
|
||||
});
|
||||
fireEvent.click(newThemeOption);
|
||||
|
||||
expect(newThemeOption).toBeChecked();
|
||||
});
|
||||
|
||||
it('Should switch the them on clicking Light theme', async () => {
|
||||
const lightThemeOption = screen.getByRole('radio', {
|
||||
name: /light/i,
|
||||
});
|
||||
fireEvent.click(lightThemeOption);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toggleThemeFunction).toBeCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('User Details Form', () => {
|
||||
it('Should properly display the User Details Form', () => {
|
||||
const userDetailsHeader = screen.getByRole('heading', {
|
||||
name: /user details/i,
|
||||
});
|
||||
const nameLabel = screen.getByTestId('name-label');
|
||||
const nameTextbox = screen.getByTestId('name-textbox');
|
||||
const updateNameButton = screen.getByTestId('update-name-button');
|
||||
const emailLabel = screen.getByTestId('email-label');
|
||||
const emailTextbox = screen.getByTestId('email-textbox');
|
||||
const roleLabel = screen.getByTestId('role-label');
|
||||
const roleTextbox = screen.getByTestId('role-textbox');
|
||||
|
||||
expect(userDetailsHeader).toBeInTheDocument();
|
||||
expect(nameLabel).toBeInTheDocument();
|
||||
expect(nameTextbox).toBeInTheDocument();
|
||||
expect(updateNameButton).toBeInTheDocument();
|
||||
expect(emailLabel).toBeInTheDocument();
|
||||
expect(emailTextbox).toBeInTheDocument();
|
||||
expect(roleLabel).toBeInTheDocument();
|
||||
expect(roleTextbox).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should update the name on clicking Update button', async () => {
|
||||
const nameTextbox = screen.getByTestId('name-textbox');
|
||||
const updateNameButton = screen.getByTestId('update-name-button');
|
||||
|
||||
act(() => {
|
||||
fireEvent.change(nameTextbox, { target: { value: 'New Name' } });
|
||||
});
|
||||
|
||||
fireEvent.click(updateNameButton);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(successNotification).toHaveBeenCalledWith({
|
||||
message: 'success',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Reset password', () => {
|
||||
let currentPasswordTextbox: Node | Window;
|
||||
let newPasswordTextbox: Node | Window;
|
||||
let submitButtonElement: HTMLElement;
|
||||
|
||||
beforeEach(() => {
|
||||
currentPasswordTextbox = screen.getByTestId('current-password-textbox');
|
||||
newPasswordTextbox = screen.getByTestId('new-password-textbox');
|
||||
submitButtonElement = screen.getByTestId('update-password-button');
|
||||
});
|
||||
|
||||
it('Should properly display the Password Reset Form', () => {
|
||||
const passwordResetHeader = screen.getByTestId('change-password-header');
|
||||
expect(passwordResetHeader).toBeInTheDocument();
|
||||
|
||||
const currentPasswordLabel = screen.getByTestId('current-password-label');
|
||||
expect(currentPasswordLabel).toBeInTheDocument();
|
||||
|
||||
expect(currentPasswordTextbox).toBeInTheDocument();
|
||||
|
||||
const newPasswordLabel = screen.getByTestId('new-password-label');
|
||||
expect(newPasswordLabel).toBeInTheDocument();
|
||||
|
||||
expect(newPasswordTextbox).toBeInTheDocument();
|
||||
expect(submitButtonElement).toBeInTheDocument();
|
||||
|
||||
const savePasswordIcon = screen.getByTestId('update-password-icon');
|
||||
expect(savePasswordIcon).toBeInTheDocument();
|
||||
expect(savePasswordIcon.tagName).toBe('svg');
|
||||
});
|
||||
|
||||
it('Should display validation error if password is less than 8 characters', async () => {
|
||||
const currentPasswordTextbox = screen.getByTestId(
|
||||
'current-password-textbox',
|
||||
);
|
||||
act(() => {
|
||||
fireEvent.change(currentPasswordTextbox, { target: { value: '123' } });
|
||||
});
|
||||
const validationMessage = await screen.findByTestId('validation-message');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(validationMessage).toHaveTextContent(
|
||||
'Password must a have minimum of 8 characters',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("Should display 'inavlid credentials' error if different current and new passwords are provided", async () => {
|
||||
act(() => {
|
||||
fireEvent.change(currentPasswordTextbox, {
|
||||
target: { value: '123456879' },
|
||||
});
|
||||
|
||||
fireEvent.change(newPasswordTextbox, { target: { value: '123456789' } });
|
||||
});
|
||||
|
||||
fireEvent.click(submitButtonElement);
|
||||
|
||||
await waitFor(() => expect(errorNotification).toHaveBeenCalled());
|
||||
});
|
||||
|
||||
it('Should check if the "Change Password" button is disabled in case current / new password is less than 8 characters', () => {
|
||||
act(() => {
|
||||
fireEvent.change(currentPasswordTextbox, {
|
||||
target: { value: '123' },
|
||||
});
|
||||
fireEvent.change(newPasswordTextbox, { target: { value: '123' } });
|
||||
});
|
||||
|
||||
expect(submitButtonElement).toBeDisabled();
|
||||
});
|
||||
|
||||
test("Should check if 'Change Password' button is enabled when password is at least 8 characters ", async () => {
|
||||
expect(submitButtonElement).toBeDisabled();
|
||||
|
||||
act(() => {
|
||||
fireEvent.change(currentPasswordTextbox, {
|
||||
target: { value: '123456789' },
|
||||
});
|
||||
fireEvent.change(newPasswordTextbox, { target: { value: '1234567890' } });
|
||||
});
|
||||
|
||||
expect(submitButtonElement).toBeEnabled();
|
||||
});
|
||||
|
||||
test("Should check if 'Change Password' button is disabled when current and new passwords are the same ", async () => {
|
||||
expect(submitButtonElement).toBeDisabled();
|
||||
|
||||
act(() => {
|
||||
fireEvent.change(currentPasswordTextbox, {
|
||||
target: { value: '123456789' },
|
||||
});
|
||||
fireEvent.change(newPasswordTextbox, { target: { value: '123456789' } });
|
||||
});
|
||||
|
||||
expect(submitButtonElement).toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -17,7 +17,7 @@ function MySettings(): JSX.Element {
|
||||
{
|
||||
label: (
|
||||
<div className="theme-option">
|
||||
<Moon size={12} /> Dark{' '}
|
||||
<Moon data-testid="dark-theme-icon" size={12} /> Dark{' '}
|
||||
</div>
|
||||
),
|
||||
value: 'dark',
|
||||
@@ -25,7 +25,7 @@ function MySettings(): JSX.Element {
|
||||
{
|
||||
label: (
|
||||
<div className="theme-option">
|
||||
<Sun size={12} /> Light{' '}
|
||||
<Sun size={12} data-testid="light-theme-icon" /> Light{' '}
|
||||
</div>
|
||||
),
|
||||
value: 'light',
|
||||
@@ -63,6 +63,7 @@ function MySettings(): JSX.Element {
|
||||
value={theme}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
data-testid="theme-selector"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -74,7 +75,12 @@ function MySettings(): JSX.Element {
|
||||
<Password />
|
||||
</div>
|
||||
|
||||
<Button className="flexBtn" onClick={(): void => Logout()} type="primary">
|
||||
<Button
|
||||
className="flexBtn"
|
||||
onClick={(): void => Logout()}
|
||||
type="primary"
|
||||
data-testid="logout-button"
|
||||
>
|
||||
<LogOut size={12} /> Logout
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { getNonIntegrationDashboardById } from 'mocks-server/__mockdata__/dashboards';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
|
||||
import { MemoryRouter, useLocation } from 'react-router-dom';
|
||||
import { fireEvent, render, screen, waitFor } from 'tests/test-utils';
|
||||
|
||||
import DashboardDescription from '..';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: jest.fn(),
|
||||
useRouteMatch: jest.fn().mockReturnValue({
|
||||
params: {
|
||||
dashboardId: 4,
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock(
|
||||
'container/TopNav/DateTimeSelectionV2/index.tsx',
|
||||
() =>
|
||||
function MockDateTimeSelection(): JSX.Element {
|
||||
return <div>MockDateTimeSelection</div>;
|
||||
},
|
||||
);
|
||||
|
||||
describe('Dashboard landing page actions header tests', () => {
|
||||
it('unlock dashboard should be disabled for integrations created dashboards', async () => {
|
||||
const mockLocation = {
|
||||
pathname: `${process.env.FRONTEND_API_ENDPOINT}/dashboard/4`,
|
||||
search: '',
|
||||
};
|
||||
(useLocation as jest.Mock).mockReturnValue(mockLocation);
|
||||
const { getByTestId } = render(
|
||||
<MemoryRouter initialEntries={['/dashboard/4']}>
|
||||
<DashboardProvider>
|
||||
<DashboardDescription
|
||||
handle={{
|
||||
active: false,
|
||||
enter: (): Promise<void> => Promise.resolve(),
|
||||
exit: (): Promise<void> => Promise.resolve(),
|
||||
node: { current: null },
|
||||
}}
|
||||
/>
|
||||
</DashboardProvider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(getByTestId('dashboard-title')).toHaveTextContent('thor'),
|
||||
);
|
||||
|
||||
const dashboardSettingsTrigger = getByTestId('options');
|
||||
|
||||
await fireEvent.click(dashboardSettingsTrigger);
|
||||
|
||||
const lockUnlockButton = screen.getByTestId('lock-unlock-dashboard');
|
||||
|
||||
await waitFor(() => expect(lockUnlockButton).toBeDisabled());
|
||||
});
|
||||
it('unlock dashboard should not be disabled for non integration created dashboards', async () => {
|
||||
const mockLocation = {
|
||||
pathname: `${process.env.FRONTEND_API_ENDPOINT}/dashboard/4`,
|
||||
search: '',
|
||||
};
|
||||
(useLocation as jest.Mock).mockReturnValue(mockLocation);
|
||||
server.use(
|
||||
rest.get('http://localhost/api/v1/dashboards/4', (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(getNonIntegrationDashboardById)),
|
||||
),
|
||||
);
|
||||
const { getByTestId } = render(
|
||||
<MemoryRouter initialEntries={['/dashboard/4']}>
|
||||
<DashboardProvider>
|
||||
<DashboardDescription
|
||||
handle={{
|
||||
active: false,
|
||||
enter: (): Promise<void> => Promise.resolve(),
|
||||
exit: (): Promise<void> => Promise.resolve(),
|
||||
node: { current: null },
|
||||
}}
|
||||
/>
|
||||
</DashboardProvider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(getByTestId('dashboard-title')).toHaveTextContent('thor'),
|
||||
);
|
||||
|
||||
const dashboardSettingsTrigger = getByTestId('options');
|
||||
|
||||
await fireEvent.click(dashboardSettingsTrigger);
|
||||
|
||||
const lockUnlockButton = screen.getByTestId('lock-unlock-dashboard');
|
||||
|
||||
await waitFor(() => expect(lockUnlockButton).not.toBeDisabled());
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,16 @@
|
||||
import './Description.styles.scss';
|
||||
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { Button, Card, Input, Modal, Popover, Tag, Typography } from 'antd';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Input,
|
||||
Modal,
|
||||
Popover,
|
||||
Tag,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
|
||||
import { dashboardHelpMessage } from 'components/facingIssueBtn/util';
|
||||
@@ -300,17 +309,6 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
{title}
|
||||
</Button>
|
||||
</section>
|
||||
<FacingIssueBtn
|
||||
attributes={{
|
||||
uuid: selectedDashboard?.uuid,
|
||||
title: updatedTitle,
|
||||
screen: 'Dashboard Details',
|
||||
}}
|
||||
eventName="Dashboard: Facing Issues in dashboard"
|
||||
message={dashboardHelpMessage(selectedDashboard?.data, selectedDashboard)}
|
||||
buttonText="Facing issues with dashboards?"
|
||||
onHoverText="Click here to get help with dashboard details"
|
||||
/>
|
||||
</div>
|
||||
<section className="dashbord-details">
|
||||
<div className="left-section">
|
||||
@@ -319,10 +317,24 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
alt="dashboard-img"
|
||||
style={{ width: '16px', height: '16px' }}
|
||||
/>
|
||||
<Typography.Text className="dashboard-title">{title}</Typography.Text>
|
||||
<Typography.Text className="dashboard-title" data-testid="dashboard-title">
|
||||
{title}
|
||||
</Typography.Text>
|
||||
{isDashboardLocked && <LockKeyhole size={14} />}
|
||||
</div>
|
||||
<div className="right-section">
|
||||
<FacingIssueBtn
|
||||
attributes={{
|
||||
uuid: selectedDashboard?.uuid,
|
||||
title: updatedTitle,
|
||||
screen: 'Dashboard Details',
|
||||
}}
|
||||
eventName="Dashboard: Facing Issues in dashboard"
|
||||
message={dashboardHelpMessage(selectedDashboard?.data, selectedDashboard)}
|
||||
buttonText="Need help with this dashboard?"
|
||||
onHoverText="Click here to get help with dashboard"
|
||||
intercomMessageDisabled
|
||||
/>
|
||||
<DateTimeSelectionV2 showAutoRefresh hideShareModal />
|
||||
<Popover
|
||||
open={isDashboardSettingsOpen}
|
||||
@@ -333,13 +345,22 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
<div className="menu-content">
|
||||
<section className="section-1">
|
||||
{(isAuthor || role === USER_ROLES.ADMIN) && (
|
||||
<Button
|
||||
type="text"
|
||||
icon={<LockKeyhole size={14} />}
|
||||
onClick={handleLockDashboardToggle}
|
||||
<Tooltip
|
||||
title={
|
||||
selectedDashboard?.created_by === 'integration' &&
|
||||
'Dashboards created by integrations cannot be unlocked'
|
||||
}
|
||||
>
|
||||
{isDashboardLocked ? 'Unlock Dashboard' : 'Lock Dashboard'}
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<LockKeyhole size={14} />}
|
||||
disabled={selectedDashboard?.created_by === 'integration'}
|
||||
onClick={handleLockDashboardToggle}
|
||||
data-testid="lock-unlock-dashboard"
|
||||
>
|
||||
{isDashboardLocked ? 'Unlock Dashboard' : 'Lock Dashboard'}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{!isDashboardLocked && editDashboard && (
|
||||
|
||||
@@ -309,6 +309,7 @@ function ExplorerColumnsRenderer({
|
||||
>
|
||||
<Button
|
||||
className="action-btn"
|
||||
data-testid="add-columns-button"
|
||||
icon={
|
||||
<PlusCircle
|
||||
size={16}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Tabs, Tooltip, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import PromQLIcon from 'assets/Dashboard/PromQl';
|
||||
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
|
||||
import TextToolTip from 'components/TextToolTip';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { QBShortcuts } from 'constants/shortcuts/QBShortcuts';
|
||||
@@ -236,6 +237,21 @@ function QuerySection({
|
||||
onChange={handleQueryCategoryChange}
|
||||
tabBarExtraContent={
|
||||
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
|
||||
<FacingIssueBtn
|
||||
attributes={{
|
||||
uuid: selectedDashboard?.uuid,
|
||||
title: selectedDashboard?.data.title,
|
||||
screen: 'Dashboard widget',
|
||||
panelType: selectedGraph,
|
||||
widgetId: query.id,
|
||||
queryType: currentQuery.queryType,
|
||||
}}
|
||||
eventName="Dashboard: Facing Issues in dashboard"
|
||||
buttonText="Need help with this chart?"
|
||||
// message={chartHelpMessage(selectedDashboard, graphType)}
|
||||
onHoverText="Click here to get help with this dashboard widget"
|
||||
intercomMessageDisabled
|
||||
/>
|
||||
<TextToolTip
|
||||
text="This will temporarily save the current query and graph state. This will persist across tab change"
|
||||
url="https://signoz.io/docs/userguide/query-builder?utm_source=product&utm_medium=query-builder"
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
.y-axis-unit-selector {
|
||||
flex-direction: row !important;
|
||||
align-items: center;
|
||||
|
||||
.heading {
|
||||
width: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,9 @@ export function ColumnUnitSelector(
|
||||
|
||||
function getAggregateColumnsNamesAndLabels(): string[] {
|
||||
if (currentQuery.queryType === EQueryType.QUERY_BUILDER) {
|
||||
return currentQuery.builder.queryData.map((q) => q.queryName);
|
||||
const queries = currentQuery.builder.queryData.map((q) => q.queryName);
|
||||
const formulas = currentQuery.builder.queryFormulas.map((q) => q.queryName);
|
||||
return [...queries, ...formulas];
|
||||
}
|
||||
if (currentQuery.queryType === EQueryType.CLICKHOUSE) {
|
||||
return currentQuery.clickhouse_sql.map((q) => q.name);
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import ROUTES from 'constants/routes';
|
||||
import { QueryBuilderProvider } from 'providers/QueryBuilder';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { render } from 'tests/test-utils';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { ColumnUnitSelector } from '../ColumnUnitSelector';
|
||||
|
||||
const compositeQueryParam = {
|
||||
queryType: 'builder',
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
dataSource: 'metrics',
|
||||
queryName: 'A',
|
||||
aggregateOperator: 'count',
|
||||
aggregateAttribute: {
|
||||
key: 'signoz_latency',
|
||||
dataType: 'float64',
|
||||
type: 'ExponentialHistogram',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'signoz_latency--float64--ExponentialHistogram--true',
|
||||
},
|
||||
timeAggregation: '',
|
||||
spaceAggregation: 'p90',
|
||||
functions: [],
|
||||
filters: {
|
||||
items: [],
|
||||
op: 'AND',
|
||||
},
|
||||
expression: 'A',
|
||||
disabled: false,
|
||||
stepInterval: 60,
|
||||
having: [],
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
groupBy: [
|
||||
{
|
||||
key: 'service_name',
|
||||
dataType: 'string',
|
||||
type: 'tag',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
id: 'service_name--string--tag--false',
|
||||
},
|
||||
],
|
||||
legend: '',
|
||||
reduceTo: 'avg',
|
||||
},
|
||||
],
|
||||
queryFormulas: [
|
||||
{
|
||||
queryName: 'F1',
|
||||
expression: 'A * 10',
|
||||
disabled: false,
|
||||
legend: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
promql: [
|
||||
{
|
||||
name: 'A',
|
||||
query: '',
|
||||
legend: '',
|
||||
disabled: false,
|
||||
},
|
||||
],
|
||||
clickhouse_sql: [
|
||||
{
|
||||
name: 'A',
|
||||
legend: '',
|
||||
disabled: false,
|
||||
query: '',
|
||||
},
|
||||
],
|
||||
id: '12e1d311-cb47-4b76-af68-65d8e85c9e0d',
|
||||
};
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: jest.fn(),
|
||||
useRouteMatch: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/queryBuilder/useGetCompositeQueryParam', () => ({
|
||||
useGetCompositeQueryParam: (): Query => compositeQueryParam as Query,
|
||||
}));
|
||||
|
||||
describe('Column unit selector panel unit test', () => {
|
||||
it('unit selectors should be rendered for queries and formula', () => {
|
||||
const mockLocation = {
|
||||
pathname: `${process.env.FRONTEND_API_ENDPOINT}/${ROUTES.DASHBOARD_WIDGET}/`,
|
||||
};
|
||||
(useLocation as jest.Mock).mockReturnValue(mockLocation);
|
||||
const { getByText } = render(
|
||||
<QueryBuilderProvider>
|
||||
<ColumnUnitSelector columnUnits={{}} setColumnUnits={(): void => {}} />,
|
||||
</QueryBuilderProvider>,
|
||||
);
|
||||
|
||||
expect(getByText('F1')).toBeInTheDocument();
|
||||
expect(getByText('A')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -36,6 +36,10 @@ export const timeItems: timePreferance[] = [
|
||||
name: 'Last 1 week',
|
||||
enum: 'LAST_1_WEEK',
|
||||
},
|
||||
{
|
||||
name: 'Last 1 month',
|
||||
enum: 'LAST_1_MONTH',
|
||||
},
|
||||
];
|
||||
|
||||
export interface timePreferance {
|
||||
@@ -52,7 +56,8 @@ export type timePreferenceType =
|
||||
| LAST_6_HR
|
||||
| LAST_1_DAY
|
||||
| LAST_3_DAYS
|
||||
| LAST_1_WEEK;
|
||||
| LAST_1_WEEK
|
||||
| LAST_1_MONTH;
|
||||
|
||||
type GLOBAL_TIME = 'GLOBAL_TIME';
|
||||
type LAST_5_MIN = 'LAST_5_MIN';
|
||||
@@ -63,5 +68,6 @@ type LAST_6_HR = 'LAST_6_HR';
|
||||
type LAST_1_DAY = 'LAST_1_DAY';
|
||||
type LAST_3_DAYS = 'LAST_3_DAYS';
|
||||
type LAST_1_WEEK = 'LAST_1_WEEK';
|
||||
type LAST_1_MONTH = 'LAST_1_MONTH';
|
||||
|
||||
export default timeItems;
|
||||
|
||||
@@ -4,8 +4,6 @@ import './NewWidget.styles.scss';
|
||||
import { WarningOutlined } from '@ant-design/icons';
|
||||
import { Button, Flex, Modal, Space, Tooltip, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
|
||||
import { chartHelpMessage } from 'components/facingIssueBtn/util';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { QueryParams } from 'constants/query';
|
||||
@@ -79,6 +77,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
stagedQuery,
|
||||
redirectWithQueryBuilderData,
|
||||
supersetQuery,
|
||||
setSupersetQuery,
|
||||
} = useQueryBuilder();
|
||||
|
||||
const isQueryModified = useMemo(
|
||||
@@ -548,6 +547,17 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
isNewTraceLogsAvailable,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* we need this extra handling for superset query because we cannot keep this in sync with current query
|
||||
* always.we do not sync superset query in the initQueryBuilderData because that function is called on all stage and run
|
||||
* actions. we do not want that as we loose out on superset functionalities if we do the same. hence initialising the superset query
|
||||
* on mount here with the currentQuery in the begining itself
|
||||
*/
|
||||
setSupersetQuery(currentQuery);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
registerShortcut(DashboardShortcuts.SaveChanges, onSaveDashboard);
|
||||
registerShortcut(DashboardShortcuts.DiscardChanges, onClickDiscardHandler);
|
||||
@@ -563,11 +573,11 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
if (selectedGraph === PANEL_TYPES.LIST) {
|
||||
const initialDataSource = currentQuery.builder.queryData[0].dataSource;
|
||||
if (initialDataSource === DataSource.LOGS) {
|
||||
// we do not need selected log columns in the request data as the entire response contains all the necessary data
|
||||
setRequestData((prev) => ({
|
||||
...prev,
|
||||
tableParams: {
|
||||
...prev.tableParams,
|
||||
selectColumns: selectedLogFields,
|
||||
},
|
||||
}));
|
||||
} else if (initialDataSource === DataSource.TRACES) {
|
||||
@@ -596,20 +606,6 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
<Typography.Text className="configure-panel">
|
||||
Configure panel
|
||||
</Typography.Text>
|
||||
<FacingIssueBtn
|
||||
attributes={{
|
||||
uuid: selectedDashboard?.uuid,
|
||||
title: selectedDashboard?.data.title,
|
||||
screen: 'Dashboard widget',
|
||||
panelType: graphType,
|
||||
widgetId: query.get('widgetId'),
|
||||
queryType: currentQuery.queryType,
|
||||
}}
|
||||
eventName="Dashboard: Facing Issues in dashboard"
|
||||
message={chartHelpMessage(selectedDashboard, graphType)}
|
||||
buttonText="Facing issues with dashboards?"
|
||||
onHoverText="Click here to get help with dashboard widget"
|
||||
/>
|
||||
</Flex>
|
||||
</div>
|
||||
{isSaveDisabled && (
|
||||
|
||||
@@ -268,6 +268,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'aggregateOperator',
|
||||
'aggregateAttribute',
|
||||
'groupBy',
|
||||
'reduceTo',
|
||||
'limit',
|
||||
'having',
|
||||
'orderBy',
|
||||
@@ -286,6 +287,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'aggregateOperator',
|
||||
'aggregateAttribute',
|
||||
'groupBy',
|
||||
'reduceTo',
|
||||
'limit',
|
||||
'having',
|
||||
'orderBy',
|
||||
@@ -305,6 +307,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'aggregateOperator',
|
||||
'aggregateAttribute',
|
||||
'groupBy',
|
||||
'reduceTo',
|
||||
'limit',
|
||||
'having',
|
||||
'orderBy',
|
||||
|
||||
@@ -1,24 +1,43 @@
|
||||
## Install otel-collector in your Kubernetes infra
|
||||
|
||||
### Install otel-collector in your Kubernetes infra
|
||||
|
||||
Add the SigNoz Helm Chart repository
|
||||
```bash
|
||||
helm repo add signoz https://charts.signoz.io
|
||||
```
|
||||
|
||||
|
||||
|
||||
If the chart is already present, update the chart to the latest using:
|
||||
```bash
|
||||
helm repo update
|
||||
```
|
||||
|
||||
|
||||
|
||||
Install the Kubernetes Infrastructure chart provided by SigNoz
|
||||
```bash
|
||||
helm install my-release signoz/k8s-infra \
|
||||
--set otelCollectorEndpoint=ingest.{{REGION}}.signoz.cloud:443 \
|
||||
--set otelInsecure=false \
|
||||
--set signozApiKey={{SIGNOZ_INGESTION_KEY}} \
|
||||
--set global.clusterName=<CLUSTER_NAME>
|
||||
For generic Kubernetes clusters, you can create *override-values.yaml* with the following configuration:
|
||||
|
||||
```yaml
|
||||
global:
|
||||
cloud: others
|
||||
clusterName: <CLUSTER_NAME>
|
||||
deploymentEnvironment: <DEPLOYMENT_ENVIRONMENT>
|
||||
otelCollectorEndpoint: ingest.{{REGION}}.signoz.cloud:443
|
||||
otelInsecure: false
|
||||
signozApiKey: {{SIGNOZ_INGESTION_KEY}}
|
||||
presets:
|
||||
otlpExporter:
|
||||
enabled: true
|
||||
loggingExporter:
|
||||
enabled: false
|
||||
```
|
||||
|
||||
- Replace `<CLUSTER_NAME>` with the name of the Kubernetes cluster or a unique identifier of the cluster.
|
||||
- Replace `<DEPLOYMENT_ENVIRONMENT>` with the deployment environment of your application. Example: **"staging"**, **"production"**, etc.
|
||||
|
||||
|
||||
|
||||
To install the k8s-infra chart with the above configuration, run the following command:
|
||||
|
||||
```bash
|
||||
helm install my-release signoz/k8s-infra -f override-values.yaml
|
||||
```
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
**Step 1: Installing the OpenTelemetry dependency packages:**
|
||||
|
||||
```bash
|
||||
dotnet add package OpenTelemetry
|
||||
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
|
||||
dotnet add package OpenTelemetry.Extensions.Hosting
|
||||
dotnet add package OpenTelemetry.Instrumentation.Runtime
|
||||
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
|
||||
dotnet add package OpenTelemetry.AutoInstrumentation
|
||||
```
|
||||
|
||||
**Step 2: Adding OpenTelemetry as a service and configuring exporter options in `Program.cs`:**
|
||||
|
||||
In your `Program.cs` file, add OpenTelemetry as a service.
|
||||
|
||||
Here’s a sample `Program.cs` file with the configured variables.
|
||||
|
||||
```bash
|
||||
using System.Diagnostics;
|
||||
using OpenTelemetry.Exporter;
|
||||
using OpenTelemetry.Resources;
|
||||
using OpenTelemetry.Trace;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Configure OpenTelemetry with tracing and auto-start.
|
||||
builder.Services.AddOpenTelemetry()
|
||||
.ConfigureResource(resource =>
|
||||
resource.AddService(serviceName: "{{MYAPP}}"))
|
||||
.WithTracing(tracing => tracing
|
||||
.AddAspNetCoreInstrumentation()
|
||||
.AddOtlpExporter(otlpOptions =>
|
||||
{
|
||||
//SigNoz Cloud Endpoint
|
||||
otlpOptions.Endpoint = new Uri("https://ingest.{{REGION}}.signoz.cloud:443");
|
||||
|
||||
otlpOptions.Protocol = OtlpExportProtocol.Grpc;
|
||||
|
||||
//SigNoz Cloud account Ingestion key
|
||||
string headerKey = "signoz-access-token";
|
||||
string headerValue = "{{SIGNOZ_INGESTION_KEY}}";
|
||||
|
||||
string formattedHeader = $"{headerKey}={headerValue}";
|
||||
otlpOptions.Headers = formattedHeader;
|
||||
}));
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// The index route ("/") is set up to write out the OpenTelemetry trace information on the response:
|
||||
app.MapGet("/", () => $"Hello World! OpenTelemetry Trace: {Activity.Current?.Id}");
|
||||
|
||||
app.Run();
|
||||
```
|
||||
|
||||
|
||||
**Step 3. Running the .NET application:**
|
||||
|
||||
```bash
|
||||
dotnet build
|
||||
dotnet run
|
||||
```
|
||||
|
||||
**Step 4: Generating some load data and checking your application in SigNoz UI**
|
||||
|
||||
Once your application is running, generate some traffic by interacting with it.
|
||||
|
||||
In the SigNoz account, open the `Services` tab. Hit the `Refresh` button on the top right corner, and your application should appear in the list of `Applications`. Ensure that you're checking data for the `time range filter` applied in the top right corner. You might have to wait for a few seconds before the data appears on SigNoz UI.
|
||||
@@ -0,0 +1,10 @@
|
||||
|
||||
To run your .NET application, use the below command :
|
||||
|
||||
```bash
|
||||
dotnet build
|
||||
dotnet run
|
||||
```
|
||||
|
||||
Once you run your .NET application, interact with your application to generate some load and see your application in the SigNoz UI.
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
## Setup OpenTelemetry Binary as an agent
|
||||
|
||||
|
||||
|
||||
As a first step, you should install the OTel collector Binary according to the instructions provided on [this link](https://signoz.io/docs/tutorial/opentelemetry-binary-usage-in-virtual-machine/).
|
||||
|
||||
|
||||
|
||||
Once you are done setting up the OTel collector binary, you can follow the next steps.
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
After setting up the Otel collector agent, follow the steps below to instrument your .NET Application
|
||||
|
||||
|
||||
|
||||
|
||||
### Step 1: Install OpenTelemetry Dependencies
|
||||
Install the following dependencies in your application.
|
||||
|
||||
```bash
|
||||
dotnet add package OpenTelemetry
|
||||
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
|
||||
dotnet add package OpenTelemetry.Extensions.Hosting
|
||||
dotnet add package OpenTelemetry.Instrumentation.Runtime
|
||||
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
|
||||
dotnet add package OpenTelemetry.AutoInstrumentation
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Step 2: Adding OpenTelemetry as a service and configuring exporter options
|
||||
|
||||
In your `Program.cs` file, add OpenTelemetry as a service. Here, we are configuring these variables:
|
||||
|
||||
|
||||
|
||||
Here’s a sample `Program.cs` file with the configured variables:
|
||||
|
||||
```bash
|
||||
using System.Diagnostics;
|
||||
using OpenTelemetry.Exporter;
|
||||
using OpenTelemetry.Resources;
|
||||
using OpenTelemetry.Trace;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Configure OpenTelemetry with tracing and auto-start.
|
||||
builder.Services.AddOpenTelemetry()
|
||||
.ConfigureResource(resource =>
|
||||
resource.AddService(serviceName: "{{MYAPP}}"))
|
||||
.WithTracing(tracing => tracing
|
||||
.AddAspNetCoreInstrumentation()
|
||||
.AddOtlpExporter(otlpOptions =>
|
||||
{
|
||||
otlpOptions.Endpoint = new Uri("http://localhost:4317");
|
||||
|
||||
otlpOptions.Protocol = OtlpExportProtocol.Grpc;
|
||||
}));
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
//The index route ("/") is set up to write out the OpenTelemetry trace information on the response:
|
||||
app.MapGet("/", () => $"Hello World! OpenTelemetry Trace: {Activity.Current?.Id}");
|
||||
|
||||
app.Run();
|
||||
```
|
||||
|
||||
|
||||
The OpenTelemetry.Exporter.Options get or set the target to which the exporter is going to send traces. Here, we’re configuring it to send traces to the OTel Collector agent. The target must be a valid Uri with the scheme (http or https) and host and may contain a port and a path.
|
||||
|
||||
This is done by configuring an OpenTelemetry [TracerProvider](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/docs/trace/customizing-the-sdk#readme) using extension methods and setting it to auto-start when the host is started.
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
|
||||
Once you are done intrumenting your .NET application, you can run it using the below commands
|
||||
|
||||
|
||||
### Step 1: Run OTel Collector
|
||||
Run this command inside the `otelcol-contrib` directory that you created in the install Otel Collector step
|
||||
|
||||
```bash
|
||||
./otelcol-contrib --config ./config.yaml
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Step 2: Run your .NET application
|
||||
```bash
|
||||
dotnet build
|
||||
dotnet run
|
||||
```
|
||||
@@ -1,24 +1,43 @@
|
||||
## Install otel-collector in your Kubernetes infra
|
||||
|
||||
### Install otel-collector in your Kubernetes infra
|
||||
|
||||
Add the SigNoz Helm Chart repository
|
||||
```bash
|
||||
helm repo add signoz https://charts.signoz.io
|
||||
```
|
||||
|
||||
|
||||
|
||||
If the chart is already present, update the chart to the latest using:
|
||||
```bash
|
||||
helm repo update
|
||||
```
|
||||
|
||||
|
||||
|
||||
Install the Kubernetes Infrastructure chart provided by SigNoz
|
||||
```bash
|
||||
helm install my-release signoz/k8s-infra \
|
||||
--set otelCollectorEndpoint=ingest.{{REGION}}.signoz.cloud:443 \
|
||||
--set otelInsecure=false \
|
||||
--set signozApiKey={{SIGNOZ_INGESTION_KEY}} \
|
||||
--set global.clusterName=<CLUSTER_NAME>
|
||||
For generic Kubernetes clusters, you can create *override-values.yaml* with the following configuration:
|
||||
|
||||
```yaml
|
||||
global:
|
||||
cloud: others
|
||||
clusterName: <CLUSTER_NAME>
|
||||
deploymentEnvironment: <DEPLOYMENT_ENVIRONMENT>
|
||||
otelCollectorEndpoint: ingest.{{REGION}}.signoz.cloud:443
|
||||
otelInsecure: false
|
||||
signozApiKey: {{SIGNOZ_INGESTION_KEY}}
|
||||
presets:
|
||||
otlpExporter:
|
||||
enabled: true
|
||||
loggingExporter:
|
||||
enabled: false
|
||||
```
|
||||
|
||||
- Replace `<CLUSTER_NAME>` with the name of the Kubernetes cluster or a unique identifier of the cluster.
|
||||
- Replace `<DEPLOYMENT_ENVIRONMENT>` with the deployment environment of your application. Example: **"staging"**, **"production"**, etc.
|
||||
|
||||
|
||||
|
||||
To install the k8s-infra chart with the above configuration, run the following command:
|
||||
|
||||
```bash
|
||||
helm install my-release signoz/k8s-infra -f override-values.yaml
|
||||
```
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
|
||||
|
||||
Follow the steps below to instrument your Elixir (Phoenix + Ecto) Application
|
||||
|
||||
### Step 1: Add dependencies
|
||||
Install dependencies related to OpenTelemetry by adding them to `mix.exs` file
|
||||
|
||||
```bash
|
||||
{:opentelemetry_exporter, "~> 1.6"},
|
||||
{:opentelemetry_api, "~> 1.2"},
|
||||
{:opentelemetry, "~> 1.3"},
|
||||
{:opentelemetry_semantic_conventions, "~> 0.2"},
|
||||
{:opentelemetry_cowboy, "~> 0.2.1"},
|
||||
{:opentelemetry_phoenix, "~> 1.1"},
|
||||
{:opentelemetry_ecto, "~> 1.1"}
|
||||
```
|
||||
|
||||
|
||||
In your application start, usually the `application.ex` file, setup the telemetry handlers
|
||||
|
||||
```bash
|
||||
:opentelemetry_cowboy.setup()
|
||||
OpentelemetryPhoenix.setup(adapter: :cowboy2)
|
||||
OpentelemetryEcto.setup([:{{MYAPP}}, :repo])
|
||||
```
|
||||
|
||||
|
||||
As an example, this is how you can setup the handlers in your application.ex file for an application called demo :
|
||||
|
||||
```bash
|
||||
# application.ex
|
||||
@impl true
|
||||
def start(_type, _args) do
|
||||
:opentelemetry_cowboy.setup()
|
||||
OpentelemetryPhoenix.setup(adapter: :cowboy2)
|
||||
OpentelemetryEcto.setup([:demo, :repo])
|
||||
|
||||
end
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Step 2: Configure Application
|
||||
You need to configure your application to send telemetry data by adding the following config to your `runtime.exs` file:
|
||||
|
||||
```bash
|
||||
config :opentelemetry, :resource, service: %{name: "{{MYAPP}}"}
|
||||
|
||||
config :opentelemetry, :processors,
|
||||
otel_batch_processor: %{
|
||||
exporter: {
|
||||
:opentelemetry_exporter,
|
||||
%{
|
||||
endpoints: ["https://ingest.{{REGION}}.signoz.cloud:443"],
|
||||
headers: [
|
||||
{"signoz-access-token", {{SIGNOZ_ACCESS_TOKEN}} }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
### Running your Elixir application
|
||||
Once you are done instrumenting your Elixir (Phoenix + Ecto) application with OpenTelemetry, you should install the dependencies needed to run your application and run it as you normally would.
|
||||
|
||||
|
||||
|
||||
To see some examples for instrumented applications, you can checkout [this link](https://signoz.io/docs/instrumentation/elixir/#sample-examples)
|
||||
@@ -0,0 +1,105 @@
|
||||
## Setup OpenTelemetry Binary as an agent
|
||||
|
||||
|
||||
### Step 1: Download otel-collector tar.gz
|
||||
|
||||
|
||||
|
||||
```bash
|
||||
wget https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v{{OTEL_VERSION}}/otelcol-contrib_{{OTEL_VERSION}}_linux_amd64.tar.gz
|
||||
```
|
||||
|
||||
|
||||
### Step 2: Extract otel-collector tar.gz to the `otelcol-contrib` folder
|
||||
|
||||
|
||||
|
||||
```bash
|
||||
mkdir otelcol-contrib && tar xvzf otelcol-contrib_{{OTEL_VERSION}}_linux_amd64.tar.gz -C otelcol-contrib
|
||||
```
|
||||
|
||||
|
||||
### Step 3: Create config.yaml in folder otelcol-contrib with the below content in it
|
||||
|
||||
|
||||
|
||||
```bash
|
||||
receivers:
|
||||
otlp:
|
||||
protocols:
|
||||
grpc:
|
||||
endpoint: 0.0.0.0:4317
|
||||
http:
|
||||
endpoint: 0.0.0.0:4318
|
||||
hostmetrics:
|
||||
collection_interval: 60s
|
||||
scrapers:
|
||||
cpu: {}
|
||||
disk: {}
|
||||
load: {}
|
||||
filesystem: {}
|
||||
memory: {}
|
||||
network: {}
|
||||
paging: {}
|
||||
process:
|
||||
mute_process_name_error: true
|
||||
mute_process_exe_error: true
|
||||
mute_process_io_error: true
|
||||
processes: {}
|
||||
prometheus:
|
||||
config:
|
||||
global:
|
||||
scrape_interval: 60s
|
||||
scrape_configs:
|
||||
- job_name: otel-collector-binary
|
||||
static_configs:
|
||||
- targets:
|
||||
# - localhost:8888
|
||||
processors:
|
||||
batch:
|
||||
send_batch_size: 1000
|
||||
timeout: 10s
|
||||
# Ref: https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/processor/resourcedetectionprocessor/README.md
|
||||
resourcedetection:
|
||||
detectors: [env, system] # Before system detector, include ec2 for AWS, gcp for GCP and azure for Azure.
|
||||
# Using OTEL_RESOURCE_ATTRIBUTES envvar, env detector adds custom labels.
|
||||
timeout: 2s
|
||||
system:
|
||||
hostname_sources: [os] # alternatively, use [dns,os] for setting FQDN as host.name and os as fallback
|
||||
extensions:
|
||||
health_check: {}
|
||||
zpages: {}
|
||||
exporters:
|
||||
otlp:
|
||||
endpoint: "ingest.{{REGION}}.signoz.cloud:443"
|
||||
tls:
|
||||
insecure: false
|
||||
headers:
|
||||
"signoz-access-token": "{{SIGNOZ_INGESTION_KEY}}"
|
||||
logging:
|
||||
verbosity: normal
|
||||
service:
|
||||
telemetry:
|
||||
metrics:
|
||||
address: 0.0.0.0:8888
|
||||
extensions: [health_check, zpages]
|
||||
pipelines:
|
||||
metrics:
|
||||
receivers: [otlp]
|
||||
processors: [batch]
|
||||
exporters: [otlp]
|
||||
metrics/internal:
|
||||
receivers: [prometheus, hostmetrics]
|
||||
processors: [resourcedetection, batch]
|
||||
exporters: [otlp]
|
||||
traces:
|
||||
receivers: [otlp]
|
||||
processors: [batch]
|
||||
exporters: [otlp]
|
||||
logs:
|
||||
receivers: [otlp]
|
||||
processors: [batch]
|
||||
exporters: [otlp]
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
|
||||
|
||||
After setting up the Otel collector agent, follow the steps below to instrument your Elixir (Phoenix + Ecto) Application
|
||||
|
||||
### Step 1: Add dependencies
|
||||
Install dependencies related to OpenTelemetry by adding them to `mix.exs` file
|
||||
|
||||
```bash
|
||||
{:opentelemetry_exporter, "~> 1.6"},
|
||||
{:opentelemetry_api, "~> 1.2"},
|
||||
{:opentelemetry, "~> 1.3"},
|
||||
{:opentelemetry_semantic_conventions, "~> 0.2"},
|
||||
{:opentelemetry_cowboy, "~> 0.2.1"},
|
||||
{:opentelemetry_phoenix, "~> 1.1"},
|
||||
{:opentelemetry_ecto, "~> 1.1"}
|
||||
```
|
||||
|
||||
|
||||
In your application start, usually the `application.ex` file, setup the telemetry handlers
|
||||
|
||||
```bash
|
||||
:opentelemetry_cowboy.setup()
|
||||
OpentelemetryPhoenix.setup(adapter: :cowboy2)
|
||||
OpentelemetryEcto.setup([:{{MYAPP}}, :repo])
|
||||
```
|
||||
|
||||
|
||||
As an example, this is how you can setup the handlers in your application.ex file for an application called demo :
|
||||
|
||||
```bash
|
||||
# application.ex
|
||||
@impl true
|
||||
def start(_type, _args) do
|
||||
:opentelemetry_cowboy.setup()
|
||||
OpentelemetryPhoenix.setup(adapter: :cowboy2)
|
||||
OpentelemetryEcto.setup([:demo, :repo])
|
||||
|
||||
end
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Step 2: Configure Application
|
||||
You need to configure your application to send telemetry data by adding the following config to your `runtime.exs` file:
|
||||
|
||||
```bash
|
||||
config :opentelemetry, :resource, service: %{name: "{{MYAPP}}"}
|
||||
|
||||
config :opentelemetry, :processors,
|
||||
otel_batch_processor: %{
|
||||
exporter:
|
||||
{:opentelemetry_exporter,
|
||||
%{endpoints: ["http://localhost:4318"]}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
|
||||
OTel Collector binary helps to collect logs, hostmetrics, resource and infra attributes. It is recommended to install Otel Collector binary to collect and send traces to SigNoz cloud. You can correlate signals and have rich contextual data through this way.
|
||||
|
||||
You can find instructions to install OTel Collector binary [here](https://signoz.io/docs/tutorial/opentelemetry-binary-usage-in-virtual-machine/) in your VM. Once you are done setting up your OTel Collector binary, you can follow the below steps for instrumenting your Elixir (Phoenix + Ecto) application.
|
||||
|
||||
**Step 1. Add dependencies**
|
||||
|
||||
Install dependencies related to OpenTelemetry by adding them to `mix.exs` file
|
||||
|
||||
```bash
|
||||
{:opentelemetry_exporter, "~> 1.6"},
|
||||
{:opentelemetry_api, "~> 1.2"},
|
||||
{:opentelemetry, "~> 1.3"},
|
||||
{:opentelemetry_semantic_conventions, "~> 0.2"},
|
||||
{:opentelemetry_cowboy, "~> 0.2.1"},
|
||||
{:opentelemetry_phoenix, "~> 1.1"},
|
||||
{:opentelemetry_ecto, "~> 1.1"}
|
||||
```
|
||||
|
||||
In your application start, usually the `application.ex` file, setup the telemetry handlers
|
||||
|
||||
```elixir
|
||||
:opentelemetry_cowboy.setup()
|
||||
OpentelemetryPhoenix.setup(adapter: :cowboy2)
|
||||
OpentelemetryEcto.setup([:YOUR_APP_NAME, :repo])
|
||||
```
|
||||
|
||||
As an example, this is how you can setup the handlers in your `application.ex` file for an application called `demo` :
|
||||
|
||||
```elixir
|
||||
# application.ex
|
||||
@impl true
|
||||
def start(_type, _args) do
|
||||
:opentelemetry_cowboy.setup()
|
||||
OpentelemetryPhoenix.setup(adapter: :cowboy2)
|
||||
OpentelemetryEcto.setup([:demo, :repo])
|
||||
|
||||
end
|
||||
```
|
||||
|
||||
**Step 2. Configure Application**
|
||||
|
||||
You need to configure your application to send telemtry data by adding the follwing config to your `runtime.exs` file:
|
||||
|
||||
```elixir
|
||||
config :opentelemetry, :resource, service: %{name: "{{MYAPP}}"}
|
||||
|
||||
config :opentelemetry, :processors,
|
||||
otel_batch_processor: %{
|
||||
exporter:
|
||||
{:opentelemetry_exporter,
|
||||
%{endpoints: ["http://localhost:4318"]}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,24 +1,43 @@
|
||||
## Install otel-collector in your Kubernetes infra
|
||||
|
||||
### Install otel-collector in your Kubernetes infra
|
||||
|
||||
Add the SigNoz Helm Chart repository
|
||||
```bash
|
||||
helm repo add signoz https://charts.signoz.io
|
||||
```
|
||||
|
||||
|
||||
|
||||
If the chart is already present, update the chart to the latest using:
|
||||
```bash
|
||||
helm repo update
|
||||
```
|
||||
|
||||
|
||||
|
||||
Install the Kubernetes Infrastructure chart provided by SigNoz
|
||||
```bash
|
||||
helm install my-release signoz/k8s-infra \
|
||||
--set otelCollectorEndpoint=ingest.{{REGION}}.signoz.cloud:443 \
|
||||
--set otelInsecure=false \
|
||||
--set signozApiKey={{SIGNOZ_INGESTION_KEY}} \
|
||||
--set global.clusterName=<CLUSTER_NAME>
|
||||
For generic Kubernetes clusters, you can create *override-values.yaml* with the following configuration:
|
||||
|
||||
```yaml
|
||||
global:
|
||||
cloud: others
|
||||
clusterName: <CLUSTER_NAME>
|
||||
deploymentEnvironment: <DEPLOYMENT_ENVIRONMENT>
|
||||
otelCollectorEndpoint: ingest.{{REGION}}.signoz.cloud:443
|
||||
otelInsecure: false
|
||||
signozApiKey: {{SIGNOZ_INGESTION_KEY}}
|
||||
presets:
|
||||
otlpExporter:
|
||||
enabled: true
|
||||
loggingExporter:
|
||||
enabled: false
|
||||
```
|
||||
|
||||
- Replace `<CLUSTER_NAME>` with the name of the Kubernetes cluster or a unique identifier of the cluster.
|
||||
- Replace `<DEPLOYMENT_ENVIRONMENT>` with the deployment environment of your application. Example: **"staging"**, **"production"**, etc.
|
||||
|
||||
|
||||
|
||||
To install the k8s-infra chart with the above configuration, run the following command:
|
||||
|
||||
```bash
|
||||
helm install my-release signoz/k8s-infra -f override-values.yaml
|
||||
```
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
|
||||
1. **Install Dependencies**<br></br>
|
||||
Dependencies related to OpenTelemetry exporter and SDK have to be installed first. Note that we are assuming you are using `gin` request router. If you are using other request routers, check out the [corresponding package](#request-routers).
|
||||
|
||||
Run the below commands after navigating to the application source folder:
|
||||
|
||||
|
||||
```bash
|
||||
go get go.opentelemetry.io/otel \
|
||||
go.opentelemetry.io/otel/trace \
|
||||
go.opentelemetry.io/otel/sdk \
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin \
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace \
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc
|
||||
```
|
||||
|
||||
|
||||
2. **Declare environment variables for configuring OpenTelemetry**<br></br>
|
||||
Declare the following global variables in `main.go` which we will use to configure OpenTelemetry:
|
||||
|
||||
```bash
|
||||
var (
|
||||
serviceName = "{{MYAPP}}")
|
||||
collectorURL = "https://ingest.{{REGION}}.signoz.cloud:443"
|
||||
headers="signoz-access-token={{SIGNOZ_INGESTION_KEY}}"
|
||||
insecure = os.Getenv("INSECURE_MODE")
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
3. **Instrument your Go application with OpenTelemetry**<br></br>
|
||||
To configure your application to send data we will need a function to initialize OpenTelemetry. Add the following snippet of code in your `main.go` file.
|
||||
|
||||
```bash
|
||||
|
||||
import (
|
||||
.....
|
||||
|
||||
"google.golang.org/grpc/credentials"
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
|
||||
|
||||
"go.opentelemetry.io/otel/sdk/resource"
|
||||
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||
)
|
||||
|
||||
func initTracer() func(context.Context) error {
|
||||
|
||||
var secureOption otlptracegrpc.Option
|
||||
|
||||
if strings.ToLower(insecure) == "false" || insecure == "0" || strings.ToLower(insecure) == "f" {
|
||||
secureOption = otlptracegrpc.WithTLSCredentials(credentials.NewClientTLSFromCert(nil, ""))
|
||||
} else {
|
||||
secureOption = otlptracegrpc.WithInsecure()
|
||||
}
|
||||
|
||||
exporter, err := otlptrace.New(
|
||||
context.Background(),
|
||||
otlptracegrpc.NewClient(
|
||||
secureOption,
|
||||
otlptracegrpc.WithEndpoint(collectorURL),
|
||||
otlptracegrpc.WithHeaders(headers),
|
||||
),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create exporter: %v", err)
|
||||
}
|
||||
resources, err := resource.New(
|
||||
context.Background(),
|
||||
resource.WithAttributes(
|
||||
attribute.String("{{MYAPP}}", serviceName),
|
||||
attribute.String("library.language", "go"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not set resources: %v", err)
|
||||
}
|
||||
|
||||
otel.SetTracerProvider(
|
||||
sdktrace.NewTracerProvider(
|
||||
sdktrace.WithSampler(sdktrace.AlwaysSample()),
|
||||
sdktrace.WithBatcher(exporter),
|
||||
sdktrace.WithResource(resources),
|
||||
),
|
||||
)
|
||||
return exporter.Shutdown
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
4. **Initialize the tracer in main.go**<br></br>
|
||||
Modify the main function to initialise the tracer in `main.go`. Initiate the tracer at the very beginning of our main function.
|
||||
|
||||
```go
|
||||
func main() {
|
||||
cleanup := initTracer()
|
||||
defer cleanup(context.Background())
|
||||
|
||||
......
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
5. **Add the OpenTelemetry Gin middleware**<br></br>
|
||||
Configure Gin to use the middleware by adding the following lines in `main.go`.
|
||||
|
||||
```go
|
||||
import (
|
||||
....
|
||||
"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
......
|
||||
r := gin.Default()
|
||||
r.Use(otelgin.Middleware(serviceName))
|
||||
......
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,13 @@
|
||||
**Set environment variables and run your Go Gin application**<br></br>
|
||||
The run command must have some environment variables to send data to SigNoz cloud. The run commands:
|
||||
|
||||
|
||||
|
||||
```bash
|
||||
setx INSECURE_MODE=false
|
||||
```
|
||||
|
||||
|
||||
```bash
|
||||
go run main.go
|
||||
```
|
||||
@@ -0,0 +1,12 @@
|
||||
## Setup OpenTelemetry Binary as an agent
|
||||
|
||||
|
||||
|
||||
As a first step, you should install the OTel collector Binary according to the instructions provided on [this link](https://signoz.io/docs/tutorial/opentelemetry-binary-usage-in-virtual-machine/).
|
||||
|
||||
|
||||
|
||||
Once you are done setting up the OTel collector binary, you can follow the next steps.
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
|
||||
1. **Install Dependencies**<br></br>
|
||||
Dependencies related to OpenTelemetry exporter and SDK have to be installed first. Note that we are assuming you are using `gin` request router. If you are using other request routers, check out the [corresponding package](#request-routers).
|
||||
|
||||
Run the below commands after navigating to the application source folder:
|
||||
|
||||
```bash
|
||||
go get go.opentelemetry.io/otel \
|
||||
go.opentelemetry.io/otel/trace \
|
||||
go.opentelemetry.io/otel/sdk \
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin \
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace \
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc
|
||||
```
|
||||
|
||||
|
||||
2. **Declare environment variables for configuring OpenTelemetry**<br></br>
|
||||
Declare the following global variables in `main.go` which we will use to configure OpenTelemetry:
|
||||
|
||||
```go
|
||||
var (
|
||||
serviceName = os.Getenv("SERVICE_NAME")
|
||||
collectorURL = os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT")
|
||||
insecure = os.Getenv("INSECURE_MODE")
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
3. **Instrument your Go application with OpenTelemetry**<br></br>
|
||||
To configure your application to send data we will need a function to initialize OpenTelemetry. Add the following snippet of code in your `main.go` file.
|
||||
|
||||
```go
|
||||
|
||||
import (
|
||||
.....
|
||||
|
||||
"google.golang.org/grpc/credentials"
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
|
||||
|
||||
"go.opentelemetry.io/otel/sdk/resource"
|
||||
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||
)
|
||||
|
||||
func initTracer() func(context.Context) error {
|
||||
|
||||
var secureOption otlptracegrpc.Option
|
||||
|
||||
if strings.ToLower(insecure) == "false" || insecure == "0" || strings.ToLower(insecure) == "f" {
|
||||
secureOption = otlptracegrpc.WithTLSCredentials(credentials.NewClientTLSFromCert(nil, ""))
|
||||
} else {
|
||||
secureOption = otlptracegrpc.WithInsecure()
|
||||
}
|
||||
|
||||
exporter, err := otlptrace.New(
|
||||
context.Background(),
|
||||
otlptracegrpc.NewClient(
|
||||
secureOption,
|
||||
otlptracegrpc.WithEndpoint(collectorURL),
|
||||
),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create exporter: %v", err)
|
||||
}
|
||||
resources, err := resource.New(
|
||||
context.Background(),
|
||||
resource.WithAttributes(
|
||||
attribute.String("service.name", serviceName),
|
||||
attribute.String("library.language", "go"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not set resources: %v", err)
|
||||
}
|
||||
|
||||
otel.SetTracerProvider(
|
||||
sdktrace.NewTracerProvider(
|
||||
sdktrace.WithSampler(sdktrace.AlwaysSample()),
|
||||
sdktrace.WithBatcher(exporter),
|
||||
sdktrace.WithResource(resources),
|
||||
),
|
||||
)
|
||||
return exporter.Shutdown
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
4. **Initialize the tracer in main.go**<br></br>
|
||||
Modify the main function to initialise the tracer in `main.go`. Initiate the tracer at the very beginning of our main function.
|
||||
|
||||
```go
|
||||
func main() {
|
||||
cleanup := initTracer()
|
||||
defer cleanup(context.Background())
|
||||
|
||||
......
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
5. **Add the OpenTelemetry Gin middleware**<br></br>
|
||||
Configure Gin to use the middleware by adding the following lines in `main.go`.
|
||||
|
||||
```go
|
||||
import (
|
||||
....
|
||||
"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
......
|
||||
r := gin.Default()
|
||||
r.Use(otelgin.Middleware(serviceName))
|
||||
......
|
||||
}
|
||||
```
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
**Set environment variables and run your Go Gin application**<br></br>
|
||||
The run command must have some environment variables to send data to SigNoz. Then run the following commands:
|
||||
|
||||
|
||||
```bash
|
||||
setx SERVICE_NAME={{MYAPP}}
|
||||
setx INSECURE_MODE=true
|
||||
setx OTEL_EXPORTER_OTLP_ENDPOINT=localhost:4317
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
```bash
|
||||
go run main.go
|
||||
```
|
||||
@@ -1,24 +1,43 @@
|
||||
## Install otel-collector in your Kubernetes infra
|
||||
|
||||
### Install otel-collector in your Kubernetes infra
|
||||
|
||||
Add the SigNoz Helm Chart repository
|
||||
```bash
|
||||
helm repo add signoz https://charts.signoz.io
|
||||
```
|
||||
|
||||
|
||||
|
||||
If the chart is already present, update the chart to the latest using:
|
||||
```bash
|
||||
helm repo update
|
||||
```
|
||||
|
||||
|
||||
|
||||
Install the Kubernetes Infrastructure chart provided by SigNoz
|
||||
```bash
|
||||
helm install my-release signoz/k8s-infra \
|
||||
--set otelCollectorEndpoint=ingest.{{REGION}}.signoz.cloud:443 \
|
||||
--set otelInsecure=false \
|
||||
--set signozApiKey={{SIGNOZ_INGESTION_KEY}} \
|
||||
--set global.clusterName=<CLUSTER_NAME>
|
||||
For generic Kubernetes clusters, you can create *override-values.yaml* with the following configuration:
|
||||
|
||||
```yaml
|
||||
global:
|
||||
cloud: others
|
||||
clusterName: <CLUSTER_NAME>
|
||||
deploymentEnvironment: <DEPLOYMENT_ENVIRONMENT>
|
||||
otelCollectorEndpoint: ingest.{{REGION}}.signoz.cloud:443
|
||||
otelInsecure: false
|
||||
signozApiKey: {{SIGNOZ_INGESTION_KEY}}
|
||||
presets:
|
||||
otlpExporter:
|
||||
enabled: true
|
||||
loggingExporter:
|
||||
enabled: false
|
||||
```
|
||||
|
||||
- Replace `<CLUSTER_NAME>` with the name of the Kubernetes cluster or a unique identifier of the cluster.
|
||||
- Replace `<DEPLOYMENT_ENVIRONMENT>` with the deployment environment of your application. Example: **"staging"**, **"production"**, etc.
|
||||
|
||||
|
||||
|
||||
To install the k8s-infra chart with the above configuration, run the following command:
|
||||
|
||||
```bash
|
||||
helm install my-release signoz/k8s-infra -f override-values.yaml
|
||||
```
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
|
||||
**Step 1.** Download otel java binary agent
|
||||
|
||||
```bash
|
||||
wget https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar
|
||||
```
|
||||
|
||||
|
||||
**Step 2.** Edit your configuration file,i.e `standalone.conf` for JBoss with nano or notepad.
|
||||
|
||||
|
||||
|
||||
**Step 3.** Update `JAVA_OPTS` environment variable
|
||||
|
||||
Update `JAVA_OPTS` environment variable with configurations required to send data to SigNoz cloud in your configuration file.
|
||||
|
||||
```bash
|
||||
set JAVA_OPTS=-javaagent:C:\path\to\opentelemetry-javaagent.jar
|
||||
set JAVA_OPTS=%JAVA_OPTS% -Dotel.exporter.otlp.endpoint=https://ingest.{{REGION}}.signoz.cloud:443
|
||||
set JAVA_OPTS=%JAVA_OPTS% -Dotel.exporter.otlp.headers="signoz-access-token={{SIGNOZ_INGESTION_KEY}}"
|
||||
set JAVA_OPTS=%JAVA_OPTS% -Dotel.resource.attributes="service.name={{MYAPP}}"
|
||||
```
|
||||
|
||||
|
||||
You need to replace the following things based on your environment:<br></br>
|
||||
|
||||
- `path` - Update it to the path of your downloaded Java JAR agent instead of `C:\path\to\opentelemetry-javaagent.jar`<br></br>
|
||||
@@ -0,0 +1,7 @@
|
||||
Write the output/logs of standalone.sh script to a file nohup.out as a background thread
|
||||
|
||||
```bash
|
||||
/opt/jboss-eap-7.1/bin/standalone.sh > /opt/jboss-eap-7.1/bin/nohup.out &
|
||||
```
|
||||
|
||||
In case you encounter an issue where all applications do not get listed in the services section then please refer to the [troubleshooting section](#troubleshooting-your-installation).
|
||||
@@ -0,0 +1,12 @@
|
||||
## Setup OpenTelemetry Binary as an agent
|
||||
|
||||
|
||||
|
||||
As a first step, you should install the OTel collector Binary according to the instructions provided on [this link](https://signoz.io/docs/tutorial/opentelemetry-binary-usage-in-virtual-machine/).
|
||||
|
||||
|
||||
|
||||
Once you are done setting up the OTel collector binary, you can follow the next steps.
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
After setting up the Otel collector agent, follow the steps below to instrument your JavaScript Application
|
||||
|
||||
#### Requirements
|
||||
- Java 8 or higher
|
||||
|
||||
|
||||
|
||||
Open the configuration file, generally at `jboss-eap-7.1/bin/standalone.conf` .
|
||||
@@ -0,0 +1,13 @@
|
||||
Update `JAVA_OPTS` environment variable
|
||||
|
||||
Update `JAVA_OPTS` environment variable with configurations required to send data to SigNoz cloud in your configuration file.
|
||||
|
||||
```bash
|
||||
JAVA_OPTS="-javaagent:C:/path/to/opentelemetry-javaagent.jar"
|
||||
```
|
||||
|
||||
|
||||
where,
|
||||
- `path` - Update it to the path of your downloaded Java JAR agent.<br></br>
|
||||
|
||||
In case you encounter an issue where all applications do not get listed in the services section then please refer to the [troubleshooting section](#troubleshooting-your-installation).
|
||||
@@ -1,24 +1,43 @@
|
||||
## Install otel-collector in your Kubernetes infra
|
||||
|
||||
### Install otel-collector in your Kubernetes infra
|
||||
|
||||
Add the SigNoz Helm Chart repository
|
||||
```bash
|
||||
helm repo add signoz https://charts.signoz.io
|
||||
```
|
||||
|
||||
|
||||
|
||||
If the chart is already present, update the chart to the latest using:
|
||||
```bash
|
||||
helm repo update
|
||||
```
|
||||
|
||||
|
||||
|
||||
Install the Kubernetes Infrastructure chart provided by SigNoz
|
||||
```bash
|
||||
helm install my-release signoz/k8s-infra \
|
||||
--set otelCollectorEndpoint=ingest.{{REGION}}.signoz.cloud:443 \
|
||||
--set otelInsecure=false \
|
||||
--set signozApiKey={{SIGNOZ_INGESTION_KEY}} \
|
||||
--set global.clusterName=<CLUSTER_NAME>
|
||||
For generic Kubernetes clusters, you can create *override-values.yaml* with the following configuration:
|
||||
|
||||
```yaml
|
||||
global:
|
||||
cloud: others
|
||||
clusterName: <CLUSTER_NAME>
|
||||
deploymentEnvironment: <DEPLOYMENT_ENVIRONMENT>
|
||||
otelCollectorEndpoint: ingest.{{REGION}}.signoz.cloud:443
|
||||
otelInsecure: false
|
||||
signozApiKey: {{SIGNOZ_INGESTION_KEY}}
|
||||
presets:
|
||||
otlpExporter:
|
||||
enabled: true
|
||||
loggingExporter:
|
||||
enabled: false
|
||||
```
|
||||
|
||||
- Replace `<CLUSTER_NAME>` with the name of the Kubernetes cluster or a unique identifier of the cluster.
|
||||
- Replace `<DEPLOYMENT_ENVIRONMENT>` with the deployment environment of your application. Example: **"staging"**, **"production"**, etc.
|
||||
|
||||
|
||||
|
||||
To install the k8s-infra chart with the above configuration, run the following command:
|
||||
|
||||
```bash
|
||||
helm install my-release signoz/k8s-infra -f override-values.yaml
|
||||
```
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
|
||||
#### Requirements
|
||||
- Java 8 or higher
|
||||
|
||||
|
||||
|
||||
**Step 1.** Download otel java binary agent
|
||||
|
||||
```bash
|
||||
wget https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar
|
||||
```
|
||||
@@ -0,0 +1,15 @@
|
||||
Run your application
|
||||
|
||||
|
||||
```bash
|
||||
setx OTEL_RESOURCE_ATTRIBUTES=service.name={{MYAPP}}
|
||||
setx OTEL_EXPORTER_OTLP_HEADERS="signoz-access-token={{SIGNOZ_INGESTION_KEY}}"
|
||||
setx OTEL_EXPORTER_OTLP_ENDPOINT=https://ingest.{{REGION}}.signoz.cloud:443
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
```bash
|
||||
java -javaagent:$PWD/opentelemetry-javaagent.jar -jar {{MYAPP}}.jar
|
||||
```
|
||||
@@ -0,0 +1,12 @@
|
||||
## Setup OpenTelemetry Binary as an agent
|
||||
|
||||
|
||||
|
||||
As a first step, you should install the OTel collector Binary according to the instructions provided on [this link](https://signoz.io/docs/tutorial/opentelemetry-binary-usage-in-virtual-machine/).
|
||||
|
||||
|
||||
|
||||
Once you are done setting up the OTel collector binary, you can follow the next steps.
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
After setting up the Otel collector agent, follow the steps below to instrument your Java Application
|
||||
|
||||
#### Requirements
|
||||
- Java 8 or higher
|
||||
|
||||
|
||||
|
||||
|
||||
Download OTel java binary agent
|
||||
|
||||
|
||||
|
||||
|
||||
```bash
|
||||
wget https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar
|
||||
@@ -0,0 +1,7 @@
|
||||
## Run your application
|
||||
|
||||
|
||||
|
||||
```bash
|
||||
java -javaagent:/opentelemetry-javaagent.jar -jar {{MYAPP}}.jar
|
||||
```
|
||||
@@ -1,24 +1,43 @@
|
||||
## Install otel-collector in your Kubernetes infra
|
||||
|
||||
### Install otel-collector in your Kubernetes infra
|
||||
|
||||
Add the SigNoz Helm Chart repository
|
||||
```bash
|
||||
helm repo add signoz https://charts.signoz.io
|
||||
```
|
||||
|
||||
|
||||
|
||||
If the chart is already present, update the chart to the latest using:
|
||||
```bash
|
||||
helm repo update
|
||||
```
|
||||
|
||||
|
||||
|
||||
Install the Kubernetes Infrastructure chart provided by SigNoz
|
||||
```bash
|
||||
helm install my-release signoz/k8s-infra \
|
||||
--set otelCollectorEndpoint=ingest.{{REGION}}.signoz.cloud:443 \
|
||||
--set otelInsecure=false \
|
||||
--set signozApiKey={{SIGNOZ_INGESTION_KEY}} \
|
||||
--set global.clusterName=<CLUSTER_NAME>
|
||||
For generic Kubernetes clusters, you can create *override-values.yaml* with the following configuration:
|
||||
|
||||
```yaml
|
||||
global:
|
||||
cloud: others
|
||||
clusterName: <CLUSTER_NAME>
|
||||
deploymentEnvironment: <DEPLOYMENT_ENVIRONMENT>
|
||||
otelCollectorEndpoint: ingest.{{REGION}}.signoz.cloud:443
|
||||
otelInsecure: false
|
||||
signozApiKey: {{SIGNOZ_INGESTION_KEY}}
|
||||
presets:
|
||||
otlpExporter:
|
||||
enabled: true
|
||||
loggingExporter:
|
||||
enabled: false
|
||||
```
|
||||
|
||||
- Replace `<CLUSTER_NAME>` with the name of the Kubernetes cluster or a unique identifier of the cluster.
|
||||
- Replace `<DEPLOYMENT_ENVIRONMENT>` with the deployment environment of your application. Example: **"staging"**, **"production"**, etc.
|
||||
|
||||
|
||||
|
||||
To install the k8s-infra chart with the above configuration, run the following command:
|
||||
|
||||
```bash
|
||||
helm install my-release signoz/k8s-infra -f override-values.yaml
|
||||
```
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
|
||||
#### Requirements
|
||||
- Java 8 or higher
|
||||
|
||||
|
||||
|
||||
**Step 1.** Download otel java binary agent
|
||||
|
||||
```bash
|
||||
wget https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar
|
||||
```
|
||||
@@ -0,0 +1,21 @@
|
||||
Run your application
|
||||
|
||||
```bash
|
||||
setx OTEL_RESOURCE_ATTRIBUTES=service.name={{MYAPP}}
|
||||
setx OTEL_EXPORTER_OTLP_HEADERS="signoz-access-token={{SIGNOZ_INGESTION_KEY}}"
|
||||
setx OTEL_EXPORTER_OTLP_ENDPOINT=https://ingest.{{REGION}}.signoz.cloud:443
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
```bash
|
||||
java -javaagent:/opentelemetry-javaagent.jar -jar
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
```bash
|
||||
<my-app>.jar
|
||||
```
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user