mirror of
https://github.com/SigNoz/signoz.git
synced 2026-09-21 10:50:41 +01:00
Compare commits
52 Commits
v0.75.0
...
v0.76.0-ca
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae6bbc7192 | ||
|
|
da627b9779 | ||
|
|
5bd30af3f7 | ||
|
|
29f72451d8 | ||
|
|
d09b85bea8 | ||
|
|
85fe1a2a18 | ||
|
|
2115656a5b | ||
|
|
c9da6006db | ||
|
|
114a979b14 | ||
|
|
cb69cd91a0 | ||
|
|
2d73f91380 | ||
|
|
efe86b0a00 | ||
|
|
52780a7ad9 | ||
|
|
44b46c089b | ||
|
|
ca65b4148c | ||
|
|
064a522293 | ||
|
|
8563bcdacf | ||
|
|
296a444bd8 | ||
|
|
727cd7747b | ||
|
|
a7ff27ef07 | ||
|
|
f61e33aa23 | ||
|
|
36ebde5470 | ||
|
|
509d9c7fe5 | ||
|
|
816cae3aac | ||
|
|
cb2c492618 | ||
|
|
4177b88a4e | ||
|
|
5bf79edb8b | ||
|
|
3e2c23d015 | ||
|
|
b1e3f03bb5 | ||
|
|
02865cf49e | ||
|
|
c6bd1dd283 | ||
|
|
51b4c8d85b | ||
|
|
697f16743f | ||
|
|
0f4e4473ef | ||
|
|
4eb2e0b97b | ||
|
|
8e5526c66c | ||
|
|
423561f652 | ||
|
|
dc61db6936 | ||
|
|
e9bba641bc | ||
|
|
c2d038c025 | ||
|
|
52693eb53e | ||
|
|
2f3cee814e | ||
|
|
8a01312967 | ||
|
|
fbc4e50136 | ||
|
|
2117075f50 | ||
|
|
6049ba194a | ||
|
|
068126db40 | ||
|
|
7bc52fb92b | ||
|
|
4b87ac6424 | ||
|
|
c731a74727 | ||
|
|
f47a4207a9 | ||
|
|
657240c71b |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -76,3 +76,5 @@ dist/
|
||||
|
||||
# ignore user_scripts that is fetched by init-clickhouse
|
||||
deploy/common/clickhouse/user_scripts/
|
||||
|
||||
queries.active
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
baseconstants "go.signoz.io/signoz/pkg/query-service/constants"
|
||||
"go.signoz.io/signoz/pkg/query-service/dao"
|
||||
basemodel "go.signoz.io/signoz/pkg/query-service/model"
|
||||
"go.signoz.io/signoz/pkg/types"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
@@ -45,7 +46,7 @@ func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseW
|
||||
return
|
||||
}
|
||||
|
||||
apiKey, apiErr := ah.getOrCreateCloudIntegrationPAT(r.Context(), currentUser.OrgId, cloudProvider)
|
||||
apiKey, apiErr := ah.getOrCreateCloudIntegrationPAT(r.Context(), currentUser.OrgID, cloudProvider)
|
||||
if apiErr != nil {
|
||||
RespondError(w, basemodel.WrapApiError(
|
||||
apiErr, "couldn't provision PAT for cloud integration:",
|
||||
@@ -124,7 +125,7 @@ func (ah *APIHandler) getOrCreateCloudIntegrationPAT(ctx context.Context, orgId
|
||||
))
|
||||
}
|
||||
for _, p := range allPats {
|
||||
if p.UserID == integrationUser.Id && p.Name == integrationPATName {
|
||||
if p.UserID == integrationUser.ID && p.Name == integrationPATName {
|
||||
return p.Token, nil
|
||||
}
|
||||
}
|
||||
@@ -136,7 +137,7 @@ func (ah *APIHandler) getOrCreateCloudIntegrationPAT(ctx context.Context, orgId
|
||||
|
||||
newPAT := model.PAT{
|
||||
Token: generatePATToken(),
|
||||
UserID: integrationUser.Id,
|
||||
UserID: integrationUser.ID,
|
||||
Name: integrationPATName,
|
||||
Role: baseconstants.ViewerGroup,
|
||||
ExpiresAt: 0,
|
||||
@@ -154,7 +155,7 @@ func (ah *APIHandler) getOrCreateCloudIntegrationPAT(ctx context.Context, orgId
|
||||
|
||||
func (ah *APIHandler) getOrCreateCloudIntegrationUser(
|
||||
ctx context.Context, orgId string, cloudProvider string,
|
||||
) (*basemodel.User, *basemodel.ApiError) {
|
||||
) (*types.User, *basemodel.ApiError) {
|
||||
cloudIntegrationUserId := fmt.Sprintf("%s-integration", cloudProvider)
|
||||
|
||||
integrationUserResult, apiErr := ah.AppDao().GetUser(ctx, cloudIntegrationUserId)
|
||||
@@ -171,19 +172,21 @@ func (ah *APIHandler) getOrCreateCloudIntegrationUser(
|
||||
zap.String("cloudProvider", cloudProvider),
|
||||
)
|
||||
|
||||
newUser := &basemodel.User{
|
||||
Id: cloudIntegrationUserId,
|
||||
Name: fmt.Sprintf("%s integration", cloudProvider),
|
||||
Email: fmt.Sprintf("%s@signoz.io", cloudIntegrationUserId),
|
||||
CreatedAt: time.Now().Unix(),
|
||||
OrgId: orgId,
|
||||
newUser := &types.User{
|
||||
ID: cloudIntegrationUserId,
|
||||
Name: fmt.Sprintf("%s integration", cloudProvider),
|
||||
Email: fmt.Sprintf("%s@signoz.io", cloudIntegrationUserId),
|
||||
TimeAuditable: types.TimeAuditable{
|
||||
CreatedAt: time.Now(),
|
||||
},
|
||||
OrgID: orgId,
|
||||
}
|
||||
|
||||
viewerGroup, apiErr := dao.DB().GetGroupByName(ctx, baseconstants.ViewerGroup)
|
||||
if apiErr != nil {
|
||||
return nil, basemodel.WrapApiError(apiErr, "couldn't get viewer group for creating integration user")
|
||||
}
|
||||
newUser.GroupId = viewerGroup.ID
|
||||
newUser.GroupID = viewerGroup.ID
|
||||
|
||||
passwordHash, err := auth.PasswordHash(uuid.NewString())
|
||||
if err != nil {
|
||||
|
||||
@@ -54,7 +54,7 @@ func (ah *APIHandler) createPAT(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// All the PATs are associated with the user creating the PAT.
|
||||
pat.UserID = user.Id
|
||||
pat.UserID = user.ID
|
||||
pat.CreatedAt = time.Now().Unix()
|
||||
pat.UpdatedAt = time.Now().Unix()
|
||||
pat.LastUsed = 0
|
||||
@@ -112,7 +112,7 @@ func (ah *APIHandler) updatePAT(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
req.UpdatedByUserID = user.Id
|
||||
req.UpdatedByUserID = user.ID
|
||||
id := mux.Vars(r)["id"]
|
||||
req.UpdatedAt = time.Now().Unix()
|
||||
zap.L().Info("Got Update PAT request", zap.Any("pat", req))
|
||||
@@ -135,7 +135,7 @@ func (ah *APIHandler) getPATs(w http.ResponseWriter, r *http.Request) {
|
||||
}, nil)
|
||||
return
|
||||
}
|
||||
zap.L().Info("Get PATs for user", zap.String("user_id", user.Id))
|
||||
zap.L().Info("Get PATs for user", zap.String("user_id", user.ID))
|
||||
pats, apierr := ah.AppDao().ListPATs(ctx)
|
||||
if apierr != nil {
|
||||
RespondError(w, apierr, nil)
|
||||
@@ -157,7 +157,7 @@ func (ah *APIHandler) revokePAT(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
zap.L().Info("Revoke PAT with id", zap.String("id", id))
|
||||
if apierr := ah.AppDao().RevokePAT(ctx, id, user.Id); apierr != nil {
|
||||
if apierr := ah.AppDao().RevokePAT(ctx, id, user.ID); apierr != nil {
|
||||
RespondError(w, apierr, nil)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
"go.signoz.io/signoz/ee/query-service/rules"
|
||||
"go.signoz.io/signoz/pkg/http/middleware"
|
||||
"go.signoz.io/signoz/pkg/signoz"
|
||||
"go.signoz.io/signoz/pkg/types"
|
||||
"go.signoz.io/signoz/pkg/types/authtypes"
|
||||
"go.signoz.io/signoz/pkg/web"
|
||||
|
||||
@@ -340,14 +341,14 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
|
||||
r := baseapp.NewRouter()
|
||||
|
||||
// add auth middleware
|
||||
getUserFromRequest := func(ctx context.Context) (*basemodel.UserPayload, error) {
|
||||
getUserFromRequest := func(ctx context.Context) (*types.GettableUser, error) {
|
||||
user, err := auth.GetUserFromRequestContext(ctx, apiHandler)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user.User.OrgId == "" {
|
||||
if user.User.OrgID == "" {
|
||||
return nil, basemodel.UnauthorizedError(errors.New("orgId is missing in the claims"))
|
||||
}
|
||||
|
||||
|
||||
@@ -7,14 +7,14 @@ import (
|
||||
|
||||
"go.signoz.io/signoz/ee/query-service/app/api"
|
||||
baseauth "go.signoz.io/signoz/pkg/query-service/auth"
|
||||
basemodel "go.signoz.io/signoz/pkg/query-service/model"
|
||||
"go.signoz.io/signoz/pkg/query-service/telemetry"
|
||||
"go.signoz.io/signoz/pkg/types"
|
||||
"go.signoz.io/signoz/pkg/types/authtypes"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func GetUserFromRequestContext(ctx context.Context, apiHandler *api.APIHandler) (*basemodel.UserPayload, error) {
|
||||
func GetUserFromRequestContext(ctx context.Context, apiHandler *api.APIHandler) (*types.GettableUser, error) {
|
||||
patToken, ok := authtypes.UUIDFromContext(ctx)
|
||||
if ok && patToken != "" {
|
||||
zap.L().Debug("Received a non-zero length PAT token")
|
||||
@@ -40,9 +40,9 @@ func GetUserFromRequestContext(ctx context.Context, apiHandler *api.APIHandler)
|
||||
}
|
||||
telemetry.GetInstance().SetPatTokenUser()
|
||||
dao.UpdatePATLastUsed(ctx, patToken, time.Now().Unix())
|
||||
user.User.GroupId = group.ID
|
||||
user.User.Id = pat.Id
|
||||
return &basemodel.UserPayload{
|
||||
user.User.GroupID = group.ID
|
||||
user.User.ID = pat.Id
|
||||
return &types.GettableUser{
|
||||
User: user.User,
|
||||
Role: pat.Role,
|
||||
}, nil
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
basedao "go.signoz.io/signoz/pkg/query-service/dao"
|
||||
baseint "go.signoz.io/signoz/pkg/query-service/interfaces"
|
||||
basemodel "go.signoz.io/signoz/pkg/query-service/model"
|
||||
"go.signoz.io/signoz/pkg/types"
|
||||
"go.signoz.io/signoz/pkg/types/authtypes"
|
||||
)
|
||||
|
||||
@@ -39,7 +40,7 @@ type ModelDao interface {
|
||||
GetPAT(ctx context.Context, pat string) (*model.PAT, basemodel.BaseApiError)
|
||||
UpdatePATLastUsed(ctx context.Context, pat string, lastUsed int64) basemodel.BaseApiError
|
||||
GetPATByID(ctx context.Context, id string) (*model.PAT, basemodel.BaseApiError)
|
||||
GetUserByPAT(ctx context.Context, token string) (*basemodel.UserPayload, basemodel.BaseApiError)
|
||||
GetUserByPAT(ctx context.Context, token string) (*types.GettableUser, basemodel.BaseApiError)
|
||||
ListPATs(ctx context.Context) ([]model.PAT, basemodel.BaseApiError)
|
||||
RevokePAT(ctx context.Context, id string, userID string) basemodel.BaseApiError
|
||||
}
|
||||
|
||||
@@ -14,11 +14,12 @@ import (
|
||||
baseconst "go.signoz.io/signoz/pkg/query-service/constants"
|
||||
basemodel "go.signoz.io/signoz/pkg/query-service/model"
|
||||
"go.signoz.io/signoz/pkg/query-service/utils"
|
||||
"go.signoz.io/signoz/pkg/types"
|
||||
"go.signoz.io/signoz/pkg/types/authtypes"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func (m *modelDao) createUserForSAMLRequest(ctx context.Context, email string) (*basemodel.User, basemodel.BaseApiError) {
|
||||
func (m *modelDao) createUserForSAMLRequest(ctx context.Context, email string) (*types.User, basemodel.BaseApiError) {
|
||||
// get auth domain from email domain
|
||||
domain, apierr := m.GetDomainByEmail(ctx, email)
|
||||
if apierr != nil {
|
||||
@@ -42,15 +43,17 @@ func (m *modelDao) createUserForSAMLRequest(ctx context.Context, email string) (
|
||||
return nil, apiErr
|
||||
}
|
||||
|
||||
user := &basemodel.User{
|
||||
Id: uuid.NewString(),
|
||||
Name: "",
|
||||
Email: email,
|
||||
Password: hash,
|
||||
CreatedAt: time.Now().Unix(),
|
||||
user := &types.User{
|
||||
ID: uuid.NewString(),
|
||||
Name: "",
|
||||
Email: email,
|
||||
Password: hash,
|
||||
TimeAuditable: types.TimeAuditable{
|
||||
CreatedAt: time.Now(),
|
||||
},
|
||||
ProfilePictureURL: "", // Currently unused
|
||||
GroupId: group.ID,
|
||||
OrgId: domain.OrgId,
|
||||
GroupID: group.ID,
|
||||
OrgID: domain.OrgId,
|
||||
}
|
||||
|
||||
user, apiErr = m.CreateUser(ctx, user, false)
|
||||
@@ -73,7 +76,7 @@ func (m *modelDao) PrepareSsoRedirect(ctx context.Context, redirectUri, email st
|
||||
return "", model.BadRequestStr("invalid user email received from the auth provider")
|
||||
}
|
||||
|
||||
user := &basemodel.User{}
|
||||
user := &types.User{}
|
||||
|
||||
if userPayload == nil {
|
||||
newUser, apiErr := m.createUserForSAMLRequest(ctx, email)
|
||||
@@ -95,7 +98,7 @@ func (m *modelDao) PrepareSsoRedirect(ctx context.Context, redirectUri, email st
|
||||
return fmt.Sprintf("%s?jwt=%s&usr=%s&refreshjwt=%s",
|
||||
redirectUri,
|
||||
tokenStore.AccessJwt,
|
||||
user.Id,
|
||||
user.ID,
|
||||
tokenStore.RefreshJwt), nil
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"go.signoz.io/signoz/ee/query-service/model"
|
||||
basemodel "go.signoz.io/signoz/pkg/query-service/model"
|
||||
"go.signoz.io/signoz/pkg/types"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
@@ -42,10 +43,10 @@ func (m *modelDao) CreatePAT(ctx context.Context, p model.PAT) (model.PAT, basem
|
||||
}
|
||||
} else {
|
||||
p.CreatedByUser = model.User{
|
||||
Id: createdByUser.Id,
|
||||
Id: createdByUser.ID,
|
||||
Name: createdByUser.Name,
|
||||
Email: createdByUser.Email,
|
||||
CreatedAt: createdByUser.CreatedAt,
|
||||
CreatedAt: createdByUser.CreatedAt.Unix(),
|
||||
ProfilePictureURL: createdByUser.ProfilePictureURL,
|
||||
NotFound: false,
|
||||
}
|
||||
@@ -95,10 +96,10 @@ func (m *modelDao) ListPATs(ctx context.Context) ([]model.PAT, basemodel.BaseApi
|
||||
}
|
||||
} else {
|
||||
pats[i].CreatedByUser = model.User{
|
||||
Id: createdByUser.Id,
|
||||
Id: createdByUser.ID,
|
||||
Name: createdByUser.Name,
|
||||
Email: createdByUser.Email,
|
||||
CreatedAt: createdByUser.CreatedAt,
|
||||
CreatedAt: createdByUser.CreatedAt.Unix(),
|
||||
ProfilePictureURL: createdByUser.ProfilePictureURL,
|
||||
NotFound: false,
|
||||
}
|
||||
@@ -111,10 +112,10 @@ func (m *modelDao) ListPATs(ctx context.Context) ([]model.PAT, basemodel.BaseApi
|
||||
}
|
||||
} else {
|
||||
pats[i].UpdatedByUser = model.User{
|
||||
Id: updatedByUser.Id,
|
||||
Id: updatedByUser.ID,
|
||||
Name: updatedByUser.Name,
|
||||
Email: updatedByUser.Email,
|
||||
CreatedAt: updatedByUser.CreatedAt,
|
||||
CreatedAt: updatedByUser.CreatedAt.Unix(),
|
||||
ProfilePictureURL: updatedByUser.ProfilePictureURL,
|
||||
NotFound: false,
|
||||
}
|
||||
@@ -170,8 +171,8 @@ func (m *modelDao) GetPATByID(ctx context.Context, id string) (*model.PAT, basem
|
||||
}
|
||||
|
||||
// deprecated
|
||||
func (m *modelDao) GetUserByPAT(ctx context.Context, token string) (*basemodel.UserPayload, basemodel.BaseApiError) {
|
||||
users := []basemodel.UserPayload{}
|
||||
func (m *modelDao) GetUserByPAT(ctx context.Context, token string) (*types.GettableUser, basemodel.BaseApiError) {
|
||||
users := []types.GettableUser{}
|
||||
|
||||
query := `SELECT
|
||||
u.id,
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
saml2 "github.com/russellhaering/gosaml2"
|
||||
"go.signoz.io/signoz/ee/query-service/sso"
|
||||
"go.signoz.io/signoz/ee/query-service/sso/saml"
|
||||
basemodel "go.signoz.io/signoz/pkg/query-service/model"
|
||||
"go.signoz.io/signoz/pkg/types"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
@@ -33,7 +33,7 @@ type OrgDomain struct {
|
||||
SamlConfig *SamlConfig `json:"samlConfig"`
|
||||
GoogleAuthConfig *GoogleOAuthConfig `json:"googleAuthConfig"`
|
||||
|
||||
Org *basemodel.Organization
|
||||
Org *types.Organization
|
||||
}
|
||||
|
||||
func (od *OrgDomain) String() string {
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
"@tanstack/react-virtual": "3.11.2",
|
||||
"@uiw/react-md-editor": "3.23.5",
|
||||
"@visx/group": "3.3.0",
|
||||
"@visx/hierarchy": "3.12.0",
|
||||
"@visx/shape": "3.5.0",
|
||||
"@visx/tooltip": "3.3.0",
|
||||
"@xstate/react": "^3.0.0",
|
||||
@@ -69,8 +70,9 @@
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "5.0.0",
|
||||
"css-minimizer-webpack-plugin": "5.0.1",
|
||||
"d3-hierarchy": "3.1.2",
|
||||
"dayjs": "^1.10.7",
|
||||
"dompurify": "3.1.3",
|
||||
"dompurify": "3.2.4",
|
||||
"dotenv": "8.2.0",
|
||||
"event-source-polyfill": "1.0.31",
|
||||
"eventemitter3": "5.0.1",
|
||||
|
||||
@@ -4,6 +4,7 @@ import getOrgUser from 'api/user/getOrgUser';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import history from 'lib/history';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
@@ -13,7 +14,6 @@ import { matchPath, useLocation } from 'react-router-dom';
|
||||
import { LicenseState, LicenseStatus } from 'types/api/licensesV3/getActive';
|
||||
import { Organization } from 'types/api/user/getOrganization';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
import { isCloudUser } from 'utils/app';
|
||||
import { routePermission } from 'utils/permission';
|
||||
|
||||
import routes, {
|
||||
@@ -55,7 +55,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
);
|
||||
const isOldRoute = oldRoutes.indexOf(pathname) > -1;
|
||||
const currentRoute = mapRoutes.get('current');
|
||||
const isCloudUserVal = isCloudUser();
|
||||
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
|
||||
|
||||
const [orgData, setOrgData] = useState<Organization | undefined>(undefined);
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import AppLayout from 'container/AppLayout';
|
||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
||||
import { KeyboardHotkeysProvider } from 'hooks/hotkeys/useKeyboardHotkeys';
|
||||
import { useThemeConfig } from 'hooks/useDarkMode';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { LICENSE_PLAN_KEY } from 'hooks/useLicense';
|
||||
import { NotificationProvider } from 'hooks/useNotifications';
|
||||
import { ResourceProvider } from 'hooks/useResourceAttribute';
|
||||
@@ -24,7 +25,7 @@ import { QueryBuilderProvider } from 'providers/QueryBuilder';
|
||||
import { Suspense, useCallback, useEffect, useState } from 'react';
|
||||
import { Route, Router, Switch } from 'react-router-dom';
|
||||
import { CompatRouter } from 'react-router-dom-v5-compat';
|
||||
import { extractDomain, isCloudUser, isEECloudUser } from 'utils/app';
|
||||
import { extractDomain } from 'utils/app';
|
||||
|
||||
import PrivateRoute from './Private';
|
||||
import defaultRoutes, {
|
||||
@@ -54,7 +55,10 @@ function App(): JSX.Element {
|
||||
|
||||
const { hostname, pathname } = window.location;
|
||||
|
||||
const isCloudUserVal = isCloudUser();
|
||||
const {
|
||||
isCloudUser: isCloudUserVal,
|
||||
isEECloudUser: isEECloudUserVal,
|
||||
} = useGetTenantLicense();
|
||||
|
||||
const enableAnalytics = useCallback(
|
||||
(user: IUser): void => {
|
||||
@@ -150,7 +154,7 @@ function App(): JSX.Element {
|
||||
|
||||
let updatedRoutes = defaultRoutes;
|
||||
// if the user is a cloud user
|
||||
if (isCloudUserVal || isEECloudUser()) {
|
||||
if (isCloudUserVal || isEECloudUserVal) {
|
||||
// if the user is on basic plan then remove billing
|
||||
if (isOnBasicPlan) {
|
||||
updatedRoutes = updatedRoutes.filter(
|
||||
@@ -175,6 +179,7 @@ function App(): JSX.Element {
|
||||
isCloudUserVal,
|
||||
isFetchingLicenses,
|
||||
isFetchingUser,
|
||||
isEECloudUserVal,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -9,19 +9,21 @@ const create = async (
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
let httpConfig = {};
|
||||
const username = props.username ? props.username.trim() : '';
|
||||
const password = props.password ? props.password.trim() : '';
|
||||
|
||||
if (props.username !== '' && props.password !== '') {
|
||||
if (username !== '' && password !== '') {
|
||||
httpConfig = {
|
||||
basic_auth: {
|
||||
username: props.username,
|
||||
password: props.password,
|
||||
username,
|
||||
password,
|
||||
},
|
||||
};
|
||||
} else if (props.username === '' && props.password !== '') {
|
||||
} else if (username === '' && password !== '') {
|
||||
httpConfig = {
|
||||
authorization: {
|
||||
type: 'bearer',
|
||||
credentials: props.password,
|
||||
type: 'Bearer',
|
||||
credentials: password,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,18 +9,21 @@ const editWebhook = async (
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
let httpConfig = {};
|
||||
if (props.username !== '' && props.password !== '') {
|
||||
const username = props.username ? props.username.trim() : '';
|
||||
const password = props.password ? props.password.trim() : '';
|
||||
|
||||
if (username !== '' && password !== '') {
|
||||
httpConfig = {
|
||||
basic_auth: {
|
||||
username: props.username,
|
||||
password: props.password,
|
||||
username,
|
||||
password,
|
||||
},
|
||||
};
|
||||
} else if (props.username === '' && props.password !== '') {
|
||||
} else if (username === '' && password !== '') {
|
||||
httpConfig = {
|
||||
authorization: {
|
||||
type: 'bearer',
|
||||
credentials: props.password,
|
||||
type: 'Bearer',
|
||||
credentials: password,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,19 +9,21 @@ const testWebhook = async (
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
let httpConfig = {};
|
||||
const username = props.username ? props.username.trim() : '';
|
||||
const password = props.password ? props.password.trim() : '';
|
||||
|
||||
if (props.username !== '' && props.password !== '') {
|
||||
if (username !== '' && password !== '') {
|
||||
httpConfig = {
|
||||
basic_auth: {
|
||||
username: props.username,
|
||||
password: props.password,
|
||||
username,
|
||||
password,
|
||||
},
|
||||
};
|
||||
} else if (props.username === '' && props.password !== '') {
|
||||
} else if (username === '' && password !== '') {
|
||||
httpConfig = {
|
||||
authorization: {
|
||||
type: 'bearer',
|
||||
credentials: props.password,
|
||||
type: 'Bearer',
|
||||
credentials: password,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
67
frontend/src/api/metricsExplorer/getMetricsList.ts
Normal file
67
frontend/src/api/metricsExplorer/getMetricsList.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import {
|
||||
OrderByPayload,
|
||||
TreemapViewType,
|
||||
} from 'container/MetricsExplorer/Summary/types';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
export interface MetricsListPayload {
|
||||
filters: TagFilter;
|
||||
groupBy?: BaseAutocompleteData[];
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
orderBy?: OrderByPayload;
|
||||
}
|
||||
|
||||
export enum MetricType {
|
||||
SUM = 'Sum',
|
||||
GAUGE = 'Gauge',
|
||||
HISTOGRAM = 'Histogram',
|
||||
SUMMARY = 'Summary',
|
||||
EXPONENTIAL_HISTOGRAM = 'ExponentialHistogram',
|
||||
}
|
||||
|
||||
export interface MetricsListItemData {
|
||||
metric_name: string;
|
||||
description: string;
|
||||
type: MetricType;
|
||||
unit: string;
|
||||
[TreemapViewType.CARDINALITY]: number;
|
||||
[TreemapViewType.DATAPOINTS]: number;
|
||||
lastReceived: string;
|
||||
}
|
||||
|
||||
export interface MetricsListResponse {
|
||||
status: string;
|
||||
data: {
|
||||
metrics: MetricsListItemData[];
|
||||
total?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export const getMetricsList = async (
|
||||
props: MetricsListPayload,
|
||||
signal?: AbortSignal,
|
||||
headers?: Record<string, string>,
|
||||
): Promise<SuccessResponse<MetricsListResponse> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.post('/metrics', props, {
|
||||
signal,
|
||||
headers,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data,
|
||||
params: props,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
34
frontend/src/api/metricsExplorer/getMetricsListFilterKeys.ts
Normal file
34
frontend/src/api/metricsExplorer/getMetricsListFilterKeys.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
export interface MetricsListFilterKeysResponse {
|
||||
status: string;
|
||||
data: {
|
||||
metricColumns: string[];
|
||||
attributeKeys: BaseAutocompleteData[];
|
||||
};
|
||||
}
|
||||
|
||||
export const getMetricsListFilterKeys = async (
|
||||
signal?: AbortSignal,
|
||||
headers?: Record<string, string>,
|
||||
): Promise<SuccessResponse<MetricsListFilterKeysResponse> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.get('/metrics/filters/keys', {
|
||||
signal,
|
||||
headers,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
export interface MetricsListFilterValuesPayload {
|
||||
filterAttributeKeyDataType: string;
|
||||
filterKey: string;
|
||||
searchText: string;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
export interface MetricsListFilterValuesResponse {
|
||||
status: string;
|
||||
data: {
|
||||
FilterValues: BaseAutocompleteData[];
|
||||
};
|
||||
}
|
||||
|
||||
export const getMetricsListFilterValues = async (
|
||||
props: MetricsListFilterValuesPayload,
|
||||
signal?: AbortSignal,
|
||||
headers?: Record<string, string>,
|
||||
): Promise<
|
||||
SuccessResponse<MetricsListFilterValuesResponse> | ErrorResponse
|
||||
> => {
|
||||
try {
|
||||
const response = await axios.post('/metrics/filters/values', props, {
|
||||
signal,
|
||||
headers,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data,
|
||||
params: props,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
54
frontend/src/api/metricsExplorer/getMetricsTreeMap.ts
Normal file
54
frontend/src/api/metricsExplorer/getMetricsTreeMap.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { TreemapViewType } from 'container/MetricsExplorer/Summary/types';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
export interface MetricsTreeMapPayload {
|
||||
filters: TagFilter;
|
||||
limit?: number;
|
||||
treemap?: TreemapViewType;
|
||||
}
|
||||
|
||||
export interface MetricsTreeMapResponse {
|
||||
status: string;
|
||||
data: {
|
||||
[TreemapViewType.CARDINALITY]: CardinalityData[];
|
||||
[TreemapViewType.DATAPOINTS]: DatapointsData[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface CardinalityData {
|
||||
percentage: number;
|
||||
total_value: number;
|
||||
metric_name: string;
|
||||
}
|
||||
|
||||
export interface DatapointsData {
|
||||
percentage: number;
|
||||
metric_name: string;
|
||||
}
|
||||
|
||||
export const getMetricsTreeMap = async (
|
||||
props: MetricsTreeMapPayload,
|
||||
signal?: AbortSignal,
|
||||
headers?: Record<string, string>,
|
||||
): Promise<SuccessResponse<MetricsTreeMapResponse> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.post('/metrics/treemap', props, {
|
||||
signal,
|
||||
headers,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data,
|
||||
params: props,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
@@ -1,26 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/user/setFlags';
|
||||
|
||||
const setFlags = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.patch(`/user/${props.userId}/flags`, {
|
||||
...props.flags,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data?.status,
|
||||
payload: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default setFlags;
|
||||
@@ -6,6 +6,7 @@ import logEvent from 'api/common/logEvent';
|
||||
import cx from 'classnames';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { defaultTo } from 'lodash-es';
|
||||
import { CreditCard, HelpCircle, X } from 'lucide-react';
|
||||
@@ -16,7 +17,6 @@ import { useLocation } from 'react-router-dom';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
|
||||
import { License } from 'types/api/licenses/def';
|
||||
import { isCloudUser } from 'utils/app';
|
||||
|
||||
export interface LaunchChatSupportProps {
|
||||
eventName: string;
|
||||
@@ -38,7 +38,7 @@ function LaunchChatSupport({
|
||||
onHoverText = '',
|
||||
intercomMessageDisabled = false,
|
||||
}: LaunchChatSupportProps): JSX.Element | null {
|
||||
const isCloudUserVal = isCloudUser();
|
||||
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
|
||||
const { notifications } = useNotifications();
|
||||
const {
|
||||
licenses,
|
||||
@@ -77,7 +77,6 @@ function LaunchChatSupport({
|
||||
) {
|
||||
let isChatSupportEnabled = false;
|
||||
let isPremiumSupportEnabled = false;
|
||||
const isCloudUserVal = isCloudUser();
|
||||
if (featureFlags && featureFlags.length > 0) {
|
||||
isChatSupportEnabled =
|
||||
featureFlags.find((flag) => flag.name === FeatureKeys.CHAT_SUPPORT)
|
||||
@@ -99,6 +98,7 @@ function LaunchChatSupport({
|
||||
}, [
|
||||
featureFlags,
|
||||
featureFlagsFetchError,
|
||||
isCloudUserVal,
|
||||
isFetchingFeatureFlags,
|
||||
isLoggedIn,
|
||||
licenses,
|
||||
|
||||
@@ -16,6 +16,7 @@ import { OnboardingStatusResponse } from 'api/messagingQueues/onboarding/getOnbo
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { History } from 'history';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { Bolt, Check, OctagonAlert, X } from 'lucide-react';
|
||||
import {
|
||||
KAFKA_SETUP_DOC_LINK,
|
||||
@@ -23,7 +24,6 @@ import {
|
||||
} from 'pages/MessagingQueues/MessagingQueuesUtils';
|
||||
import { ReactNode, useEffect, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { isCloudUser } from 'utils/app';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
interface AttributeCheckListProps {
|
||||
@@ -181,7 +181,7 @@ function AttributeCheckList({
|
||||
const handleFilterChange = (value: AttributesFilters): void => {
|
||||
setFilter(value);
|
||||
};
|
||||
const isCloudUserVal = isCloudUser();
|
||||
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
|
||||
const history = useHistory();
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
export default interface ReleaseNoteProps {
|
||||
path?: string;
|
||||
release?: string;
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import { Button, Space } from 'antd';
|
||||
import setFlags from 'api/user/setFlags';
|
||||
import MessageTip from 'components/MessageTip';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useCallback } from 'react';
|
||||
import { UserFlags } from 'types/api/user/setFlags';
|
||||
|
||||
import ReleaseNoteProps from '../ReleaseNoteProps';
|
||||
|
||||
export default function ReleaseNote0120({
|
||||
release,
|
||||
}: ReleaseNoteProps): JSX.Element | null {
|
||||
const { user, setUserFlags } = useAppContext();
|
||||
|
||||
const handleDontShow = useCallback(async (): Promise<void> => {
|
||||
const flags: UserFlags = { ReleaseNote0120Hide: 'Y' };
|
||||
|
||||
try {
|
||||
setUserFlags(flags);
|
||||
if (!user) {
|
||||
// no user is set, so escape the routine
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await setFlags({ userId: user.id, flags });
|
||||
|
||||
if (response.statusCode !== 200) {
|
||||
console.log('failed to complete do not show status', response.error);
|
||||
}
|
||||
} catch (e) {
|
||||
// here we do not nothing as the cost of error is minor,
|
||||
// the user can switch the do no show option again in the further.
|
||||
console.log('unexpected error: failed to complete do not show status', e);
|
||||
}
|
||||
}, [setUserFlags, user]);
|
||||
|
||||
return (
|
||||
<MessageTip
|
||||
show
|
||||
message={
|
||||
<div>
|
||||
You are using {release} of SigNoz. We have introduced distributed setup in
|
||||
v0.12.0 release. If you use or plan to use clickhouse queries in dashboard
|
||||
or alerts, you might want to read about querying the new distributed tables{' '}
|
||||
<a
|
||||
href="https://signoz.io/docs/operate/migration/upgrade-0.12/#querying-distributed-tables"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
here
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
action={
|
||||
<Space>
|
||||
<Button onClick={handleDontShow}>Do not show again</Button>
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
import ReleaseNoteProps from 'components/ReleaseNote/ReleaseNoteProps';
|
||||
import ReleaseNote0120 from 'components/ReleaseNote/Releases/ReleaseNote0120';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { UserFlags } from 'types/api/user/setFlags';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
|
||||
interface ComponentMapType {
|
||||
match: (
|
||||
path: string | undefined,
|
||||
version: string,
|
||||
userFlags: UserFlags | null,
|
||||
) => boolean;
|
||||
component: ({ path, release }: ReleaseNoteProps) => JSX.Element | null;
|
||||
}
|
||||
|
||||
const allComponentMap: ComponentMapType[] = [
|
||||
{
|
||||
match: (
|
||||
path: string | undefined,
|
||||
version: string,
|
||||
userFlags: UserFlags | null,
|
||||
): boolean => {
|
||||
if (!path) {
|
||||
return false;
|
||||
}
|
||||
const allowedPaths: string[] = [
|
||||
ROUTES.LIST_ALL_ALERT,
|
||||
ROUTES.APPLICATION,
|
||||
ROUTES.ALL_DASHBOARD,
|
||||
];
|
||||
|
||||
return (
|
||||
userFlags?.ReleaseNote0120Hide !== 'Y' &&
|
||||
allowedPaths.includes(path) &&
|
||||
version.startsWith('v0.12')
|
||||
);
|
||||
},
|
||||
component: ReleaseNote0120,
|
||||
},
|
||||
];
|
||||
|
||||
// ReleaseNote prints release specific warnings and notes that
|
||||
// user needs to be aware of before using the upgraded version.
|
||||
function ReleaseNote({ path }: ReleaseNoteProps): JSX.Element | null {
|
||||
const { user } = useAppContext();
|
||||
const { currentVersion } = useSelector<AppState, AppReducer>(
|
||||
(state) => state.app,
|
||||
);
|
||||
|
||||
const c = allComponentMap.find((item) =>
|
||||
item.match(path, currentVersion, user.flags),
|
||||
);
|
||||
|
||||
if (!c) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <c.component path={path} release={currentVersion} />;
|
||||
}
|
||||
|
||||
ReleaseNote.defaultProps = {
|
||||
path: '',
|
||||
};
|
||||
|
||||
export default ReleaseNote;
|
||||
@@ -43,4 +43,10 @@ export const REACT_QUERY_KEY = {
|
||||
AWS_GENERATE_CONNECTION_URL: 'AWS_GENERATE_CONNECTION_URL',
|
||||
AWS_GET_CONNECTION_PARAMS: 'AWS_GET_CONNECTION_PARAMS',
|
||||
GET_ATTRIBUTE_VALUES: 'GET_ATTRIBUTE_VALUES',
|
||||
|
||||
// Metrics Explorer Query Keys
|
||||
GET_METRICS_LIST: 'GET_METRICS_LIST',
|
||||
GET_METRICS_TREE_MAP: 'GET_METRICS_TREE_MAP',
|
||||
GET_METRICS_LIST_FILTER_KEYS: 'GET_METRICS_LIST_FILTER_KEYS',
|
||||
GET_METRICS_LIST_FILTER_VALUES: 'GET_METRICS_LIST_FILTER_VALUES',
|
||||
};
|
||||
|
||||
@@ -23,6 +23,7 @@ import SideNav from 'container/SideNav';
|
||||
import TopNav from 'container/TopNav';
|
||||
import dayjs from 'dayjs';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import history from 'lib/history';
|
||||
import { isNull } from 'lodash-es';
|
||||
@@ -54,7 +55,6 @@ import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
|
||||
import { LicenseEvent } from 'types/api/licensesV3/getActive';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
import { isCloudUser } from 'utils/app';
|
||||
import { eventEmitter } from 'utils/getEventEmitter';
|
||||
import {
|
||||
getFormattedDate,
|
||||
@@ -122,6 +122,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
const { pathname } = useLocation();
|
||||
const { t } = useTranslation(['titles']);
|
||||
|
||||
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
|
||||
|
||||
const [getUserVersionResponse, getUserLatestVersionResponse] = useQueries([
|
||||
{
|
||||
queryFn: getUserVersion,
|
||||
@@ -354,7 +356,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
) {
|
||||
let isChatSupportEnabled = false;
|
||||
let isPremiumSupportEnabled = false;
|
||||
const isCloudUserVal = isCloudUser();
|
||||
if (featureFlags && featureFlags.length > 0) {
|
||||
isChatSupportEnabled =
|
||||
featureFlags.find((flag) => flag.name === FeatureKeys.CHAT_SUPPORT)
|
||||
@@ -376,6 +377,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
}, [
|
||||
featureFlags,
|
||||
featureFlagsFetchError,
|
||||
isCloudUserVal,
|
||||
isFetchingFeatureFlags,
|
||||
isLoggedIn,
|
||||
licenses,
|
||||
|
||||
@@ -24,6 +24,7 @@ import Spinner from 'components/Spinner';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import useAxiosError from 'hooks/useAxiosError';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { isEmpty, pick } from 'lodash-es';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
@@ -33,7 +34,6 @@ import { useMutation, useQuery } from 'react-query';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
|
||||
import { License } from 'types/api/licenses/def';
|
||||
import { isCloudUser } from 'utils/app';
|
||||
import { getFormattedDate, getRemainingDays } from 'utils/timeUtils';
|
||||
|
||||
import { BillingUsageGraph } from './BillingUsageGraph/BillingUsageGraph';
|
||||
@@ -145,7 +145,7 @@ export default function BillingContainer(): JSX.Element {
|
||||
|
||||
const handleError = useAxiosError();
|
||||
|
||||
const isCloudUserVal = isCloudUser();
|
||||
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
|
||||
|
||||
const processUsageData = useCallback(
|
||||
(data: any): void => {
|
||||
|
||||
@@ -5,6 +5,7 @@ import setRetentionApi from 'api/settings/setRetention';
|
||||
import TextToolTip from 'components/TextToolTip';
|
||||
import GeneralSettingsCloud from 'container/GeneralSettingsCloud';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import find from 'lodash-es/find';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
@@ -23,7 +24,6 @@ import {
|
||||
PayloadPropsMetrics as GetRetentionPeriodMetricsPayload,
|
||||
PayloadPropsTraces as GetRetentionPeriodTracesPayload,
|
||||
} from 'types/api/settings/getRetention';
|
||||
import { isCloudUser } from 'utils/app';
|
||||
|
||||
import Retention from './Retention';
|
||||
import StatusMessage from './StatusMessage';
|
||||
@@ -394,7 +394,7 @@ function GeneralSettings({
|
||||
onModalToggleHandler(type);
|
||||
};
|
||||
|
||||
const isCloudUserVal = isCloudUser();
|
||||
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
|
||||
|
||||
const renderConfig = [
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Col, Row, Select } from 'antd';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { find } from 'lodash-es';
|
||||
import {
|
||||
ChangeEvent,
|
||||
@@ -8,7 +9,6 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { isCloudUser } from 'utils/app';
|
||||
|
||||
import {
|
||||
Input,
|
||||
@@ -39,6 +39,9 @@ function Retention({
|
||||
initialValue,
|
||||
);
|
||||
const interacted = useRef(false);
|
||||
|
||||
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
|
||||
|
||||
useEffect(() => {
|
||||
if (!interacted.current) setSelectedValue(initialValue);
|
||||
}, [initialValue]);
|
||||
@@ -91,8 +94,6 @@ function Retention({
|
||||
return null;
|
||||
}
|
||||
|
||||
const isCloudUserVal = isCloudUser();
|
||||
|
||||
return (
|
||||
<RetentionContainer>
|
||||
<Row justify="space-between">
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
import { Space } from 'antd';
|
||||
import getAll from 'api/alerts/getAll';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import ReleaseNote from 'components/ReleaseNote';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { isUndefined } from 'lodash-es';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { AlertsEmptyState } from './AlertsEmptyState/AlertsEmptyState';
|
||||
import ListAlert from './ListAlert';
|
||||
|
||||
function ListAlertRules(): JSX.Element {
|
||||
const { t } = useTranslation('common');
|
||||
const location = useLocation();
|
||||
const { data, isError, isLoading, refetch, status } = useQuery('allAlerts', {
|
||||
queryFn: getAll,
|
||||
cacheTime: 0,
|
||||
@@ -70,7 +67,6 @@ function ListAlertRules(): JSX.Element {
|
||||
|
||||
return (
|
||||
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||
<ReleaseNote path={location.pathname} />
|
||||
<ListAlert
|
||||
{...{
|
||||
allAlertRules: data.payload,
|
||||
|
||||
@@ -34,6 +34,7 @@ import { Base64Icons } from 'container/NewDashboard/DashboardSettings/General/ut
|
||||
import dayjs from 'dayjs';
|
||||
import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import { get, isEmpty, isUndefined } from 'lodash-es';
|
||||
@@ -82,7 +83,6 @@ import {
|
||||
WidgetRow,
|
||||
Widgets,
|
||||
} from 'types/api/dashboard/getAll';
|
||||
import { isCloudUser } from 'utils/app';
|
||||
|
||||
import DashboardTemplatesModal from './DashboardTemplates/DashboardTemplatesModal';
|
||||
import ImportJSON from './ImportJSON';
|
||||
@@ -111,6 +111,8 @@ function DashboardsList(): JSX.Element {
|
||||
setListSortOrder: setSortOrder,
|
||||
} = useDashboard();
|
||||
|
||||
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
|
||||
|
||||
const [searchString, setSearchString] = useState<string>(
|
||||
sortOrder.search || '',
|
||||
);
|
||||
@@ -694,7 +696,7 @@ function DashboardsList(): JSX.Element {
|
||||
Create and manage dashboards for your workspace.
|
||||
</Typography.Text>
|
||||
</Flex>
|
||||
{isCloudUser() && (
|
||||
{isCloudUserVal && (
|
||||
<div className="integrations-container">
|
||||
<div className="integrations-content">
|
||||
<RequestDashboardBtn />
|
||||
@@ -735,7 +737,7 @@ function DashboardsList(): JSX.Element {
|
||||
<Button
|
||||
type="text"
|
||||
className="learn-more"
|
||||
onClick={(): void => handleContactSupport(isCloudUser())}
|
||||
onClick={(): void => handleContactSupport(isCloudUserVal)}
|
||||
>
|
||||
Contact Support
|
||||
</Button>
|
||||
|
||||
@@ -22,4 +22,10 @@
|
||||
width: fit-content;
|
||||
}
|
||||
}
|
||||
}
|
||||
&__item {
|
||||
width: 100%;
|
||||
.raw-log-content {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import './ContextLogRenderer.styles.scss';
|
||||
|
||||
import { Skeleton } from 'antd';
|
||||
import { Button, Skeleton } from 'antd';
|
||||
import RawLogView from 'components/Logs/RawLogView';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ShowButton from 'container/LogsContextList/ShowButton';
|
||||
import { convertKeysToColumnFields } from 'container/LogsExplorerList/utils';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
@@ -11,7 +12,9 @@ import { defaultLogsSelectedColumns } from 'container/OptionsMenu/constants';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
@@ -101,20 +104,51 @@ function ContextLogRenderer({
|
||||
}
|
||||
}, [options.fontSize]);
|
||||
|
||||
const urlQuery = useUrlQuery();
|
||||
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const handleLogClick = useCallback(
|
||||
(logId: string): void => {
|
||||
urlQuery.set(QueryParams.activeLogId, `"${logId}"`);
|
||||
|
||||
urlQuery.set(
|
||||
QueryParams.compositeQuery,
|
||||
encodeURIComponent(JSON.stringify(query)),
|
||||
);
|
||||
|
||||
const link = `${pathname}?${urlQuery.toString()}`;
|
||||
|
||||
window.open(link, '_blank', 'noopener,noreferrer');
|
||||
},
|
||||
[pathname, query, urlQuery],
|
||||
);
|
||||
|
||||
const getItemContent = useCallback(
|
||||
(_: number, logTorender: ILog): JSX.Element => (
|
||||
<RawLogView
|
||||
isActiveLog={logTorender.id === log.id}
|
||||
isReadOnly
|
||||
isTextOverflowEllipsisDisabled
|
||||
key={logTorender.id}
|
||||
data={logTorender}
|
||||
linesPerRow={1}
|
||||
fontSize={options.fontSize}
|
||||
selectedFields={convertKeysToColumnFields(defaultLogsSelectedColumns)}
|
||||
/>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
className="context-log-renderer__item"
|
||||
onClick={(): void => {
|
||||
handleLogClick(logTorender.id);
|
||||
}}
|
||||
>
|
||||
<RawLogView
|
||||
isActiveLog={logTorender.id === log.id}
|
||||
isReadOnly
|
||||
isTextOverflowEllipsisDisabled
|
||||
key={logTorender.id}
|
||||
data={logTorender}
|
||||
linesPerRow={1}
|
||||
fontSize={options.fontSize}
|
||||
selectedFields={convertKeysToColumnFields(
|
||||
options.selectColumns ?? defaultLogsSelectedColumns,
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
),
|
||||
[log.id, options.fontSize],
|
||||
[handleLogClick, log.id, options.fontSize, options.selectColumns],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,13 +3,15 @@
|
||||
import './LogsError.styles.scss';
|
||||
|
||||
import { Typography } from 'antd';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import history from 'lib/history';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import { isCloudUser } from 'utils/app';
|
||||
|
||||
export default function LogsError(): JSX.Element {
|
||||
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
|
||||
|
||||
const handleContactSupport = (): void => {
|
||||
if (isCloudUser()) {
|
||||
if (isCloudUserVal) {
|
||||
history.push('/support');
|
||||
} else {
|
||||
window.open('https://signoz.io/slack', '_blank');
|
||||
|
||||
@@ -30,7 +30,6 @@ import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
|
||||
import dayjs from 'dayjs';
|
||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
import { addEmptyWidgetInDashboardJSONWithQuery } from 'hooks/dashboard/utils';
|
||||
import { LogTimeRange } from 'hooks/logs/types';
|
||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
|
||||
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
|
||||
@@ -104,7 +103,7 @@ function LogsExplorerViews({
|
||||
// this is to respect the panel type present in the URL rather than defaulting it to list always.
|
||||
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
|
||||
|
||||
const { activeLogId, onTimeRangeChange } = useCopyLogLink();
|
||||
const { activeLogId } = useCopyLogLink();
|
||||
|
||||
const { queryData: pageSize } = useUrlQueryData(
|
||||
QueryParams.pageSize,
|
||||
@@ -562,7 +561,6 @@ function LogsExplorerViews({
|
||||
}, [handleSetConfig, panelTypes]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentParams = data?.params as Omit<LogTimeRange, 'pageSize'>;
|
||||
const currentData = data?.payload?.data?.newResult?.data?.result || [];
|
||||
if (currentData.length > 0 && currentData[0].list) {
|
||||
const currentLogs: ILog[] = currentData[0].list.map((item) => ({
|
||||
@@ -572,11 +570,6 @@ function LogsExplorerViews({
|
||||
const newLogs = [...logs, ...currentLogs];
|
||||
|
||||
setLogs(newLogs);
|
||||
onTimeRangeChange({
|
||||
start: currentParams?.start,
|
||||
end: currentParams?.end,
|
||||
pageSize: newLogs.length,
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -612,7 +605,6 @@ function LogsExplorerViews({
|
||||
pageSize,
|
||||
minTime,
|
||||
activeLogId,
|
||||
onTimeRangeChange,
|
||||
panelType,
|
||||
selectedView,
|
||||
]);
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Select } from 'antd';
|
||||
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import { HardHat } from 'lucide-react';
|
||||
|
||||
import { TREEMAP_VIEW_OPTIONS } from './constants';
|
||||
import { MetricsSearchProps } from './types';
|
||||
|
||||
function MetricsSearch({
|
||||
query,
|
||||
onChange,
|
||||
heatmapView,
|
||||
setHeatmapView,
|
||||
}: MetricsSearchProps): JSX.Element {
|
||||
return (
|
||||
<div className="metrics-search-container">
|
||||
<div className="metrics-search-options">
|
||||
<Select
|
||||
style={{ width: 140 }}
|
||||
options={TREEMAP_VIEW_OPTIONS}
|
||||
value={heatmapView}
|
||||
onChange={setHeatmapView}
|
||||
/>
|
||||
<DateTimeSelectionV2
|
||||
showAutoRefresh={false}
|
||||
showRefreshText={false}
|
||||
hideShareModal
|
||||
/>
|
||||
</div>
|
||||
<QueryBuilderSearch
|
||||
query={query}
|
||||
onChange={onChange}
|
||||
suffixIcon={<HardHat size={16} />}
|
||||
isMetricsExplorer
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MetricsSearch;
|
||||
@@ -0,0 +1,86 @@
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Spin,
|
||||
Table,
|
||||
TablePaginationConfig,
|
||||
TableProps,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { SorterResult } from 'antd/es/table/interface';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { MetricsListItemRowData, MetricsTableProps } from './types';
|
||||
import { metricsTableColumns } from './utils';
|
||||
|
||||
function MetricsTable({
|
||||
isLoading,
|
||||
data,
|
||||
pageSize,
|
||||
currentPage,
|
||||
onPaginationChange,
|
||||
setOrderBy,
|
||||
totalCount,
|
||||
}: MetricsTableProps): JSX.Element {
|
||||
const handleTableChange: TableProps<MetricsListItemRowData>['onChange'] = useCallback(
|
||||
(
|
||||
_pagination: TablePaginationConfig,
|
||||
_filters: Record<string, (string | number | boolean)[] | null>,
|
||||
sorter:
|
||||
| SorterResult<MetricsListItemRowData>
|
||||
| SorterResult<MetricsListItemRowData>[],
|
||||
): void => {
|
||||
if ('field' in sorter && sorter.order) {
|
||||
setOrderBy({
|
||||
columnName: sorter.field as string,
|
||||
order: sorter.order === 'ascend' ? 'asc' : 'desc',
|
||||
});
|
||||
} else {
|
||||
setOrderBy({
|
||||
columnName: 'type',
|
||||
order: 'asc',
|
||||
});
|
||||
}
|
||||
},
|
||||
[setOrderBy],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="metrics-table-container">
|
||||
<Table
|
||||
loading={{
|
||||
spinning: isLoading,
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
dataSource={data}
|
||||
columns={metricsTableColumns}
|
||||
locale={{
|
||||
emptyText: (
|
||||
<div className="no-metrics-message-container">
|
||||
<img
|
||||
src="/Icons/emptyState.svg"
|
||||
alt="thinking-emoji"
|
||||
className="empty-state-svg"
|
||||
/>
|
||||
|
||||
<Typography.Text className="no-metrics-message">
|
||||
This query had no results. Edit your query and try again!
|
||||
</Typography.Text>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
tableLayout="fixed"
|
||||
onChange={handleTableChange}
|
||||
pagination={{
|
||||
current: currentPage,
|
||||
pageSize,
|
||||
showSizeChanger: true,
|
||||
hideOnSinglePage: false,
|
||||
onChange: onPaginationChange,
|
||||
total: totalCount,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MetricsTable;
|
||||
@@ -0,0 +1,130 @@
|
||||
import { Group } from '@visx/group';
|
||||
import { Treemap } from '@visx/hierarchy';
|
||||
import { Empty, Skeleton, Tooltip } from 'antd';
|
||||
import { stratify, treemapBinary } from 'd3-hierarchy';
|
||||
import { useMemo } from 'react';
|
||||
import { useWindowSize } from 'react-use';
|
||||
|
||||
import {
|
||||
TREEMAP_HEIGHT,
|
||||
TREEMAP_MARGINS,
|
||||
TREEMAP_SQUARE_PADDING,
|
||||
} from './constants';
|
||||
import { TreemapProps, TreemapTile, TreemapViewType } from './types';
|
||||
import {
|
||||
getTreemapTileStyle,
|
||||
getTreemapTileTextStyle,
|
||||
transformTreemapData,
|
||||
} from './utils';
|
||||
|
||||
function MetricsTreemap({
|
||||
viewType,
|
||||
data,
|
||||
isLoading,
|
||||
}: TreemapProps): JSX.Element {
|
||||
const { width: windowWidth } = useWindowSize();
|
||||
|
||||
const treemapWidth = useMemo(
|
||||
() =>
|
||||
Math.max(
|
||||
windowWidth - TREEMAP_MARGINS.LEFT - TREEMAP_MARGINS.RIGHT - 70,
|
||||
300,
|
||||
),
|
||||
[windowWidth],
|
||||
);
|
||||
|
||||
const treemapData = useMemo(() => {
|
||||
const extracedTreemapData =
|
||||
(viewType === TreemapViewType.CARDINALITY
|
||||
? data?.data?.[TreemapViewType.CARDINALITY]
|
||||
: data?.data?.[TreemapViewType.DATAPOINTS]) || [];
|
||||
return transformTreemapData(extracedTreemapData, viewType);
|
||||
}, [data, viewType]);
|
||||
|
||||
const transformedTreemapData = stratify<TreemapTile>()
|
||||
.id((d) => d.id)
|
||||
.parentId((d) => d.parent)(treemapData)
|
||||
.sum((d) => d.size ?? 0);
|
||||
|
||||
const xMax = treemapWidth - TREEMAP_MARGINS.LEFT - TREEMAP_MARGINS.RIGHT;
|
||||
const yMax = TREEMAP_HEIGHT - TREEMAP_MARGINS.TOP - TREEMAP_MARGINS.BOTTOM;
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Skeleton style={{ width: treemapWidth, height: TREEMAP_HEIGHT }} active />
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!data ||
|
||||
!data.data ||
|
||||
data?.status === 'error' ||
|
||||
(data?.status === 'success' && !data?.data?.[viewType])
|
||||
) {
|
||||
return (
|
||||
<Empty
|
||||
description="No metrics found"
|
||||
style={{ width: treemapWidth, height: TREEMAP_HEIGHT, paddingTop: 30 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="metrics-treemap">
|
||||
<svg width={treemapWidth} height={TREEMAP_HEIGHT}>
|
||||
<rect
|
||||
width={treemapWidth}
|
||||
height={TREEMAP_HEIGHT}
|
||||
rx={14}
|
||||
fill="transparent"
|
||||
/>
|
||||
<Treemap<TreemapTile>
|
||||
top={TREEMAP_MARGINS.TOP}
|
||||
root={transformedTreemapData}
|
||||
size={[xMax, yMax]}
|
||||
tile={treemapBinary}
|
||||
round
|
||||
>
|
||||
{(treemap): JSX.Element => (
|
||||
<Group>
|
||||
{treemap
|
||||
.descendants()
|
||||
.reverse()
|
||||
.map((node, i) => {
|
||||
const nodeWidth = node.x1 - node.x0 - TREEMAP_SQUARE_PADDING;
|
||||
const nodeHeight = node.y1 - node.y0 - TREEMAP_SQUARE_PADDING;
|
||||
return (
|
||||
<Group
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={node.data.id || `node-${i}`}
|
||||
top={node.y0 + TREEMAP_MARGINS.TOP}
|
||||
left={node.x0 + TREEMAP_MARGINS.LEFT}
|
||||
>
|
||||
{node.depth > 0 && (
|
||||
<Tooltip
|
||||
title={`${node.data.id}: ${node.data.displayValue}%`}
|
||||
placement="top"
|
||||
>
|
||||
<foreignObject
|
||||
width={nodeWidth}
|
||||
height={nodeHeight}
|
||||
style={getTreemapTileStyle(node.data)}
|
||||
>
|
||||
<div style={getTreemapTileTextStyle()}>
|
||||
{`${node.data.displayValue}%`}
|
||||
</div>
|
||||
</foreignObject>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Group>
|
||||
);
|
||||
})}
|
||||
</Group>
|
||||
)}
|
||||
</Treemap>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MetricsTreemap;
|
||||
@@ -0,0 +1,223 @@
|
||||
.metrics-explorer-summary-tab {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 16px 0;
|
||||
|
||||
.metrics-search-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.metrics-search-options {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
.metrics-table-container {
|
||||
margin-left: -16px;
|
||||
margin-right: -16px;
|
||||
|
||||
.ant-table {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
.ant-table-thead > tr > th {
|
||||
padding: 12px;
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
|
||||
background: var(--bg-ink-500);
|
||||
border-bottom: none;
|
||||
|
||||
color: var(--Vanilla-400, #c0c1c3);
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 18px; /* 163.636% */
|
||||
letter-spacing: 0.44px;
|
||||
text-transform: uppercase;
|
||||
|
||||
&::before {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-thead > tr > th:has(.metric-name-column-header) {
|
||||
background: var(--bg-ink-400);
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
padding: 12px;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
color: var(--bg-vanilla-100);
|
||||
background: var(--bg-ink-500);
|
||||
}
|
||||
|
||||
.ant-table-cell:has(.metric-name-column-value) {
|
||||
background: var(--bg-ink-400);
|
||||
}
|
||||
|
||||
.metric-name-column-value {
|
||||
color: var(--bg-vanilla-100);
|
||||
font-family: 'Geist Mono';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.status-cell {
|
||||
.active-tag {
|
||||
color: var(--bg-forest-500);
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
.ant-progress-bg {
|
||||
height: 8px !important;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr:hover > td {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.ant-table-cell:first-child {
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.ant-table-cell:nth-child(2) {
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.ant-table-cell:nth-child(n + 3) {
|
||||
padding-right: 24px;
|
||||
}
|
||||
.column-header-right {
|
||||
text-align: right;
|
||||
}
|
||||
.column-header-left {
|
||||
text-align: left;
|
||||
}
|
||||
.ant-table-tbody > tr > td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.ant-table-thead
|
||||
> tr
|
||||
> th:not(:last-child):not(.ant-table-selection-column):not(.ant-table-row-expand-icon-cell):not([colspan])::before {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.ant-empty-normal {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-pagination {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: calc(100% - 64px);
|
||||
background: var(--bg-ink-500);
|
||||
padding: 16px;
|
||||
margin: 0;
|
||||
|
||||
// this is to offset intercom icon till we improve the design
|
||||
right: 20px;
|
||||
|
||||
.ant-pagination-item {
|
||||
border-radius: 4px;
|
||||
|
||||
&-active {
|
||||
background: var(--bg-robin-500);
|
||||
border-color: var(--bg-robin-500);
|
||||
|
||||
a {
|
||||
color: var(--bg-ink-500) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-metrics-message-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
min-height: 400px;
|
||||
gap: 16px;
|
||||
padding-top: 32px;
|
||||
}
|
||||
|
||||
.metric-type-renderer {
|
||||
border-radius: 50px;
|
||||
height: 24px;
|
||||
width: fit-content;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 4px;
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
padding: 5px 10px;
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.metrics-table-container {
|
||||
.ant-table {
|
||||
.ant-table-thead > tr > th {
|
||||
background: var(--bg-vanilla-100);
|
||||
color: var(--text-ink-300);
|
||||
}
|
||||
|
||||
.ant-table-thead > tr > th:has(.metric-name-column-header) {
|
||||
background: var(--bg-vanilla-100);
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
background: var(--bg-vanilla-100);
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
|
||||
.ant-table-cell:has(.metric-name-column-value) {
|
||||
background: var(--bg-vanilla-100);
|
||||
}
|
||||
|
||||
.metric-name-column-value {
|
||||
color: var(--bg-ink-300);
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr:hover > td {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-pagination {
|
||||
background: var(--bg-vanilla-100);
|
||||
|
||||
.ant-pagination-item {
|
||||
&-active {
|
||||
background: var(--bg-robin-500);
|
||||
border-color: var(--bg-robin-500);
|
||||
|
||||
a {
|
||||
color: var(--bg-vanilla-100) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,162 @@
|
||||
import './Summary.styles.scss';
|
||||
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { usePageSize } from 'container/InfraMonitoringK8s/utils';
|
||||
import { useGetMetricsList } from 'hooks/metricsExplorer/useGetMetricsList';
|
||||
import { useGetMetricsTreeMap } from 'hooks/metricsExplorer/useGetMetricsTreeMap';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import MetricsSearch from './MetricsSearch';
|
||||
import MetricsTable from './MetricsTable';
|
||||
import MetricsTreemap from './MetricsTreemap';
|
||||
import { OrderByPayload, TreemapViewType } from './types';
|
||||
import {
|
||||
convertNanoToMilliseconds,
|
||||
formatDataForMetricsTable,
|
||||
getMetricsListQuery,
|
||||
} from './utils';
|
||||
|
||||
function Summary(): JSX.Element {
|
||||
const { pageSize, setPageSize } = usePageSize('metricsExplorer');
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [orderBy, setOrderBy] = useState<OrderByPayload>({
|
||||
columnName: 'type',
|
||||
order: 'asc',
|
||||
});
|
||||
const [heatmapView, setHeatmapView] = useState<TreemapViewType>(
|
||||
TreemapViewType.CARDINALITY,
|
||||
);
|
||||
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const queryFilters = useMemo(
|
||||
() =>
|
||||
currentQuery?.builder?.queryData[0]?.filters || {
|
||||
items: [],
|
||||
op: 'and',
|
||||
},
|
||||
[currentQuery],
|
||||
);
|
||||
|
||||
const { handleChangeQueryData } = useQueryOperations({
|
||||
index: 0,
|
||||
query: currentQuery.builder.queryData[0],
|
||||
entityVersion: '',
|
||||
});
|
||||
|
||||
const metricsListQuery = useMemo(() => {
|
||||
const baseQuery = getMetricsListQuery();
|
||||
return {
|
||||
...baseQuery,
|
||||
limit: pageSize,
|
||||
offset: (currentPage - 1) * pageSize,
|
||||
filters: queryFilters,
|
||||
start: convertNanoToMilliseconds(minTime),
|
||||
end: convertNanoToMilliseconds(maxTime),
|
||||
orderBy,
|
||||
};
|
||||
}, [queryFilters, minTime, maxTime, orderBy, pageSize, currentPage]);
|
||||
|
||||
const metricsTreemapQuery = useMemo(
|
||||
() => ({
|
||||
limit: 100,
|
||||
filters: queryFilters,
|
||||
treemap: heatmapView,
|
||||
start: convertNanoToMilliseconds(minTime),
|
||||
end: convertNanoToMilliseconds(maxTime),
|
||||
}),
|
||||
[queryFilters, heatmapView, minTime, maxTime],
|
||||
);
|
||||
|
||||
const {
|
||||
data: metricsData,
|
||||
isLoading: isMetricsLoading,
|
||||
isFetching: isMetricsFetching,
|
||||
} = useGetMetricsList(metricsListQuery, {
|
||||
enabled: !!metricsListQuery,
|
||||
});
|
||||
|
||||
const {
|
||||
data: treeMapData,
|
||||
isLoading: isTreeMapLoading,
|
||||
isFetching: isTreeMapFetching,
|
||||
} = useGetMetricsTreeMap(metricsTreemapQuery, {
|
||||
enabled: !!metricsTreemapQuery,
|
||||
});
|
||||
|
||||
const handleFilterChange = useCallback(
|
||||
(value: TagFilter) => {
|
||||
handleChangeQueryData('filters', value);
|
||||
setCurrentPage(1);
|
||||
},
|
||||
[handleChangeQueryData],
|
||||
);
|
||||
|
||||
const updatedCurrentQuery = useMemo(
|
||||
() => ({
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: [
|
||||
{
|
||||
...currentQuery.builder.queryData[0],
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: {
|
||||
...currentQuery.builder.queryData[0].aggregateAttribute,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
[currentQuery],
|
||||
);
|
||||
|
||||
const searchQuery = updatedCurrentQuery?.builder?.queryData[0] || null;
|
||||
|
||||
const onPaginationChange = (page: number, pageSize: number): void => {
|
||||
setCurrentPage(page);
|
||||
setPageSize(pageSize);
|
||||
};
|
||||
|
||||
const formattedMetricsData = useMemo(
|
||||
() => formatDataForMetricsTable(metricsData?.payload?.data?.metrics || []),
|
||||
[metricsData],
|
||||
);
|
||||
|
||||
return (
|
||||
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||
Summary
|
||||
<div className="metrics-explorer-summary-tab">
|
||||
<MetricsSearch
|
||||
query={searchQuery}
|
||||
onChange={handleFilterChange}
|
||||
heatmapView={heatmapView}
|
||||
setHeatmapView={setHeatmapView}
|
||||
/>
|
||||
<MetricsTreemap
|
||||
data={treeMapData?.payload}
|
||||
isLoading={isTreeMapLoading || isTreeMapFetching}
|
||||
viewType={heatmapView}
|
||||
/>
|
||||
<MetricsTable
|
||||
isLoading={isMetricsLoading || isMetricsFetching}
|
||||
data={formattedMetricsData}
|
||||
pageSize={pageSize}
|
||||
currentPage={currentPage}
|
||||
onPaginationChange={onPaginationChange}
|
||||
setOrderBy={setOrderBy}
|
||||
totalCount={metricsData?.payload?.data.total || 0}
|
||||
/>
|
||||
</div>
|
||||
</Sentry.ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
26
frontend/src/container/MetricsExplorer/Summary/constants.ts
Normal file
26
frontend/src/container/MetricsExplorer/Summary/constants.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
||||
|
||||
import { TreemapViewType } from './types';
|
||||
|
||||
export const METRICS_TABLE_PAGE_SIZE = 10;
|
||||
|
||||
export const TREEMAP_VIEW_OPTIONS: {
|
||||
value: TreemapViewType;
|
||||
label: string;
|
||||
}[] = [
|
||||
{ value: TreemapViewType.CARDINALITY, label: 'Cardinality' },
|
||||
{ value: TreemapViewType.DATAPOINTS, label: 'Datapoints' },
|
||||
];
|
||||
|
||||
export const TREEMAP_HEIGHT = 300;
|
||||
export const TREEMAP_SQUARE_PADDING = 5;
|
||||
|
||||
export const TREEMAP_MARGINS = { TOP: 10, LEFT: 10, RIGHT: 10, BOTTOM: 10 };
|
||||
|
||||
export const METRIC_TYPE_LABEL_MAP = {
|
||||
[MetricType.SUM]: 'Sum',
|
||||
[MetricType.GAUGE]: 'Gauge',
|
||||
[MetricType.HISTOGRAM]: 'Histogram',
|
||||
[MetricType.SUMMARY]: 'Summary',
|
||||
[MetricType.EXPONENTIAL_HISTOGRAM]: 'Exp. Histogram',
|
||||
};
|
||||
56
frontend/src/container/MetricsExplorer/Summary/types.ts
Normal file
56
frontend/src/container/MetricsExplorer/Summary/types.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { MetricsTreeMapResponse } from 'api/metricsExplorer/getMetricsTreeMap';
|
||||
import React, { Dispatch, SetStateAction } from 'react';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
TagFilter,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
export interface MetricsTableProps {
|
||||
isLoading: boolean;
|
||||
data: MetricsListItemRowData[];
|
||||
pageSize: number;
|
||||
currentPage: number;
|
||||
onPaginationChange: (page: number, pageSize: number) => void;
|
||||
setOrderBy: Dispatch<SetStateAction<OrderByPayload>>;
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
export interface MetricsSearchProps {
|
||||
query: IBuilderQuery;
|
||||
onChange: (value: TagFilter) => void;
|
||||
heatmapView: TreemapViewType;
|
||||
setHeatmapView: (value: TreemapViewType) => void;
|
||||
}
|
||||
|
||||
export interface TreemapProps {
|
||||
data: MetricsTreeMapResponse | null | undefined;
|
||||
isLoading: boolean;
|
||||
viewType: TreemapViewType;
|
||||
}
|
||||
|
||||
export interface OrderByPayload {
|
||||
columnName: string;
|
||||
order: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
export interface MetricsListItemRowData {
|
||||
key: string;
|
||||
metric_name: React.ReactNode;
|
||||
description: React.ReactNode;
|
||||
metric_type: React.ReactNode;
|
||||
unit: React.ReactNode;
|
||||
samples: React.ReactNode;
|
||||
timeseries: React.ReactNode;
|
||||
}
|
||||
|
||||
export enum TreemapViewType {
|
||||
CARDINALITY = 'timeseries',
|
||||
DATAPOINTS = 'samples',
|
||||
}
|
||||
|
||||
export interface TreemapTile {
|
||||
id: string;
|
||||
size: number;
|
||||
displayValue: number | string | null;
|
||||
parent: string | null;
|
||||
}
|
||||
241
frontend/src/container/MetricsExplorer/Summary/utils.tsx
Normal file
241
frontend/src/container/MetricsExplorer/Summary/utils.tsx
Normal file
@@ -0,0 +1,241 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Tooltip, Typography } from 'antd';
|
||||
import { ColumnType } from 'antd/es/table';
|
||||
import {
|
||||
MetricsListItemData,
|
||||
MetricsListPayload,
|
||||
MetricType,
|
||||
} from 'api/metricsExplorer/getMetricsList';
|
||||
import {
|
||||
CardinalityData,
|
||||
DatapointsData,
|
||||
} from 'api/metricsExplorer/getMetricsTreeMap';
|
||||
import {
|
||||
BarChart,
|
||||
BarChart2,
|
||||
BarChartHorizontal,
|
||||
Diff,
|
||||
Gauge,
|
||||
} from 'lucide-react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { METRIC_TYPE_LABEL_MAP } from './constants';
|
||||
import { MetricsListItemRowData, TreemapTile, TreemapViewType } from './types';
|
||||
|
||||
export const metricsTableColumns: ColumnType<MetricsListItemRowData>[] = [
|
||||
{
|
||||
title: <div className="metric-name-column-header">METRIC</div>,
|
||||
dataIndex: 'metric_name',
|
||||
width: 400,
|
||||
sorter: true,
|
||||
className: 'metric-name-column-header',
|
||||
render: (value: string): React.ReactNode => (
|
||||
<div className="metric-name-column-value">{value}</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'DESCRIPTION',
|
||||
dataIndex: 'description',
|
||||
width: 400,
|
||||
},
|
||||
{
|
||||
title: 'TYPE',
|
||||
dataIndex: 'metric_type',
|
||||
sorter: true,
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: 'UNIT',
|
||||
dataIndex: 'unit',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: 'DATAPOINTS',
|
||||
dataIndex: TreemapViewType.DATAPOINTS,
|
||||
width: 150,
|
||||
sorter: true,
|
||||
},
|
||||
{
|
||||
title: 'CARDINALITY',
|
||||
dataIndex: TreemapViewType.CARDINALITY,
|
||||
width: 150,
|
||||
sorter: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const getMetricsListQuery = (): MetricsListPayload => ({
|
||||
filters: {
|
||||
items: [],
|
||||
op: 'and',
|
||||
},
|
||||
orderBy: { columnName: 'metric_name', order: 'asc' },
|
||||
});
|
||||
|
||||
function MetricTypeRenderer({ type }: { type: MetricType }): JSX.Element {
|
||||
const [icon, color] = useMemo(() => {
|
||||
switch (type) {
|
||||
case MetricType.SUM:
|
||||
return [
|
||||
<Diff key={type} size={12} color={Color.BG_ROBIN_500} />,
|
||||
Color.BG_ROBIN_500,
|
||||
];
|
||||
case MetricType.GAUGE:
|
||||
return [
|
||||
<Gauge key={type} size={12} color={Color.BG_SAKURA_500} />,
|
||||
Color.BG_SAKURA_500,
|
||||
];
|
||||
case MetricType.HISTOGRAM:
|
||||
return [
|
||||
<BarChart2 key={type} size={12} color={Color.BG_SIENNA_500} />,
|
||||
Color.BG_SIENNA_500,
|
||||
];
|
||||
case MetricType.SUMMARY:
|
||||
return [
|
||||
<BarChartHorizontal key={type} size={12} color={Color.BG_FOREST_500} />,
|
||||
Color.BG_FOREST_500,
|
||||
];
|
||||
case MetricType.EXPONENTIAL_HISTOGRAM:
|
||||
return [
|
||||
<BarChart key={type} size={12} color={Color.BG_AQUA_500} />,
|
||||
Color.BG_AQUA_500,
|
||||
];
|
||||
default:
|
||||
return [null, ''];
|
||||
}
|
||||
}, [type]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="metric-type-renderer"
|
||||
style={{
|
||||
backgroundColor: `${color}33`,
|
||||
border: `1px solid ${color}`,
|
||||
color,
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
<Typography.Text style={{ color, fontSize: 12 }}>
|
||||
{METRIC_TYPE_LABEL_MAP[type]}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ValidateRowValueWrapper({
|
||||
value,
|
||||
children,
|
||||
}: {
|
||||
value: string | number | null;
|
||||
children: React.ReactNode;
|
||||
}): JSX.Element {
|
||||
if (!value) {
|
||||
return <div>-</div>;
|
||||
}
|
||||
return <div>{children}</div>;
|
||||
}
|
||||
|
||||
export const formatDataForMetricsTable = (
|
||||
data: MetricsListItemData[],
|
||||
): MetricsListItemRowData[] =>
|
||||
data.map((metric) => ({
|
||||
key: metric.metric_name,
|
||||
metric_name: (
|
||||
<ValidateRowValueWrapper value={metric.metric_name}>
|
||||
<Tooltip title={metric.metric_name}>{metric.metric_name}</Tooltip>
|
||||
</ValidateRowValueWrapper>
|
||||
),
|
||||
description: (
|
||||
<ValidateRowValueWrapper value={metric.description}>
|
||||
<Tooltip title={metric.description}>{metric.description}</Tooltip>
|
||||
</ValidateRowValueWrapper>
|
||||
),
|
||||
metric_type: <MetricTypeRenderer type={metric.type} />,
|
||||
unit: (
|
||||
<ValidateRowValueWrapper value={metric.unit}>
|
||||
{metric.unit}
|
||||
</ValidateRowValueWrapper>
|
||||
),
|
||||
[TreemapViewType.DATAPOINTS]: (
|
||||
<ValidateRowValueWrapper value={metric[TreemapViewType.DATAPOINTS]}>
|
||||
{metric[TreemapViewType.DATAPOINTS]}
|
||||
</ValidateRowValueWrapper>
|
||||
),
|
||||
[TreemapViewType.CARDINALITY]: (
|
||||
<ValidateRowValueWrapper value={metric[TreemapViewType.CARDINALITY]}>
|
||||
{metric[TreemapViewType.CARDINALITY]}
|
||||
</ValidateRowValueWrapper>
|
||||
),
|
||||
}));
|
||||
|
||||
export const transformTreemapData = (
|
||||
data: CardinalityData[] | DatapointsData[],
|
||||
viewType: TreemapViewType,
|
||||
): TreemapTile[] => {
|
||||
const totalSize = (data as (CardinalityData | DatapointsData)[]).reduce(
|
||||
(acc: number, item: CardinalityData | DatapointsData) =>
|
||||
acc + item.percentage,
|
||||
0,
|
||||
);
|
||||
|
||||
const children = data.map((item) => ({
|
||||
id: item.metric_name,
|
||||
size: totalSize > 0 ? Number((item.percentage / totalSize).toFixed(2)) : 0,
|
||||
displayValue: Number(item.percentage).toFixed(2),
|
||||
parent: viewType,
|
||||
}));
|
||||
|
||||
return [
|
||||
{
|
||||
id: viewType,
|
||||
size: 0,
|
||||
parent: null,
|
||||
displayValue: null,
|
||||
},
|
||||
...children,
|
||||
];
|
||||
};
|
||||
|
||||
const getTreemapTileBackgroundColor = (node: TreemapTile): string => {
|
||||
const size = node.size * 10;
|
||||
if (size > 0.8) {
|
||||
return Color.BG_AMBER_600;
|
||||
}
|
||||
if (size > 0.6) {
|
||||
return Color.BG_AMBER_500;
|
||||
}
|
||||
if (size > 0.4) {
|
||||
return Color.BG_AMBER_400;
|
||||
}
|
||||
if (size > 0.2) {
|
||||
return Color.BG_AMBER_300;
|
||||
}
|
||||
if (size > 0.1) {
|
||||
return Color.BG_AMBER_200;
|
||||
}
|
||||
return Color.BG_AMBER_100;
|
||||
};
|
||||
|
||||
export const getTreemapTileStyle = (
|
||||
node: TreemapTile,
|
||||
): React.CSSProperties => ({
|
||||
overflow: 'visible',
|
||||
cursor: 'pointer',
|
||||
backgroundColor: getTreemapTileBackgroundColor(node),
|
||||
borderRadius: 4,
|
||||
});
|
||||
|
||||
export const getTreemapTileTextStyle = (): React.CSSProperties => ({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '12px',
|
||||
fontWeight: 'bold',
|
||||
color: Color.TEXT_SLATE_400,
|
||||
textAlign: 'center',
|
||||
padding: '4px',
|
||||
});
|
||||
|
||||
export const convertNanoToMilliseconds = (time: number): number =>
|
||||
Math.floor(time / 1000000);
|
||||
@@ -106,7 +106,7 @@ export const buildDependencyGraph = (
|
||||
Object.keys(dependencies).forEach((node) => {
|
||||
if (!inDegree[node]) inDegree[node] = 0;
|
||||
if (!adjList[node]) adjList[node] = [];
|
||||
dependencies[node].forEach((child) => {
|
||||
dependencies[node]?.forEach((child) => {
|
||||
if (!inDegree[child]) inDegree[child] = 0;
|
||||
inDegree[child]++;
|
||||
adjList[node].push(child);
|
||||
@@ -126,13 +126,13 @@ export const buildDependencyGraph = (
|
||||
}
|
||||
topologicalOrder.push(current);
|
||||
|
||||
adjList[current].forEach((neighbor) => {
|
||||
adjList[current]?.forEach((neighbor) => {
|
||||
inDegree[neighbor]--;
|
||||
if (inDegree[neighbor] === 0) queue.push(neighbor);
|
||||
});
|
||||
}
|
||||
|
||||
if (topologicalOrder.length !== Object.keys(dependencies).length) {
|
||||
if (topologicalOrder.length !== Object.keys(dependencies)?.length) {
|
||||
console.error('Cycle detected in the dependency graph!');
|
||||
}
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@ import './NoLogs.styles.scss';
|
||||
import { Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import history from 'lib/history';
|
||||
import { ArrowUpRight } from 'lucide-react';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { isCloudUser } from 'utils/app';
|
||||
import DOCLINKS from 'utils/docLinks';
|
||||
|
||||
export default function NoLogs({
|
||||
@@ -14,14 +14,15 @@ export default function NoLogs({
|
||||
}: {
|
||||
dataSource: DataSource;
|
||||
}): JSX.Element {
|
||||
const cloudUser = isCloudUser();
|
||||
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
|
||||
|
||||
const handleLinkClick = (
|
||||
e: React.MouseEvent<HTMLAnchorElement, MouseEvent>,
|
||||
): void => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (cloudUser) {
|
||||
if (isCloudUserVal) {
|
||||
if (dataSource === DataSource.TRACES) {
|
||||
logEvent('Traces Explorer: Navigate to onboarding', {});
|
||||
} else if (dataSource === DataSource.LOGS) {
|
||||
|
||||
@@ -281,7 +281,7 @@ function Members(): JSX.Element {
|
||||
const { joinedOn } = record;
|
||||
return (
|
||||
<Typography>
|
||||
{dayjs.unix(Number(joinedOn)).format(DATE_TIME_FORMATS.MONTH_DATE_FULL)}
|
||||
{dayjs(joinedOn).format(DATE_TIME_FORMATS.MONTH_DATE_FULL)}
|
||||
</Typography>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -75,6 +75,7 @@ function QueryBuilderSearch({
|
||||
placeholder,
|
||||
suffixIcon,
|
||||
isInfraMonitoring,
|
||||
isMetricsExplorer,
|
||||
disableNavigationShortcuts,
|
||||
entity,
|
||||
}: QueryBuilderSearchProps): JSX.Element {
|
||||
@@ -113,6 +114,7 @@ function QueryBuilderSearch({
|
||||
isLogsExplorerPage,
|
||||
isInfraMonitoring,
|
||||
entity,
|
||||
isMetricsExplorer,
|
||||
);
|
||||
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
@@ -129,6 +131,7 @@ function QueryBuilderSearch({
|
||||
isLogsExplorerPage,
|
||||
isInfraMonitoring,
|
||||
entity,
|
||||
isMetricsExplorer,
|
||||
);
|
||||
|
||||
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
|
||||
@@ -137,12 +140,12 @@ function QueryBuilderSearch({
|
||||
|
||||
const toggleEditMode = useCallback(
|
||||
(value: boolean) => {
|
||||
// Editing mode is required only in infra monitoring mode
|
||||
if (isInfraMonitoring) {
|
||||
// Editing mode is required only in infra monitoring or metrics explorer
|
||||
if (isInfraMonitoring || isMetricsExplorer) {
|
||||
setIsEditingTag(value);
|
||||
}
|
||||
},
|
||||
[isInfraMonitoring],
|
||||
[isInfraMonitoring, isMetricsExplorer],
|
||||
);
|
||||
|
||||
const onTagRender = ({
|
||||
@@ -168,7 +171,7 @@ function QueryBuilderSearch({
|
||||
updateTag(value);
|
||||
// Editing starts
|
||||
toggleEditMode(true);
|
||||
if (isInfraMonitoring) {
|
||||
if (isInfraMonitoring || isMetricsExplorer) {
|
||||
setSearchValue(value);
|
||||
} else {
|
||||
handleSearch(value);
|
||||
@@ -240,8 +243,11 @@ function QueryBuilderSearch({
|
||||
);
|
||||
|
||||
const isMetricsDataSource = useMemo(
|
||||
() => query.dataSource === DataSource.METRICS && !isInfraMonitoring,
|
||||
[query.dataSource, isInfraMonitoring],
|
||||
() =>
|
||||
query.dataSource === DataSource.METRICS &&
|
||||
!isInfraMonitoring &&
|
||||
!isMetricsExplorer,
|
||||
[query.dataSource, isInfraMonitoring, isMetricsExplorer],
|
||||
);
|
||||
|
||||
const fetchValueDataType = (value: unknown, operator: string): DataTypes => {
|
||||
@@ -291,8 +297,8 @@ function QueryBuilderSearch({
|
||||
};
|
||||
});
|
||||
|
||||
// If in infra monitoring, only run the onChange query when editing is finsished.
|
||||
if (isInfraMonitoring) {
|
||||
// If in infra monitoring or metrics explorer, only run the onChange query when editing is finsished.
|
||||
if (isInfraMonitoring || isMetricsExplorer) {
|
||||
if (!isEditingTag) {
|
||||
onChange(initialTagFilters);
|
||||
}
|
||||
@@ -498,6 +504,7 @@ interface QueryBuilderSearchProps {
|
||||
isInfraMonitoring?: boolean;
|
||||
disableNavigationShortcuts?: boolean;
|
||||
entity?: K8sCategory | null;
|
||||
isMetricsExplorer?: boolean;
|
||||
}
|
||||
|
||||
QueryBuilderSearch.defaultProps = {
|
||||
@@ -508,6 +515,7 @@ QueryBuilderSearch.defaultProps = {
|
||||
isInfraMonitoring: false,
|
||||
disableNavigationShortcuts: false,
|
||||
entity: null,
|
||||
isMetricsExplorer: false,
|
||||
};
|
||||
|
||||
export interface CustomTagProps {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import { MAX_RPS_LIMIT } from 'constants/global';
|
||||
import ResourceAttributesFilter from 'container/ResourceAttributesFilter';
|
||||
import { useGetQueriesRange } from 'hooks/queryBuilder/useGetQueriesRange';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
@@ -14,7 +15,6 @@ import { useLocation } from 'react-router-dom';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { ServicesList } from 'types/api/metrics/getService';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { isCloudUser } from 'utils/app';
|
||||
import { getTotalRPS } from 'utils/services';
|
||||
|
||||
import { getColumns } from '../Columns/ServiceColumn';
|
||||
@@ -34,7 +34,7 @@ function ServiceMetricTable({
|
||||
const { t: getText } = useTranslation(['services']);
|
||||
|
||||
const { licenses, isFetchingLicenses } = useAppContext();
|
||||
const isCloudUserVal = isCloudUser();
|
||||
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
|
||||
|
||||
const queries = useGetQueriesRange(queryRangeRequestData, ENTITY_VERSION_V4, {
|
||||
queryKey: [
|
||||
|
||||
@@ -3,11 +3,11 @@ import { Flex, Typography } from 'antd';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
import { MAX_RPS_LIMIT } from 'constants/global';
|
||||
import ResourceAttributesFilter from 'container/ResourceAttributesFilter';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { isCloudUser } from 'utils/app';
|
||||
import { getTotalRPS } from 'utils/services';
|
||||
|
||||
import { getColumns } from '../Columns/ServiceColumn';
|
||||
@@ -22,7 +22,7 @@ function ServiceTraceTable({
|
||||
const { t: getText } = useTranslation(['services']);
|
||||
|
||||
const { licenses, isFetchingLicenses } = useAppContext();
|
||||
const isCloudUserVal = isCloudUser();
|
||||
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
|
||||
const tableColumns = useMemo(() => getColumns(search, false), [search]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -11,6 +11,7 @@ import ROUTES from 'constants/routes';
|
||||
import { GlobalShortcuts } from 'constants/shortcuts/globalShortcuts';
|
||||
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { LICENSE_PLAN_KEY, LICENSE_PLAN_STATUS } from 'hooks/useLicense';
|
||||
import history from 'lib/history';
|
||||
import {
|
||||
@@ -28,7 +29,7 @@ import { AppState } from 'store/reducers';
|
||||
import { License } from 'types/api/licenses/def';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
import { checkVersionState, isCloudUser, isEECloudUser } from 'utils/app';
|
||||
import { checkVersionState } from 'utils/app';
|
||||
|
||||
import { routeConfig } from './config';
|
||||
import { getQueryString } from './helper';
|
||||
@@ -86,7 +87,10 @@ function SideNav(): JSX.Element {
|
||||
|
||||
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
|
||||
|
||||
const isCloudUserVal = isCloudUser();
|
||||
const {
|
||||
isCloudUser: isCloudUserVal,
|
||||
isEECloudUser: isEECloudUserVal,
|
||||
} = useGetTenantLicense();
|
||||
|
||||
const { t } = useTranslation('');
|
||||
|
||||
@@ -275,7 +279,7 @@ function SideNav(): JSX.Element {
|
||||
let updatedUserManagementItems: UserManagementMenuItems[] = [
|
||||
manageLicenseMenuItem,
|
||||
];
|
||||
if (isCloudUserVal || isEECloudUser()) {
|
||||
if (isCloudUserVal || isEECloudUserVal) {
|
||||
const isOnboardingEnabled =
|
||||
featureFlags?.find((feature) => feature.name === FeatureKeys.ONBOARDING)
|
||||
?.active || false;
|
||||
@@ -330,6 +334,7 @@ function SideNav(): JSX.Element {
|
||||
featureFlags,
|
||||
isCloudUserVal,
|
||||
isCurrentVersionError,
|
||||
isEECloudUserVal,
|
||||
isLatestVersion,
|
||||
licenses?.licenses,
|
||||
onClickVersionHandler,
|
||||
|
||||
@@ -26,6 +26,8 @@ import { popupContainer } from 'utils/selectPopupContainer';
|
||||
import { getMinMax, options } from './config';
|
||||
import { ButtonContainer } from './styles';
|
||||
|
||||
const DEFAULT_REFRESH_INTERVAL = '30s';
|
||||
|
||||
function AutoRefresh({
|
||||
disabled = false,
|
||||
showAutoRefreshBtnPrimary = true,
|
||||
@@ -67,13 +69,18 @@ function AutoRefresh({
|
||||
|
||||
const params = useUrlQuery();
|
||||
|
||||
const defaultOption = useMemo(
|
||||
() => options.find((option) => option.key === DEFAULT_REFRESH_INTERVAL),
|
||||
[],
|
||||
);
|
||||
|
||||
const [selectedOption, setSelectedOption] = useState<string>(
|
||||
localStorageValue || options[0].key,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedOption(localStorageValue || options[0].key);
|
||||
}, [localStorageValue, params]);
|
||||
}, [localStorageValue, params, defaultOption]);
|
||||
|
||||
const getOption = useMemo(
|
||||
() => options.find((option) => option.key === selectedOption),
|
||||
@@ -127,10 +134,18 @@ function AutoRefresh({
|
||||
DASHBOARD_TIME_IN_DURATION,
|
||||
JSON.stringify(_omit(localStorageData, pathname)),
|
||||
);
|
||||
} else {
|
||||
// When enabling auto-refresh, set to DEFAULT_REFRESH_INTERVAL if no previous preference
|
||||
const refreshInterval = localStorageValue || defaultOption?.key;
|
||||
set(
|
||||
DASHBOARD_TIME_IN_DURATION,
|
||||
JSON.stringify({ ...localStorageData, [pathname]: refreshInterval }),
|
||||
);
|
||||
setSelectedOption(refreshInterval);
|
||||
}
|
||||
setIsAutoRefreshfreshEnabled(checked);
|
||||
},
|
||||
[localStorageData, pathname],
|
||||
[localStorageData, pathname, localStorageValue, defaultOption],
|
||||
);
|
||||
|
||||
if (globalTime.selectedTime === 'custom') {
|
||||
|
||||
@@ -30,7 +30,7 @@ import getTimeString from 'lib/getTimeString';
|
||||
import { isObject } from 'lodash-es';
|
||||
import { Check, Copy, Info, Send, Undo } from 'lucide-react';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { connect, useDispatch, useSelector } from 'react-redux';
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
@@ -314,11 +314,6 @@ function DateTimeSelection({
|
||||
return `Refreshed ${secondsDiff} sec ago`;
|
||||
}, [maxTime, minTime, selectedTime]);
|
||||
|
||||
const isLogsExplorerPage = useMemo(
|
||||
() => location.pathname === ROUTES.LOGS_EXPLORER,
|
||||
[location.pathname],
|
||||
);
|
||||
|
||||
const onSelectHandler = useCallback(
|
||||
(value: Time | CustomTimeType): void => {
|
||||
if (isModalTimeSelection) {
|
||||
@@ -347,15 +342,13 @@ function DateTimeSelection({
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isLogsExplorerPage) {
|
||||
urlQuery.delete('startTime');
|
||||
urlQuery.delete('endTime');
|
||||
urlQuery.delete('startTime');
|
||||
urlQuery.delete('endTime');
|
||||
|
||||
urlQuery.set(QueryParams.relativeTime, value);
|
||||
urlQuery.set(QueryParams.relativeTime, value);
|
||||
|
||||
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
|
||||
safeNavigate(generatedUrl);
|
||||
}
|
||||
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
|
||||
safeNavigate(generatedUrl);
|
||||
|
||||
// For logs explorer - time range handling is managed in useCopyLogLink.ts:52
|
||||
|
||||
@@ -368,7 +361,6 @@ function DateTimeSelection({
|
||||
},
|
||||
[
|
||||
initQueryBuilderData,
|
||||
isLogsExplorerPage,
|
||||
isModalTimeSelection,
|
||||
location.pathname,
|
||||
onTimeChange,
|
||||
@@ -438,16 +430,14 @@ function DateTimeSelection({
|
||||
|
||||
updateLocalStorageForRoutes(JSON.stringify({ startTime, endTime }));
|
||||
|
||||
if (!isLogsExplorerPage) {
|
||||
urlQuery.set(
|
||||
QueryParams.startTime,
|
||||
startTime?.toDate().getTime().toString(),
|
||||
);
|
||||
urlQuery.set(QueryParams.endTime, endTime?.toDate().getTime().toString());
|
||||
urlQuery.delete(QueryParams.relativeTime);
|
||||
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
|
||||
safeNavigate(generatedUrl);
|
||||
}
|
||||
urlQuery.set(
|
||||
QueryParams.startTime,
|
||||
startTime?.toDate().getTime().toString(),
|
||||
);
|
||||
urlQuery.set(QueryParams.endTime, endTime?.toDate().getTime().toString());
|
||||
urlQuery.delete(QueryParams.relativeTime);
|
||||
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
|
||||
safeNavigate(generatedUrl);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -466,15 +456,13 @@ function DateTimeSelection({
|
||||
|
||||
setIsValidteRelativeTime(true);
|
||||
|
||||
if (!isLogsExplorerPage) {
|
||||
urlQuery.delete('startTime');
|
||||
urlQuery.delete('endTime');
|
||||
urlQuery.delete('startTime');
|
||||
urlQuery.delete('endTime');
|
||||
|
||||
urlQuery.set(QueryParams.relativeTime, dateTimeStr);
|
||||
urlQuery.set(QueryParams.relativeTime, dateTimeStr);
|
||||
|
||||
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
|
||||
safeNavigate(generatedUrl);
|
||||
}
|
||||
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
|
||||
safeNavigate(generatedUrl);
|
||||
|
||||
if (!stagedQuery) {
|
||||
return;
|
||||
|
||||
@@ -5,7 +5,6 @@ import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
export type LogTimeRange = {
|
||||
start: number;
|
||||
end: number;
|
||||
pageSize: number;
|
||||
};
|
||||
|
||||
export type UseCopyLogLink = {
|
||||
@@ -13,7 +12,6 @@ export type UseCopyLogLink = {
|
||||
isLogsExplorerPage: boolean;
|
||||
activeLogId: string | null;
|
||||
onLogCopy: MouseEventHandler<HTMLElement>;
|
||||
onTimeRangeChange: (newTimeRange: LogTimeRange | null) => void;
|
||||
};
|
||||
|
||||
export type UseActiveLog = {
|
||||
|
||||
@@ -3,7 +3,6 @@ import ROUTES from 'constants/routes';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||
import history from 'lib/history';
|
||||
import {
|
||||
MouseEventHandler,
|
||||
useCallback,
|
||||
@@ -18,7 +17,7 @@ import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { HIGHLIGHTED_DELAY } from './configs';
|
||||
import { LogTimeRange, UseCopyLogLink } from './types';
|
||||
import { UseCopyLogLink } from './types';
|
||||
|
||||
export const useCopyLogLink = (logId?: string): UseCopyLogLink => {
|
||||
const urlQuery = useUrlQuery();
|
||||
@@ -31,27 +30,8 @@ export const useCopyLogLink = (logId?: string): UseCopyLogLink => {
|
||||
null,
|
||||
);
|
||||
|
||||
const { selectedTime, minTime, maxTime } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
|
||||
const onTimeRangeChange = useCallback(
|
||||
(newTimeRange: LogTimeRange | null): void => {
|
||||
if (selectedTime !== 'custom') {
|
||||
urlQuery.delete(QueryParams.startTime);
|
||||
urlQuery.delete(QueryParams.endTime);
|
||||
urlQuery.set(QueryParams.relativeTime, selectedTime);
|
||||
} else {
|
||||
urlQuery.set(QueryParams.startTime, newTimeRange?.start.toString() || '');
|
||||
urlQuery.set(QueryParams.endTime, newTimeRange?.end.toString() || '');
|
||||
urlQuery.delete(QueryParams.relativeTime);
|
||||
}
|
||||
|
||||
const generatedUrl = `${pathname}?${urlQuery.toString()}`;
|
||||
history.replace(generatedUrl);
|
||||
},
|
||||
[pathname, urlQuery, selectedTime],
|
||||
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const isActiveLog = useMemo(() => activeLogId === logId, [activeLogId, logId]);
|
||||
@@ -101,6 +81,5 @@ export const useCopyLogLink = (logId?: string): UseCopyLogLink => {
|
||||
isLogsExplorerPage,
|
||||
activeLogId,
|
||||
onLogCopy,
|
||||
onTimeRangeChange,
|
||||
};
|
||||
};
|
||||
|
||||
47
frontend/src/hooks/metricsExplorer/useGetMetricsList.ts
Normal file
47
frontend/src/hooks/metricsExplorer/useGetMetricsList.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import {
|
||||
getMetricsList,
|
||||
MetricsListPayload,
|
||||
MetricsListResponse,
|
||||
} from 'api/metricsExplorer/getMetricsList';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { useMemo } from 'react';
|
||||
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
|
||||
type UseGetMetricsList = (
|
||||
requestData: MetricsListPayload,
|
||||
|
||||
options?: UseQueryOptions<
|
||||
SuccessResponse<MetricsListResponse> | ErrorResponse,
|
||||
Error
|
||||
>,
|
||||
|
||||
headers?: Record<string, string>,
|
||||
) => UseQueryResult<
|
||||
SuccessResponse<MetricsListResponse> | ErrorResponse,
|
||||
Error
|
||||
>;
|
||||
|
||||
export const useGetMetricsList: UseGetMetricsList = (
|
||||
requestData,
|
||||
options,
|
||||
headers,
|
||||
) => {
|
||||
const queryKey = useMemo(() => {
|
||||
if (options?.queryKey && Array.isArray(options.queryKey)) {
|
||||
return [...options.queryKey];
|
||||
}
|
||||
|
||||
if (options?.queryKey && typeof options.queryKey === 'string') {
|
||||
return options.queryKey;
|
||||
}
|
||||
|
||||
return [REACT_QUERY_KEY.GET_METRICS_LIST, requestData];
|
||||
}, [options?.queryKey, requestData]);
|
||||
|
||||
return useQuery<SuccessResponse<MetricsListResponse> | ErrorResponse, Error>({
|
||||
queryFn: ({ signal }) => getMetricsList(requestData, signal, headers),
|
||||
...options,
|
||||
queryKey,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
import {
|
||||
getMetricsListFilterKeys,
|
||||
MetricsListFilterKeysResponse,
|
||||
} from 'api/metricsExplorer/getMetricsListFilterKeys';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { useMemo } from 'react';
|
||||
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
|
||||
type UseGetMetricsListFilterKeys = (
|
||||
options?: UseQueryOptions<
|
||||
SuccessResponse<MetricsListFilterKeysResponse> | ErrorResponse,
|
||||
Error
|
||||
>,
|
||||
headers?: Record<string, string>,
|
||||
) => UseQueryResult<
|
||||
SuccessResponse<MetricsListFilterKeysResponse> | ErrorResponse,
|
||||
Error
|
||||
>;
|
||||
|
||||
export const useGetMetricsListFilterKeys: UseGetMetricsListFilterKeys = (
|
||||
options,
|
||||
headers,
|
||||
) => {
|
||||
const queryKey = useMemo(() => {
|
||||
if (options?.queryKey && Array.isArray(options.queryKey)) {
|
||||
return [...options.queryKey];
|
||||
}
|
||||
|
||||
if (options?.queryKey && typeof options.queryKey === 'string') {
|
||||
return options.queryKey;
|
||||
}
|
||||
|
||||
return [REACT_QUERY_KEY.GET_METRICS_LIST_FILTER_KEYS];
|
||||
}, [options?.queryKey]);
|
||||
|
||||
return useQuery<
|
||||
SuccessResponse<MetricsListFilterKeysResponse> | ErrorResponse,
|
||||
Error
|
||||
>({
|
||||
queryFn: ({ signal }) => getMetricsListFilterKeys(signal, headers),
|
||||
...options,
|
||||
queryKey,
|
||||
});
|
||||
};
|
||||
50
frontend/src/hooks/metricsExplorer/useGetMetricsTreeMap.ts
Normal file
50
frontend/src/hooks/metricsExplorer/useGetMetricsTreeMap.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import {
|
||||
getMetricsTreeMap,
|
||||
MetricsTreeMapPayload,
|
||||
MetricsTreeMapResponse,
|
||||
} from 'api/metricsExplorer/getMetricsTreeMap';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { useMemo } from 'react';
|
||||
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
|
||||
type UseGetMetricsTreeMap = (
|
||||
requestData: MetricsTreeMapPayload,
|
||||
|
||||
options?: UseQueryOptions<
|
||||
SuccessResponse<MetricsTreeMapResponse> | ErrorResponse,
|
||||
Error
|
||||
>,
|
||||
|
||||
headers?: Record<string, string>,
|
||||
) => UseQueryResult<
|
||||
SuccessResponse<MetricsTreeMapResponse> | ErrorResponse,
|
||||
Error
|
||||
>;
|
||||
|
||||
export const useGetMetricsTreeMap: UseGetMetricsTreeMap = (
|
||||
requestData,
|
||||
options,
|
||||
headers,
|
||||
) => {
|
||||
const queryKey = useMemo(() => {
|
||||
if (options?.queryKey && Array.isArray(options.queryKey)) {
|
||||
return [...options.queryKey];
|
||||
}
|
||||
|
||||
if (options?.queryKey && typeof options.queryKey === 'string') {
|
||||
return options.queryKey;
|
||||
}
|
||||
|
||||
return [REACT_QUERY_KEY.GET_METRICS_TREE_MAP, requestData];
|
||||
}, [options?.queryKey, requestData]);
|
||||
|
||||
return useQuery<
|
||||
SuccessResponse<MetricsTreeMapResponse> | ErrorResponse,
|
||||
Error
|
||||
>({
|
||||
queryFn: ({ signal }) => getMetricsTreeMap(requestData, signal, headers),
|
||||
...options,
|
||||
queryKey,
|
||||
});
|
||||
};
|
||||
@@ -31,6 +31,7 @@ export const useAutoComplete = (
|
||||
shouldUseSuggestions?: boolean,
|
||||
isInfraMonitoring?: boolean,
|
||||
entity?: K8sCategory | null,
|
||||
isMetricsExplorer?: boolean,
|
||||
): IAutoComplete => {
|
||||
const [searchValue, setSearchValue] = useState<string>('');
|
||||
const [searchKey, setSearchKey] = useState<string>('');
|
||||
@@ -42,6 +43,7 @@ export const useAutoComplete = (
|
||||
shouldUseSuggestions,
|
||||
isInfraMonitoring,
|
||||
entity,
|
||||
isMetricsExplorer,
|
||||
);
|
||||
|
||||
const [key, operator, result] = useSetCurrentKeyAndOperator(searchValue, keys);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import { getMetricsListFilterValues } from 'api/metricsExplorer/getMetricsListFilterValues';
|
||||
import { getAttributesValues } from 'api/queryBuilder/getAttributesValues';
|
||||
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
|
||||
import {
|
||||
@@ -10,6 +11,7 @@ import {
|
||||
getTagToken,
|
||||
isInNInOperator,
|
||||
} from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
||||
import { useGetMetricsListFilterKeys } from 'hooks/metricsExplorer/useGetMetricsListFilterKeys';
|
||||
import useDebounceValue from 'hooks/useDebounce';
|
||||
import { cloneDeep, isEqual, uniqWith, unset } from 'lodash-es';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
@@ -50,6 +52,7 @@ export const useFetchKeysAndValues = (
|
||||
shouldUseSuggestions?: boolean,
|
||||
isInfraMonitoring?: boolean,
|
||||
entity?: K8sCategory | null,
|
||||
isMetricsExplorer?: boolean,
|
||||
): IuseFetchKeysAndValues => {
|
||||
const [keys, setKeys] = useState<BaseAutocompleteData[]>([]);
|
||||
const [exampleQueries, setExampleQueries] = useState<TagFilter[]>([]);
|
||||
@@ -98,10 +101,17 @@ export const useFetchKeysAndValues = (
|
||||
|
||||
const isQueryEnabled = useMemo(
|
||||
() =>
|
||||
query.dataSource === DataSource.METRICS && !isInfraMonitoring
|
||||
query.dataSource === DataSource.METRICS &&
|
||||
!isInfraMonitoring &&
|
||||
!isMetricsExplorer
|
||||
? !!query.dataSource && !!query.aggregateAttribute.dataType
|
||||
: true,
|
||||
[isInfraMonitoring, query.aggregateAttribute.dataType, query.dataSource],
|
||||
[
|
||||
isInfraMonitoring,
|
||||
isMetricsExplorer,
|
||||
query.aggregateAttribute.dataType,
|
||||
query.dataSource,
|
||||
],
|
||||
);
|
||||
|
||||
const { data, isFetching, status } = useGetAggregateKeys(
|
||||
@@ -139,6 +149,14 @@ export const useFetchKeysAndValues = (
|
||||
},
|
||||
);
|
||||
|
||||
const {
|
||||
data: metricsListFilterKeysData,
|
||||
isFetching: isFetchingMetricsListFilterKeys,
|
||||
status: fetchingMetricsListFilterKeysStatus,
|
||||
} = useGetMetricsListFilterKeys({
|
||||
enabled: isMetricsExplorer && isQueryEnabled && !shouldUseSuggestions,
|
||||
});
|
||||
|
||||
/**
|
||||
* Fetches the options to be displayed based on the selected value
|
||||
* @param value - the selected value
|
||||
@@ -182,6 +200,15 @@ export const useFetchKeysAndValues = (
|
||||
: tagValue?.toString() ?? '',
|
||||
});
|
||||
payload = response.payload;
|
||||
} else if (isMetricsExplorer) {
|
||||
const response = await getMetricsListFilterValues({
|
||||
searchText: searchKey,
|
||||
filterKey: filterAttributeKey?.key ?? tagKey,
|
||||
filterAttributeKeyDataType:
|
||||
filterAttributeKey?.dataType ?? DataTypes.EMPTY,
|
||||
limit: 10,
|
||||
});
|
||||
payload = response.payload?.data;
|
||||
} else {
|
||||
const response = await getAttributesValues({
|
||||
aggregateOperator: query.aggregateOperator,
|
||||
@@ -238,6 +265,32 @@ export const useFetchKeysAndValues = (
|
||||
}
|
||||
}, [data?.payload?.attributeKeys, status]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
isMetricsExplorer &&
|
||||
fetchingMetricsListFilterKeysStatus === 'success' &&
|
||||
!isFetchingMetricsListFilterKeys &&
|
||||
metricsListFilterKeysData?.payload?.data?.attributeKeys
|
||||
) {
|
||||
setKeys(metricsListFilterKeysData.payload.data.attributeKeys);
|
||||
setSourceKeys((prevState) =>
|
||||
uniqWith(
|
||||
[
|
||||
...(metricsListFilterKeysData.payload.data.attributeKeys ?? []),
|
||||
...prevState,
|
||||
],
|
||||
isEqual,
|
||||
),
|
||||
);
|
||||
}
|
||||
}, [
|
||||
metricsListFilterKeysData?.payload?.data?.attributeKeys,
|
||||
fetchingMetricsListFilterKeysStatus,
|
||||
isMetricsExplorer,
|
||||
metricsListFilterKeysData,
|
||||
isFetchingMetricsListFilterKeys,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
fetchingSuggestionsStatus === 'success' &&
|
||||
|
||||
15
frontend/src/hooks/useGetTenantLicense.ts
Normal file
15
frontend/src/hooks/useGetTenantLicense.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { LicensePlatform } from 'types/api/licensesV3/getActive';
|
||||
|
||||
export const useGetTenantLicense = (): {
|
||||
isCloudUser: boolean;
|
||||
isEECloudUser: boolean;
|
||||
} => {
|
||||
const { activeLicenseV3 } = useAppContext();
|
||||
|
||||
return {
|
||||
isCloudUser: activeLicenseV3?.platform === LicensePlatform.CLOUD || false,
|
||||
isEECloudUser:
|
||||
activeLicenseV3?.platform === LicensePlatform.SELF_HOSTED || false,
|
||||
};
|
||||
};
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
import { QueryDataV3 } from 'types/api/widgets/getQuery';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { LogTimeRange } from './logs/types';
|
||||
import { useCopyLogLink } from './logs/useCopyLogLink';
|
||||
import { useGetExplorerQueryRange } from './queryBuilder/useGetExplorerQueryRange';
|
||||
import useUrlQueryData from './useUrlQueryData';
|
||||
@@ -129,7 +128,7 @@ export const useLogsData = ({
|
||||
return data;
|
||||
};
|
||||
|
||||
const { activeLogId, onTimeRangeChange } = useCopyLogLink();
|
||||
const { activeLogId } = useCopyLogLink();
|
||||
|
||||
const { data, isFetching } = useGetExplorerQueryRange(
|
||||
requestData,
|
||||
@@ -150,7 +149,6 @@ export const useLogsData = ({
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const currentParams = data?.params as Omit<LogTimeRange, 'pageSize'>;
|
||||
const currentData = data?.payload?.data?.newResult?.data?.result || [];
|
||||
if (currentData.length > 0 && currentData[0].list) {
|
||||
const currentLogs: ILog[] = currentData[0].list.map((item) => ({
|
||||
@@ -160,11 +158,6 @@ export const useLogsData = ({
|
||||
const newLogs = [...logs, ...currentLogs];
|
||||
|
||||
setLogs(newLogs);
|
||||
onTimeRangeChange({
|
||||
start: currentParams?.start,
|
||||
end: currentParams?.end,
|
||||
pageSize: newLogs.length,
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
||||
@@ -12,6 +12,10 @@ interface SafeNavigateParams {
|
||||
search?: string;
|
||||
}
|
||||
|
||||
interface UseSafeNavigateProps {
|
||||
preventSameUrlNavigation?: boolean;
|
||||
}
|
||||
|
||||
const areUrlsEffectivelySame = (url1: URL, url2: URL): boolean => {
|
||||
if (url1.pathname !== url2.pathname) return false;
|
||||
|
||||
@@ -78,7 +82,11 @@ const isDefaultNavigation = (currentUrl: URL, targetUrl: URL): boolean => {
|
||||
|
||||
return newKeys.length > 0;
|
||||
};
|
||||
export const useSafeNavigate = (): {
|
||||
export const useSafeNavigate = (
|
||||
{ preventSameUrlNavigation }: UseSafeNavigateProps = {
|
||||
preventSameUrlNavigation: true,
|
||||
},
|
||||
): {
|
||||
safeNavigate: (
|
||||
to: string | SafeNavigateParams,
|
||||
options?: NavigateOptions,
|
||||
@@ -108,7 +116,7 @@ export const useSafeNavigate = (): {
|
||||
const urlsAreSame = areUrlsEffectivelySame(currentUrl, targetUrl);
|
||||
const isDefaultParamsNavigation = isDefaultNavigation(currentUrl, targetUrl);
|
||||
|
||||
if (urlsAreSame) {
|
||||
if (preventSameUrlNavigation && urlsAreSame) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -129,7 +137,7 @@ export const useSafeNavigate = (): {
|
||||
);
|
||||
}
|
||||
},
|
||||
[navigate, location.pathname, location.search],
|
||||
[navigate, location.pathname, location.search, preventSameUrlNavigation],
|
||||
);
|
||||
|
||||
return { safeNavigate };
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import './DashboardsListPage.styles.scss';
|
||||
|
||||
import { Space, Typography } from 'antd';
|
||||
import ReleaseNote from 'components/ReleaseNote';
|
||||
import ListOfAllDashboard from 'container/ListOfDashboard';
|
||||
import { LayoutGrid } from 'lucide-react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
function DashboardsListPage(): JSX.Element {
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<Space
|
||||
direction="vertical"
|
||||
@@ -16,7 +12,6 @@ function DashboardsListPage(): JSX.Element {
|
||||
style={{ width: '100%' }}
|
||||
className="dashboard-list-page"
|
||||
>
|
||||
<ReleaseNote path={location.pathname} />
|
||||
<div className="dashboard-header">
|
||||
<LayoutGrid size={14} className="icon" />
|
||||
<Typography.Text className="text">Dashboards</Typography.Text>
|
||||
|
||||
@@ -7,9 +7,9 @@ import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Flex, Skeleton, Typography } from 'antd';
|
||||
import { useGetIntegration } from 'hooks/Integrations/useGetIntegration';
|
||||
import { useGetIntegrationStatus } from 'hooks/Integrations/useGetIntegrationStatus';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { defaultTo } from 'lodash-es';
|
||||
import { ArrowLeft, MoveUpRight, RotateCw } from 'lucide-react';
|
||||
import { isCloudUser } from 'utils/app';
|
||||
|
||||
import { handleContactSupport } from '../utils';
|
||||
import IntegrationDetailContent from './IntegrationDetailContent';
|
||||
@@ -44,6 +44,8 @@ function IntegrationDetailPage(props: IntegrationDetailPageProps): JSX.Element {
|
||||
integrationId: selectedIntegration,
|
||||
});
|
||||
|
||||
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
|
||||
|
||||
const {
|
||||
data: integrationStatus,
|
||||
isLoading: isStatusLoading,
|
||||
@@ -104,7 +106,7 @@ function IntegrationDetailPage(props: IntegrationDetailPageProps): JSX.Element {
|
||||
</Button>
|
||||
<div
|
||||
className="contact-support"
|
||||
onClick={(): void => handleContactSupport(isCloudUser())}
|
||||
onClick={(): void => handleContactSupport(isCloudUserVal)}
|
||||
>
|
||||
<Typography.Link className="text">Contact Support </Typography.Link>
|
||||
|
||||
|
||||
@@ -5,10 +5,10 @@ import './Integrations.styles.scss';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, List, Typography } from 'antd';
|
||||
import { useGetAllIntegrations } from 'hooks/Integrations/useGetAllIntegrations';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { MoveUpRight, RotateCw } from 'lucide-react';
|
||||
import { Dispatch, SetStateAction, useMemo } from 'react';
|
||||
import { IntegrationsProps } from 'types/api/integrations/types';
|
||||
import { isCloudUser } from 'utils/app';
|
||||
|
||||
import { handleContactSupport, INTEGRATION_TYPES } from './utils';
|
||||
|
||||
@@ -44,6 +44,8 @@ function IntegrationsList(props: IntegrationsListProps): JSX.Element {
|
||||
refetch,
|
||||
} = useGetAllIntegrations();
|
||||
|
||||
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
|
||||
|
||||
const filteredDataList = useMemo(() => {
|
||||
let integrationsList: IntegrationsProps[] = [];
|
||||
|
||||
@@ -90,7 +92,7 @@ function IntegrationsList(props: IntegrationsListProps): JSX.Element {
|
||||
</Button>
|
||||
<div
|
||||
className="contact-support"
|
||||
onClick={(): void => handleContactSupport(isCloudUser())}
|
||||
onClick={(): void => handleContactSupport(isCloudUserVal)}
|
||||
>
|
||||
<Typography.Link className="text">Contact Support </Typography.Link>
|
||||
|
||||
|
||||
@@ -8,10 +8,10 @@ import MessagingQueueHealthCheck from 'components/MessagingQueueHealthCheck/Mess
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { isCloudUser } from 'utils/app';
|
||||
|
||||
import {
|
||||
KAFKA_SETUP_DOC_LINK,
|
||||
@@ -34,7 +34,7 @@ function MessagingQueues(): JSX.Element {
|
||||
);
|
||||
};
|
||||
|
||||
const isCloudUserVal = isCloudUser();
|
||||
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
|
||||
|
||||
const getStartedRedirect = (link: string, sourceCard: string): void => {
|
||||
logEvent('Messaging Queues: Get started clicked', {
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
import { Space } from 'antd';
|
||||
import ReleaseNote from 'components/ReleaseNote';
|
||||
import ServicesApplication from 'container/ServiceApplication';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
function Metrics(): JSX.Element {
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<ReleaseNote path={location.pathname} />
|
||||
|
||||
<ServicesApplication />
|
||||
</Space>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import RouteTab from 'components/RouteTab';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import history from 'lib/history';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useMemo } from 'react';
|
||||
@@ -12,6 +13,10 @@ import { getRoutes } from './utils';
|
||||
function SettingsPage(): JSX.Element {
|
||||
const { pathname } = useLocation();
|
||||
const { user, featureFlags, licenses } = useAppContext();
|
||||
const {
|
||||
isCloudUser: isCloudAccount,
|
||||
isEECloudUser: isEECloudAccount,
|
||||
} = useGetTenantLicense();
|
||||
|
||||
const isWorkspaceBlocked = licenses?.workSpaceBlock || false;
|
||||
|
||||
@@ -32,9 +37,19 @@ function SettingsPage(): JSX.Element {
|
||||
isCurrentOrgSettings,
|
||||
isGatewayEnabled,
|
||||
isWorkspaceBlocked,
|
||||
isCloudAccount,
|
||||
isEECloudAccount,
|
||||
t,
|
||||
),
|
||||
[user.role, isCurrentOrgSettings, isGatewayEnabled, isWorkspaceBlocked, t],
|
||||
[
|
||||
user.role,
|
||||
isCurrentOrgSettings,
|
||||
isGatewayEnabled,
|
||||
isWorkspaceBlocked,
|
||||
isCloudAccount,
|
||||
isEECloudAccount,
|
||||
t,
|
||||
],
|
||||
);
|
||||
|
||||
return <RouteTab routes={routes} activeKey={pathname} history={history} />;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { RouteTabProps } from 'components/RouteTab/types';
|
||||
import { TFunction } from 'i18next';
|
||||
import { ROLES, USER_ROLES } from 'types/roles';
|
||||
import { isCloudUser, isEECloudUser } from 'utils/app';
|
||||
|
||||
import {
|
||||
alertChannels,
|
||||
@@ -18,13 +17,12 @@ export const getRoutes = (
|
||||
isCurrentOrgSettings: boolean,
|
||||
isGatewayEnabled: boolean,
|
||||
isWorkspaceBlocked: boolean,
|
||||
isCloudAccount: boolean,
|
||||
isEECloudAccount: boolean,
|
||||
t: TFunction,
|
||||
): RouteTabProps['routes'] => {
|
||||
const settings = [];
|
||||
|
||||
const isCloudAccount = isCloudUser();
|
||||
const isEECloudAccount = isEECloudUser();
|
||||
|
||||
const isAdmin = userRole === USER_ROLES.ADMIN;
|
||||
const isEditor = userRole === USER_ROLES.EDITOR;
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import './NoData.styles.scss';
|
||||
|
||||
import { Button, Typography } from 'antd';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { LifeBuoy, RefreshCw } from 'lucide-react';
|
||||
import { handleContactSupport } from 'pages/Integrations/utils';
|
||||
import { isCloudUser } from 'utils/app';
|
||||
|
||||
function NoData(): JSX.Element {
|
||||
const isCloudUserVal = isCloudUser();
|
||||
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
|
||||
|
||||
return (
|
||||
<div className="not-found-trace">
|
||||
<section className="description">
|
||||
|
||||
@@ -21,7 +21,6 @@ import { FeatureFlagProps as FeatureFlags } from 'types/api/features/getFeatures
|
||||
import { PayloadProps as LicensesResModel } from 'types/api/licenses/getAll';
|
||||
import { LicenseV3ResModel } from 'types/api/licensesV3/getActive';
|
||||
import { Organization } from 'types/api/user/getOrganization';
|
||||
import { UserFlags } from 'types/api/user/setFlags';
|
||||
import { OrgPreference } from 'types/reducer/app';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
|
||||
@@ -158,13 +157,6 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
|
||||
}
|
||||
}, [orgPreferencesData, isFetchingOrgPreferences]);
|
||||
|
||||
function setUserFlags(userflags: UserFlags): void {
|
||||
setUser((prev) => ({
|
||||
...prev,
|
||||
flags: userflags,
|
||||
}));
|
||||
}
|
||||
|
||||
function updateUser(user: IUser): void {
|
||||
setUser((prev) => ({
|
||||
...prev,
|
||||
@@ -252,7 +244,6 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
|
||||
orgPreferencesFetchError,
|
||||
licensesRefetch,
|
||||
updateUser,
|
||||
setUserFlags,
|
||||
updateOrgPreferences,
|
||||
updateOrg,
|
||||
}),
|
||||
|
||||
@@ -3,7 +3,6 @@ import { PayloadProps as LicensesResModel } from 'types/api/licenses/getAll';
|
||||
import { LicenseV3ResModel } from 'types/api/licensesV3/getActive';
|
||||
import { Organization } from 'types/api/user/getOrganization';
|
||||
import { PayloadProps as User } from 'types/api/user/getUser';
|
||||
import { UserFlags } from 'types/api/user/setFlags';
|
||||
import { OrgPreference } from 'types/reducer/app';
|
||||
|
||||
export interface IAppContext {
|
||||
@@ -26,7 +25,6 @@ export interface IAppContext {
|
||||
orgPreferencesFetchError: unknown;
|
||||
licensesRefetch: () => void;
|
||||
updateUser: (user: IUser) => void;
|
||||
setUserFlags: (flags: UserFlags) => void;
|
||||
updateOrgPreferences: (orgPreferences: OrgPreference[]) => void;
|
||||
updateOrg(orgId: string, updatedOrgName: string): void;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ function getUserDefaults(): IUser {
|
||||
name: '',
|
||||
profilePictureURL: '',
|
||||
createdAt: 0,
|
||||
flags: {},
|
||||
organization: '',
|
||||
orgId: '',
|
||||
role: 'VIEWER',
|
||||
|
||||
@@ -763,7 +763,12 @@ export function QueryBuilderProvider({
|
||||
[panelType, stagedQuery],
|
||||
);
|
||||
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const { safeNavigate } = useSafeNavigate({
|
||||
preventSameUrlNavigation: !(
|
||||
initialDataSource === DataSource.LOGS ||
|
||||
initialDataSource === DataSource.TRACES
|
||||
),
|
||||
});
|
||||
|
||||
const redirectWithQueryBuilderData = useCallback(
|
||||
(
|
||||
|
||||
@@ -16,6 +16,7 @@ import thunk from 'redux-thunk';
|
||||
import store from 'store';
|
||||
import {
|
||||
LicenseEvent,
|
||||
LicensePlatform,
|
||||
LicenseState,
|
||||
LicenseStatus,
|
||||
} from 'types/api/licensesV3/getActive';
|
||||
@@ -115,6 +116,7 @@ export function getAppContextMock(
|
||||
key: 'does-not-matter',
|
||||
state: LicenseState.ACTIVE,
|
||||
status: LicenseStatus.VALID,
|
||||
platform: LicensePlatform.CLOUD,
|
||||
},
|
||||
isFetchingActiveLicenseV3: false,
|
||||
activeLicenseV3FetchError: null,
|
||||
@@ -126,7 +128,6 @@ export function getAppContextMock(
|
||||
name: 'John Doe',
|
||||
profilePictureURL: '',
|
||||
createdAt: 1732544623,
|
||||
flags: {},
|
||||
organization: 'Nightswatch',
|
||||
orgId: 'does-not-matter-id',
|
||||
role: role as ROLES,
|
||||
@@ -324,7 +325,6 @@ export function getAppContextMock(
|
||||
orgPreferencesFetchError: null,
|
||||
isLoggedIn: true,
|
||||
updateUser: jest.fn(),
|
||||
setUserFlags: jest.fn(),
|
||||
updateOrg: jest.fn(),
|
||||
updateOrgPreferences: jest.fn(),
|
||||
licensesRefetch: jest.fn(),
|
||||
|
||||
@@ -13,6 +13,11 @@ export enum LicenseState {
|
||||
ACTIVE = 'ACTIVE',
|
||||
}
|
||||
|
||||
export enum LicensePlatform {
|
||||
SELF_HOSTED = 'SELF_HOSTED',
|
||||
CLOUD = 'CLOUD',
|
||||
}
|
||||
|
||||
export type LicenseV3EventQueueResModel = {
|
||||
event: LicenseEvent;
|
||||
status: string;
|
||||
@@ -26,4 +31,5 @@ export type LicenseV3ResModel = {
|
||||
status: LicenseStatus;
|
||||
state: LicenseState;
|
||||
event_queue: LicenseV3EventQueueResModel;
|
||||
platform: LicensePlatform;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { UserFlags } from 'types/api/user/setFlags';
|
||||
import { User } from 'types/reducer/app';
|
||||
import { ROLES } from 'types/roles';
|
||||
|
||||
@@ -16,6 +15,5 @@ export interface PayloadProps {
|
||||
profilePictureURL: string;
|
||||
organization: string;
|
||||
role: ROLES;
|
||||
flags: UserFlags;
|
||||
groupId: string;
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { User } from 'types/reducer/app';
|
||||
|
||||
export interface UserFlags {
|
||||
ReleaseNote0120Hide?: string;
|
||||
}
|
||||
|
||||
export type PayloadProps = UserFlags;
|
||||
|
||||
export interface Props {
|
||||
userId: User['userId'];
|
||||
flags: UserFlags;
|
||||
}
|
||||
@@ -13,18 +13,6 @@ export function extractDomain(email: string): string {
|
||||
return emailParts[1];
|
||||
}
|
||||
|
||||
export const isCloudUser = (): boolean => {
|
||||
const { hostname } = window.location;
|
||||
|
||||
return hostname?.endsWith('signoz.cloud');
|
||||
};
|
||||
|
||||
export const isEECloudUser = (): boolean => {
|
||||
const { hostname } = window.location;
|
||||
|
||||
return hostname?.endsWith('signoz.io');
|
||||
};
|
||||
|
||||
export const checkVersionState = (
|
||||
currentVersion: string,
|
||||
latestVersion: string,
|
||||
|
||||
@@ -3936,6 +3936,11 @@
|
||||
dependencies:
|
||||
"@types/geojson" "*"
|
||||
|
||||
"@types/d3-hierarchy@^1.1.6":
|
||||
version "1.1.11"
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-hierarchy/-/d3-hierarchy-1.1.11.tgz#c3bd70d025621f73cb3319e97e08ae4c9051c791"
|
||||
integrity sha512-lnQiU7jV+Gyk9oQYk0GGYccuexmQPTp08E0+4BidgFdiJivjEvf+esPSdZqCZ2C7UwTWejWpqetVaU8A+eX3FA==
|
||||
|
||||
"@types/d3-interpolate@3.0.1", "@types/d3-interpolate@^3.0.0":
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz#e7d17fa4a5830ad56fe22ce3b4fac8541a9572dc"
|
||||
@@ -4474,6 +4479,11 @@
|
||||
resolved "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.3.tgz"
|
||||
integrity sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==
|
||||
|
||||
"@types/trusted-types@^2.0.7":
|
||||
version "2.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11"
|
||||
integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==
|
||||
|
||||
"@types/unist@*", "@types/unist@^3.0.0":
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.2.tgz#6dd61e43ef60b34086287f83683a5c1b2dc53d20"
|
||||
@@ -4716,6 +4726,15 @@
|
||||
"@types/d3-shape" "^1.3.1"
|
||||
d3-shape "^1.0.6"
|
||||
|
||||
"@visx/group@3.12.0":
|
||||
version "3.12.0"
|
||||
resolved "https://registry.yarnpkg.com/@visx/group/-/group-3.12.0.tgz#2c69b810b52f1c1e69bf6f2fe923d184e32078c7"
|
||||
integrity sha512-Dye8iS1alVXPv7nj/7M37gJe6sSKqJLH7x6sEWAsRQ9clI0kFvjbKcKgF+U3aAVQr0NCohheFV+DtR8trfK/Ag==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
classnames "^2.3.1"
|
||||
prop-types "^15.6.2"
|
||||
|
||||
"@visx/group@3.3.0":
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@visx/group/-/group-3.3.0.tgz#20c1b75c1ab31798c3c702b6f58c412c688a6373"
|
||||
@@ -4725,6 +4744,18 @@
|
||||
classnames "^2.3.1"
|
||||
prop-types "^15.6.2"
|
||||
|
||||
"@visx/hierarchy@3.12.0":
|
||||
version "3.12.0"
|
||||
resolved "https://registry.yarnpkg.com/@visx/hierarchy/-/hierarchy-3.12.0.tgz#38295d2469cf957ed6d7700fe968aa16cbb878f0"
|
||||
integrity sha512-+X1HOeLEOODxjAD7ixrWJ4KCVei4wFe8ra3dYU0uZ14RdPPgUeiuyBfdeXWZuAHM6Ix9qrryneatQjkC3h4mvA==
|
||||
dependencies:
|
||||
"@types/d3-hierarchy" "^1.1.6"
|
||||
"@types/react" "*"
|
||||
"@visx/group" "3.12.0"
|
||||
classnames "^2.3.1"
|
||||
d3-hierarchy "^1.1.4"
|
||||
prop-types "^15.6.1"
|
||||
|
||||
"@visx/scale@3.5.0":
|
||||
version "3.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@visx/scale/-/scale-3.5.0.tgz#c3db3863bbdd24d44781104ef5ee4cdc8df6f11d"
|
||||
@@ -7095,6 +7126,16 @@ d3-geo@3.1.0:
|
||||
dependencies:
|
||||
d3-array "2.5.0 - 3"
|
||||
|
||||
d3-hierarchy@3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz#b01cd42c1eed3d46db77a5966cf726f8c09160c6"
|
||||
integrity sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==
|
||||
|
||||
d3-hierarchy@^1.1.4:
|
||||
version "1.1.9"
|
||||
resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz#2f6bee24caaea43f8dc37545fa01628559647a83"
|
||||
integrity sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==
|
||||
|
||||
"d3-interpolate@1 - 3", "d3-interpolate@1.2.0 - 3", d3-interpolate@3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d"
|
||||
@@ -7538,15 +7579,12 @@ domhandler@^5.0.2, domhandler@^5.0.3:
|
||||
dependencies:
|
||||
domelementtype "^2.3.0"
|
||||
|
||||
dompurify@3.1.3:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.1.3.tgz#cfe3ce4232c216d923832f68f2aa18b2fb9bd223"
|
||||
integrity sha512-5sOWYSNPaxz6o2MUPvtyxTTqR4D3L77pr5rUQoWgD5ROQtVIZQgJkXbo1DLlK3vj11YGw5+LnF4SYti4gZmwng==
|
||||
|
||||
dompurify@^3.0.0:
|
||||
version "3.1.7"
|
||||
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.1.7.tgz#711a8c96479fb6ced93453732c160c3c72418a6a"
|
||||
integrity sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==
|
||||
dompurify@3.2.4, dompurify@^3.0.0:
|
||||
version "3.2.4"
|
||||
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.2.4.tgz#af5a5a11407524431456cf18836c55d13441cd8e"
|
||||
integrity sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==
|
||||
optionalDependencies:
|
||||
"@types/trusted-types" "^2.0.7"
|
||||
|
||||
domutils@^2.5.2, domutils@^2.8.0:
|
||||
version "2.8.0"
|
||||
@@ -9974,7 +10012,7 @@ is-wsl@^2.2.0:
|
||||
|
||||
isarray@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
|
||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
|
||||
integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==
|
||||
|
||||
isarray@^2.0.5:
|
||||
@@ -12731,9 +12769,9 @@ path-to-regexp@0.1.10:
|
||||
integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==
|
||||
|
||||
path-to-regexp@^1.7.0:
|
||||
version "1.8.0"
|
||||
resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz"
|
||||
integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==
|
||||
version "1.9.0"
|
||||
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.9.0.tgz#5dc0753acbf8521ca2e0f137b4578b917b10cf24"
|
||||
integrity sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==
|
||||
dependencies:
|
||||
isarray "0.0.1"
|
||||
|
||||
|
||||
3
go.mod
3
go.mod
@@ -55,6 +55,7 @@ require (
|
||||
github.com/soheilhy/cmux v0.1.5
|
||||
github.com/srikanthccv/ClickHouse-go-mock v0.9.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
github.com/uptrace/bun v1.2.9
|
||||
github.com/uptrace/bun/dialect/pgdialect v1.2.9
|
||||
github.com/uptrace/bun/dialect/sqlitedialect v1.2.9
|
||||
@@ -204,6 +205,8 @@ require (
|
||||
github.com/smarty/assertions v1.15.0 // indirect
|
||||
github.com/spf13/cobra v1.8.1 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.13 // indirect
|
||||
github.com/tklauser/numcpus v0.7.0 // indirect
|
||||
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
|
||||
|
||||
6
go.sum
6
go.sum
@@ -891,7 +891,13 @@ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tklauser/go-sysconf v0.3.13 h1:GBUpcahXSpR2xN01jhkNAbTLRk2Yzgggk8IM08lq3r4=
|
||||
github.com/tklauser/go-sysconf v0.3.13/go.mod h1:zwleP4Q4OehZHGn4CYZDipCgg9usW5IJePewFCGVEa0=
|
||||
github.com/tklauser/numcpus v0.7.0 h1:yjuerZP127QG9m5Zh/mSO4wqurYil27tHrqwRoRjpr4=
|
||||
|
||||
@@ -15,7 +15,7 @@ var (
|
||||
type Alertmanager interface {
|
||||
factory.Service
|
||||
// GetAlerts gets the alerts from the alertmanager per organization.
|
||||
GetAlerts(context.Context, string, alertmanagertypes.GettableAlertsParams) (alertmanagertypes.GettableAlerts, error)
|
||||
GetAlerts(context.Context, string, alertmanagertypes.GettableAlertsParams) (alertmanagertypes.DeprecatedGettableAlerts, error)
|
||||
|
||||
// PutAlerts puts the alerts into the alertmanager per organization.
|
||||
PutAlerts(context.Context, string, alertmanagertypes.PostableAlerts) error
|
||||
@@ -23,18 +23,30 @@ type Alertmanager interface {
|
||||
// TestReceiver sends a test alert to a receiver.
|
||||
TestReceiver(context.Context, string, alertmanagertypes.Receiver) error
|
||||
|
||||
// TestAlert sends an alert to a list of receivers.
|
||||
TestAlert(ctx context.Context, orgID string, alert *alertmanagertypes.PostableAlert, receivers []string) error
|
||||
|
||||
// ListChannels lists all channels for the organization.
|
||||
ListChannels(context.Context, string) ([]*alertmanagertypes.Channel, error)
|
||||
|
||||
// ListAllChannels lists all channels for all organizations. It is used by the legacy alertmanager only.
|
||||
ListAllChannels(context.Context) ([]*alertmanagertypes.Channel, error)
|
||||
|
||||
// GetChannelByID gets a channel for the organization.
|
||||
GetChannelByID(context.Context, string, int) (*alertmanagertypes.Channel, error)
|
||||
|
||||
// UpdateChannel updates a channel for the organization.
|
||||
UpdateChannelByReceiver(context.Context, string, alertmanagertypes.Receiver) error
|
||||
UpdateChannelByReceiverAndID(context.Context, string, alertmanagertypes.Receiver, int) error
|
||||
|
||||
// CreateChannel creates a channel for the organization.
|
||||
CreateChannel(context.Context, string, alertmanagertypes.Receiver) error
|
||||
|
||||
// DeleteChannelByID deletes a channel for the organization.
|
||||
DeleteChannelByID(context.Context, string, int) error
|
||||
|
||||
// SetConfig sets the config for the organization.
|
||||
SetConfig(context.Context, *alertmanagertypes.Config) error
|
||||
|
||||
// GetConfig gets the config for the organization.
|
||||
GetConfig(context.Context, string) (*alertmanagertypes.Config, error)
|
||||
}
|
||||
|
||||
@@ -9,9 +9,11 @@ import (
|
||||
"go.signoz.io/signoz/pkg/types/alertmanagertypes"
|
||||
)
|
||||
|
||||
// Notifier is responsible for dispatching alert notifications to an alertmanager.
|
||||
// Batcher is responsible for batching alerts and broadcasting them on a channel.
|
||||
type Batcher struct {
|
||||
// C is the channel on which alerts are sent to alertmanager
|
||||
C chan alertmanagertypes.PostableAlerts
|
||||
|
||||
// logger
|
||||
logger *slog.Logger
|
||||
|
||||
@@ -23,14 +25,21 @@ type Batcher struct {
|
||||
|
||||
// more channel to signal the sender goroutine to send alerts
|
||||
moreC chan struct{}
|
||||
|
||||
// stop channel to signal the sender goroutine to stop
|
||||
stopC chan struct{}
|
||||
mtx sync.RWMutex
|
||||
|
||||
// mutex to synchronize access to the queue
|
||||
queueMtx sync.RWMutex
|
||||
|
||||
// wait group to wait for all goroutines to finish
|
||||
goroutinesWg sync.WaitGroup
|
||||
}
|
||||
|
||||
func New(logger *slog.Logger, config Config) *Batcher {
|
||||
batcher := &Batcher{
|
||||
logger: logger,
|
||||
queue: make(alertmanagertypes.PostableAlerts, config.Capacity),
|
||||
queue: make(alertmanagertypes.PostableAlerts, 0, config.Capacity),
|
||||
config: config,
|
||||
moreC: make(chan struct{}, 1),
|
||||
stopC: make(chan struct{}),
|
||||
@@ -41,22 +50,27 @@ func New(logger *slog.Logger, config Config) *Batcher {
|
||||
}
|
||||
|
||||
// Start dispatches notifications continuously.
|
||||
func (n *Batcher) Start(ctx context.Context) error {
|
||||
func (batcher *Batcher) Start(ctx context.Context) error {
|
||||
batcher.goroutinesWg.Add(1)
|
||||
go func() {
|
||||
n.logger.InfoContext(ctx, "starting alertmanager batcher")
|
||||
defer batcher.goroutinesWg.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case <-batcher.stopC:
|
||||
for batcher.queueLen() > 0 {
|
||||
alerts := batcher.next()
|
||||
batcher.C <- alerts
|
||||
}
|
||||
close(batcher.C)
|
||||
return
|
||||
case <-n.stopC:
|
||||
return
|
||||
case <-n.moreC:
|
||||
case <-batcher.moreC:
|
||||
}
|
||||
alerts := n.nextBatch()
|
||||
n.C <- alerts
|
||||
alerts := batcher.next()
|
||||
batcher.C <- alerts
|
||||
// If the queue still has items left, kick off the next iteration.
|
||||
if n.queueLen() > 0 {
|
||||
n.setMore()
|
||||
if batcher.queueLen() > 0 {
|
||||
batcher.setMore()
|
||||
}
|
||||
}
|
||||
}()
|
||||
@@ -64,69 +78,67 @@ func (n *Batcher) Start(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *Batcher) queueLen() int {
|
||||
n.mtx.RLock()
|
||||
defer n.mtx.RUnlock()
|
||||
// Add queues the given alerts for processing.
|
||||
func (batcher *Batcher) Add(ctx context.Context, alerts ...*alertmanagertypes.PostableAlert) {
|
||||
batcher.queueMtx.Lock()
|
||||
defer batcher.queueMtx.Unlock()
|
||||
|
||||
return len(n.queue)
|
||||
// Queue capacity should be significantly larger than a single alert
|
||||
// batch could be.
|
||||
if d := len(alerts) - batcher.config.Capacity; d > 0 {
|
||||
alerts = alerts[d:]
|
||||
batcher.logger.WarnContext(ctx, "alert batch larger than queue capacity, dropping alerts", "num_dropped", d, "capacity", batcher.config.Capacity)
|
||||
}
|
||||
|
||||
// If the queue is full, remove the oldest alerts in favor
|
||||
// of newer ones.
|
||||
if d := (len(batcher.queue) + len(alerts)) - batcher.config.Capacity; d > 0 {
|
||||
batcher.queue = batcher.queue[d:]
|
||||
batcher.logger.WarnContext(ctx, "alert batch queue full, dropping alerts", "num_dropped", d)
|
||||
}
|
||||
|
||||
batcher.queue = append(batcher.queue, alerts...)
|
||||
|
||||
// Notify sending goroutine that there are alerts to be processed.
|
||||
batcher.setMore()
|
||||
}
|
||||
|
||||
func (n *Batcher) nextBatch() alertmanagertypes.PostableAlerts {
|
||||
n.mtx.Lock()
|
||||
defer n.mtx.Unlock()
|
||||
// Stop shuts down the batcher.
|
||||
func (batcher *Batcher) Stop(ctx context.Context) {
|
||||
close(batcher.stopC)
|
||||
batcher.goroutinesWg.Wait()
|
||||
}
|
||||
|
||||
func (batcher *Batcher) queueLen() int {
|
||||
batcher.queueMtx.RLock()
|
||||
defer batcher.queueMtx.RUnlock()
|
||||
|
||||
return len(batcher.queue)
|
||||
}
|
||||
|
||||
func (batcher *Batcher) next() alertmanagertypes.PostableAlerts {
|
||||
batcher.queueMtx.Lock()
|
||||
defer batcher.queueMtx.Unlock()
|
||||
|
||||
var alerts alertmanagertypes.PostableAlerts
|
||||
|
||||
if len(n.queue) > n.config.Size {
|
||||
alerts = append(make(alertmanagertypes.PostableAlerts, 0, n.config.Size), n.queue[:n.config.Size]...)
|
||||
n.queue = n.queue[n.config.Size:]
|
||||
if len(batcher.queue) > batcher.config.Size {
|
||||
alerts = append(make(alertmanagertypes.PostableAlerts, 0, batcher.config.Size), batcher.queue[:batcher.config.Size]...)
|
||||
batcher.queue = batcher.queue[batcher.config.Size:]
|
||||
} else {
|
||||
alerts = append(make(alertmanagertypes.PostableAlerts, 0, len(n.queue)), n.queue...)
|
||||
n.queue = n.queue[:0]
|
||||
alerts = append(make(alertmanagertypes.PostableAlerts, 0, len(batcher.queue)), batcher.queue...)
|
||||
batcher.queue = batcher.queue[:0]
|
||||
}
|
||||
|
||||
return alerts
|
||||
}
|
||||
|
||||
// Send queues the given notification requests for processing.
|
||||
// Panics if called on a handler that is not running.
|
||||
func (n *Batcher) Send(ctx context.Context, alerts ...*alertmanagertypes.PostableAlert) {
|
||||
n.mtx.Lock()
|
||||
defer n.mtx.Unlock()
|
||||
|
||||
// Queue capacity should be significantly larger than a single alert
|
||||
// batch could be.
|
||||
if d := len(alerts) - n.config.Capacity; d > 0 {
|
||||
alerts = alerts[d:]
|
||||
n.logger.WarnContext(ctx, "Alert batch larger than queue capacity, dropping alerts", "num_dropped", d)
|
||||
}
|
||||
|
||||
// If the queue is full, remove the oldest alerts in favor
|
||||
// of newer ones.
|
||||
if d := (len(n.queue) + len(alerts)) - n.config.Capacity; d > 0 {
|
||||
n.queue = n.queue[d:]
|
||||
|
||||
n.logger.WarnContext(ctx, "Alert notification queue full, dropping alerts", "num_dropped", d)
|
||||
}
|
||||
n.queue = append(n.queue, alerts...)
|
||||
|
||||
// Notify sending goroutine that there are alerts to be processed.
|
||||
n.setMore()
|
||||
}
|
||||
|
||||
// setMore signals that the alert queue has items.
|
||||
func (n *Batcher) setMore() {
|
||||
func (batcher *Batcher) setMore() {
|
||||
// If we cannot send on the channel, it means the signal already exists
|
||||
// and has not been consumed yet.
|
||||
select {
|
||||
case n.moreC <- struct{}{}:
|
||||
case batcher.moreC <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// Stop shuts down the notification handler.
|
||||
func (n *Batcher) Stop(ctx context.Context) {
|
||||
n.logger.InfoContext(ctx, "Stopping alertmanager batcher")
|
||||
close(n.moreC)
|
||||
close(n.stopC)
|
||||
}
|
||||
|
||||
64
pkg/alertmanager/alertmanagerbatcher/batcher_test.go
Normal file
64
pkg/alertmanager/alertmanagerbatcher/batcher_test.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package alertmanagerbatcher
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"log/slog"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.signoz.io/signoz/pkg/types/alertmanagertypes"
|
||||
)
|
||||
|
||||
func TestBatcherWithOneAlertAndDefaultConfigs(t *testing.T) {
|
||||
batcher := New(slog.New(slog.NewTextHandler(io.Discard, nil)), NewConfig())
|
||||
batcher.Start(context.Background())
|
||||
|
||||
batcher.Add(context.Background(), &alertmanagertypes.PostableAlert{Alert: alertmanagertypes.AlertModel{
|
||||
Labels: map[string]string{"alertname": "test"},
|
||||
}})
|
||||
|
||||
alerts := <-batcher.C
|
||||
assert.Equal(t, 1, len(alerts))
|
||||
|
||||
batcher.Stop(context.Background())
|
||||
}
|
||||
|
||||
func TestBatcherWithBatchSize(t *testing.T) {
|
||||
batcher := New(slog.New(slog.NewTextHandler(io.Discard, nil)), Config{Size: 2, Capacity: 4})
|
||||
batcher.Start(context.Background())
|
||||
|
||||
var alerts alertmanagertypes.PostableAlerts
|
||||
for i := 0; i < 4; i++ {
|
||||
alerts = append(alerts, &alertmanagertypes.PostableAlert{Alert: alertmanagertypes.AlertModel{
|
||||
Labels: map[string]string{"alertname": "test"},
|
||||
}})
|
||||
}
|
||||
batcher.Add(context.Background(), alerts...)
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
alerts := <-batcher.C
|
||||
assert.Equal(t, 2, len(alerts))
|
||||
}
|
||||
|
||||
batcher.Stop(context.Background())
|
||||
}
|
||||
|
||||
func TestBatcherWithCClosed(t *testing.T) {
|
||||
batcher := New(slog.New(slog.NewTextHandler(io.Discard, nil)), Config{Size: 2, Capacity: 4})
|
||||
batcher.Start(context.Background())
|
||||
|
||||
var alerts alertmanagertypes.PostableAlerts
|
||||
for i := 0; i < 4; i++ {
|
||||
alerts = append(alerts, &alertmanagertypes.PostableAlert{Alert: alertmanagertypes.AlertModel{
|
||||
Labels: map[string]string{"alertname": "test"},
|
||||
}})
|
||||
}
|
||||
batcher.Add(context.Background(), alerts...)
|
||||
|
||||
batcher.Stop(context.Background())
|
||||
|
||||
for alerts := range batcher.C {
|
||||
assert.Equal(t, 2, len(alerts))
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,16 @@
|
||||
package alertmanagerbatcher
|
||||
|
||||
type Config struct {
|
||||
// Capacity is the maximum number of alerts that can be buffered in the batcher.
|
||||
Capacity int
|
||||
Size int
|
||||
|
||||
// Size is the number of alerts to send in each batch.
|
||||
Size int
|
||||
}
|
||||
|
||||
func NewConfig() Config {
|
||||
return Config{
|
||||
Capacity: 1000,
|
||||
Capacity: 10000,
|
||||
Size: 64,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ type Server struct {
|
||||
|
||||
func New(ctx context.Context, logger *slog.Logger, registry prometheus.Registerer, srvConfig Config, orgID string, stateStore alertmanagertypes.StateStore) (*Server, error) {
|
||||
server := &Server{
|
||||
logger: logger.With("pkg", "go.signoz.io/pkg/alertmanager/server"),
|
||||
logger: logger.With("pkg", "go.signoz.io/pkg/alertmanager/alertmanagerserver"),
|
||||
registry: registry,
|
||||
srvConfig: srvConfig,
|
||||
orgID: orgID,
|
||||
@@ -84,7 +84,10 @@ func New(ctx context.Context, logger *slog.Logger, registry prometheus.Registere
|
||||
|
||||
silencesSnapshot := ""
|
||||
if state != nil {
|
||||
silencesSnapshot = state.Silences
|
||||
silencesSnapshot, err = state.Get(alertmanagertypes.SilenceStateName)
|
||||
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// Initialize silences
|
||||
server.silences, err = silence.New(silence.Options{
|
||||
@@ -103,7 +106,10 @@ func New(ctx context.Context, logger *slog.Logger, registry prometheus.Registere
|
||||
|
||||
nflogSnapshot := ""
|
||||
if state != nil {
|
||||
nflogSnapshot = state.NFLog
|
||||
nflogSnapshot, err = state.Get(alertmanagertypes.NFLogStateName)
|
||||
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize notification log
|
||||
@@ -308,7 +314,51 @@ func (server *Server) SetConfig(ctx context.Context, alertmanagerConfig *alertma
|
||||
}
|
||||
|
||||
func (server *Server) TestReceiver(ctx context.Context, receiver alertmanagertypes.Receiver) error {
|
||||
return alertmanagertypes.TestReceiver(ctx, receiver, server.tmpl, server.logger)
|
||||
return alertmanagertypes.TestReceiver(ctx, receiver, server.alertmanagerConfig, server.tmpl, server.logger, alertmanagertypes.NewTestAlert(receiver, time.Now(), time.Now()))
|
||||
}
|
||||
|
||||
func (server *Server) TestAlert(ctx context.Context, postableAlert *alertmanagertypes.PostableAlert, receivers []string) error {
|
||||
alerts, err := alertmanagertypes.NewAlertsFromPostableAlerts(alertmanagertypes.PostableAlerts{postableAlert}, time.Duration(server.srvConfig.Global.ResolveTimeout), time.Now())
|
||||
if err != nil {
|
||||
return errors.Join(err...)
|
||||
}
|
||||
|
||||
if len(alerts) != 1 {
|
||||
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "expected 1 alert, got %d", len(alerts))
|
||||
}
|
||||
|
||||
ch := make(chan error, len(receivers))
|
||||
for _, receiverName := range receivers {
|
||||
go func(receiverName string) {
|
||||
receiver, err := server.alertmanagerConfig.GetReceiver(receiverName)
|
||||
if err != nil {
|
||||
ch <- err
|
||||
return
|
||||
}
|
||||
ch <- alertmanagertypes.TestReceiver(ctx, receiver, server.alertmanagerConfig, server.tmpl, server.logger, alerts[0])
|
||||
}(receiverName)
|
||||
}
|
||||
|
||||
var errs []error
|
||||
for i := 0; i < len(receivers); i++ {
|
||||
if err := <-ch; err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
if errs != nil {
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (server *Server) Hash() string {
|
||||
if server.alertmanagerConfig == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return server.alertmanagerConfig.StoreableConfig().Hash
|
||||
}
|
||||
|
||||
func (server *Server) Stop(ctx context.Context) error {
|
||||
|
||||
@@ -87,7 +87,7 @@ func TestServerPutAlerts(t *testing.T) {
|
||||
amConfig, err := alertmanagertypes.NewDefaultConfig(srvCfg.Global, srvCfg.Route, "1")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, amConfig.CreateReceiver(&config.Route{Receiver: "test-receiver", Continue: true}, alertmanagertypes.Receiver{
|
||||
require.NoError(t, amConfig.CreateReceiver(alertmanagertypes.Receiver{
|
||||
Name: "test-receiver",
|
||||
WebhookConfigs: []*config.WebhookConfig{
|
||||
{
|
||||
|
||||
@@ -3,7 +3,10 @@ package sqlalertmanagerstore
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strconv"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/uptrace/bun"
|
||||
"go.signoz.io/signoz/pkg/errors"
|
||||
"go.signoz.io/signoz/pkg/sqlstore"
|
||||
"go.signoz.io/signoz/pkg/types/alertmanagertypes"
|
||||
@@ -45,47 +48,23 @@ func (store *config) Get(ctx context.Context, orgID string) (*alertmanagertypes.
|
||||
}
|
||||
|
||||
// Set implements alertmanagertypes.ConfigStore.
|
||||
func (store *config) Set(ctx context.Context, config *alertmanagertypes.Config, cb func(context.Context) error) error {
|
||||
tx, err := store.sqlstore.BunDB().BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer tx.Rollback() //nolint:errcheck
|
||||
|
||||
if _, err = tx.
|
||||
NewInsert().
|
||||
Model(config.StoreableConfig()).
|
||||
On("CONFLICT (org_id) DO UPDATE").
|
||||
Set("config = ?", string(config.StoreableConfig().Config)).
|
||||
Set("updated_at = ?", config.StoreableConfig().UpdatedAt).
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
channels := config.Channels()
|
||||
if len(channels) != 0 {
|
||||
if _, err = tx.NewInsert().
|
||||
Model(&channels).
|
||||
On("CONFLICT (name) DO UPDATE").
|
||||
Set("data = EXCLUDED.data").
|
||||
Set("updated_at = EXCLUDED.updated_at").
|
||||
func (store *config) Set(ctx context.Context, config *alertmanagertypes.Config, opts ...alertmanagertypes.StoreOption) error {
|
||||
return store.wrap(ctx, func(ctx context.Context) error {
|
||||
if _, err := store.
|
||||
sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
NewInsert().
|
||||
Model(config.StoreableConfig()).
|
||||
On("CONFLICT (org_id) DO UPDATE").
|
||||
Set("config = ?", config.StoreableConfig().Config).
|
||||
Set("hash = ?", config.StoreableConfig().Hash).
|
||||
Set("updated_at = ?", config.StoreableConfig().UpdatedAt).
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if cb != nil {
|
||||
if err = cb(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err = tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return nil
|
||||
}, opts...)
|
||||
}
|
||||
|
||||
func (store *config) ListOrgs(ctx context.Context) ([]string, error) {
|
||||
@@ -104,3 +83,154 @@ func (store *config) ListOrgs(ctx context.Context) ([]string, error) {
|
||||
|
||||
return orgIDs, nil
|
||||
}
|
||||
|
||||
func (store *config) CreateChannel(ctx context.Context, channel *alertmanagertypes.Channel, opts ...alertmanagertypes.StoreOption) error {
|
||||
return store.wrap(ctx, func(ctx context.Context) error {
|
||||
if _, err := store.
|
||||
sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
NewInsert().
|
||||
Model(channel).
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}, opts...)
|
||||
}
|
||||
|
||||
func (store *config) GetChannelByID(ctx context.Context, orgID string, id int) (*alertmanagertypes.Channel, error) {
|
||||
channel := new(alertmanagertypes.Channel)
|
||||
|
||||
err := store.
|
||||
sqlstore.
|
||||
BunDB().
|
||||
NewSelect().
|
||||
Model(channel).
|
||||
Where("org_id = ?", orgID).
|
||||
Where("id = ?", id).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, errors.Newf(errors.TypeNotFound, alertmanagertypes.ErrCodeAlertmanagerChannelNotFound, "cannot find channel with id %d", id)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return channel, nil
|
||||
}
|
||||
|
||||
func (store *config) UpdateChannel(ctx context.Context, orgID string, channel *alertmanagertypes.Channel, opts ...alertmanagertypes.StoreOption) error {
|
||||
return store.wrap(ctx, func(ctx context.Context) error {
|
||||
if _, err := store.
|
||||
sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
NewUpdate().
|
||||
Model(channel).
|
||||
WherePK().
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}, opts...)
|
||||
}
|
||||
|
||||
func (store *config) DeleteChannelByID(ctx context.Context, orgID string, id int, opts ...alertmanagertypes.StoreOption) error {
|
||||
return store.wrap(ctx, func(ctx context.Context) error {
|
||||
channel := new(alertmanagertypes.Channel)
|
||||
|
||||
if _, err := store.
|
||||
sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
NewDelete().
|
||||
Model(channel).
|
||||
Where("org_id = ?", orgID).
|
||||
Where("id = ?", id).
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}, opts...)
|
||||
}
|
||||
|
||||
func (store *config) ListChannels(ctx context.Context, orgID string) ([]*alertmanagertypes.Channel, error) {
|
||||
var channels []*alertmanagertypes.Channel
|
||||
|
||||
err := store.
|
||||
sqlstore.
|
||||
BunDB().
|
||||
NewSelect().
|
||||
Model(&channels).
|
||||
Where("org_id = ?", orgID).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return channels, nil
|
||||
}
|
||||
|
||||
func (store *config) ListAllChannels(ctx context.Context) ([]*alertmanagertypes.Channel, error) {
|
||||
var channels []*alertmanagertypes.Channel
|
||||
|
||||
err := store.
|
||||
sqlstore.
|
||||
BunDB().
|
||||
NewSelect().
|
||||
Model(&channels).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return channels, nil
|
||||
}
|
||||
|
||||
func (store *config) GetMatchers(ctx context.Context, orgID string) (map[string][]string, error) {
|
||||
type matcher struct {
|
||||
bun.BaseModel `bun:"table:rules"`
|
||||
ID int `bun:"id,pk"`
|
||||
Data string `bun:"data"`
|
||||
}
|
||||
|
||||
matchers := []matcher{}
|
||||
|
||||
err := store.
|
||||
sqlstore.
|
||||
BunDB().
|
||||
NewSelect().
|
||||
Column("id", "data").
|
||||
Model(&matchers).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
matchersMap := make(map[string][]string)
|
||||
for _, matcher := range matchers {
|
||||
receivers := gjson.Get(matcher.Data, "preferredChannels").Array()
|
||||
for _, receiver := range receivers {
|
||||
matchersMap[strconv.Itoa(matcher.ID)] = append(matchersMap[strconv.Itoa(matcher.ID)], receiver.String())
|
||||
}
|
||||
}
|
||||
|
||||
return matchersMap, nil
|
||||
}
|
||||
|
||||
func (store *config) wrap(ctx context.Context, fn func(ctx context.Context) error, opts ...alertmanagertypes.StoreOption) error {
|
||||
storeOpts := alertmanagertypes.NewStoreOptions(opts...)
|
||||
|
||||
if storeOpts.Cb == nil {
|
||||
return fn(ctx)
|
||||
}
|
||||
|
||||
return store.sqlstore.RunInTxCtx(ctx, nil, func(ctx context.Context) error {
|
||||
if err := fn(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return storeOpts.Cb(ctx)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ func NewAPI(alertmanager Alertmanager) *API {
|
||||
}
|
||||
}
|
||||
|
||||
func (api *API) GetAlerts(req *http.Request, rw http.ResponseWriter) {
|
||||
func (api *API) GetAlerts(rw http.ResponseWriter, req *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
@@ -49,7 +49,7 @@ func (api *API) GetAlerts(req *http.Request, rw http.ResponseWriter) {
|
||||
render.Success(rw, http.StatusOK, alerts)
|
||||
}
|
||||
|
||||
func (api *API) TestReceiver(req *http.Request, rw http.ResponseWriter) {
|
||||
func (api *API) TestReceiver(rw http.ResponseWriter, req *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
@@ -81,7 +81,7 @@ func (api *API) TestReceiver(req *http.Request, rw http.ResponseWriter) {
|
||||
render.Success(rw, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
func (api *API) ListChannels(req *http.Request, rw http.ResponseWriter) {
|
||||
func (api *API) ListChannels(rw http.ResponseWriter, req *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
@@ -100,7 +100,20 @@ func (api *API) ListChannels(req *http.Request, rw http.ResponseWriter) {
|
||||
render.Success(rw, http.StatusOK, channels)
|
||||
}
|
||||
|
||||
func (api *API) GetChannelByID(req *http.Request, rw http.ResponseWriter) {
|
||||
func (api *API) ListAllChannels(rw http.ResponseWriter, req *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
channels, err := api.alertmanager.ListAllChannels(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, channels)
|
||||
}
|
||||
|
||||
func (api *API) GetChannelByID(rw http.ResponseWriter, req *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
@@ -137,7 +150,7 @@ func (api *API) GetChannelByID(req *http.Request, rw http.ResponseWriter) {
|
||||
render.Success(rw, http.StatusOK, channel)
|
||||
}
|
||||
|
||||
func (api *API) UpdateChannelByID(req *http.Request, rw http.ResponseWriter) {
|
||||
func (api *API) UpdateChannelByID(rw http.ResponseWriter, req *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
@@ -147,6 +160,24 @@ func (api *API) UpdateChannelByID(req *http.Request, rw http.ResponseWriter) {
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(req)
|
||||
if vars == nil {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is required in path"))
|
||||
return
|
||||
}
|
||||
|
||||
idString, ok := vars["id"]
|
||||
if !ok {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is required in path"))
|
||||
return
|
||||
}
|
||||
|
||||
id, err := strconv.Atoi(idString)
|
||||
if err != nil {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is not a valid integer"))
|
||||
return
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
@@ -160,7 +191,7 @@ func (api *API) UpdateChannelByID(req *http.Request, rw http.ResponseWriter) {
|
||||
return
|
||||
}
|
||||
|
||||
err = api.alertmanager.UpdateChannelByReceiver(ctx, claims.OrgID, receiver)
|
||||
err = api.alertmanager.UpdateChannelByReceiverAndID(ctx, claims.OrgID, receiver, id)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
@@ -169,7 +200,7 @@ func (api *API) UpdateChannelByID(req *http.Request, rw http.ResponseWriter) {
|
||||
render.Success(rw, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
func (api *API) DeleteChannelByID(req *http.Request, rw http.ResponseWriter) {
|
||||
func (api *API) DeleteChannelByID(rw http.ResponseWriter, req *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
@@ -206,7 +237,7 @@ func (api *API) DeleteChannelByID(req *http.Request, rw http.ResponseWriter) {
|
||||
render.Success(rw, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
func (api *API) CreateChannel(req *http.Request, rw http.ResponseWriter) {
|
||||
func (api *API) CreateChannel(rw http.ResponseWriter, req *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package alertmanager
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
@@ -9,9 +11,6 @@ import (
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
// Config is the config for the alertmanager server.
|
||||
alertmanagerserver.Config `mapstructure:",squash"`
|
||||
|
||||
// Provider is the provider for the alertmanager service.
|
||||
Provider string `mapstructure:"provider"`
|
||||
|
||||
@@ -25,11 +24,14 @@ type Config struct {
|
||||
type Signoz struct {
|
||||
// PollInterval is the interval at which the alertmanager is synced.
|
||||
PollInterval time.Duration `mapstructure:"poll_interval"`
|
||||
|
||||
// Config is the config for the alertmanager server.
|
||||
alertmanagerserver.Config `mapstructure:",squash"`
|
||||
}
|
||||
|
||||
type Legacy struct {
|
||||
// URL is the URL of the legacy alertmanager.
|
||||
URL *url.URL `mapstructure:"url"`
|
||||
// ApiURL is the URL of the legacy signoz alertmanager.
|
||||
ApiURL string `mapstructure:"api_url"`
|
||||
}
|
||||
|
||||
func NewConfigFactory() factory.ConfigFactory {
|
||||
@@ -38,14 +40,28 @@ func NewConfigFactory() factory.ConfigFactory {
|
||||
|
||||
func newConfig() factory.Config {
|
||||
return Config{
|
||||
Config: alertmanagerserver.NewConfig(),
|
||||
Provider: "signoz",
|
||||
Provider: "legacy",
|
||||
Legacy: Legacy{
|
||||
ApiURL: "http://alertmanager:9093/api",
|
||||
},
|
||||
Signoz: Signoz{
|
||||
PollInterval: 15 * time.Second,
|
||||
Config: alertmanagerserver.NewConfig(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c Config) Validate() error {
|
||||
if c.Provider == "legacy" {
|
||||
if c.Legacy.ApiURL == "" {
|
||||
return errors.New("api_url is required")
|
||||
}
|
||||
|
||||
_, err := url.Parse(c.Legacy.ApiURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("api_url %q is invalid: %w", c.Legacy.ApiURL, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
48
pkg/alertmanager/config_test.go
Normal file
48
pkg/alertmanager/config_test.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package alertmanager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.signoz.io/signoz/pkg/config"
|
||||
"go.signoz.io/signoz/pkg/config/envprovider"
|
||||
"go.signoz.io/signoz/pkg/factory"
|
||||
)
|
||||
|
||||
func TestNewWithEnvProvider(t *testing.T) {
|
||||
t.Setenv("SIGNOZ_ALERTMANAGER_PROVIDER", "legacy")
|
||||
t.Setenv("SIGNOZ_ALERTMANAGER_LEGACY_API__URL", "http://localhost:9093/api")
|
||||
|
||||
conf, err := config.New(
|
||||
context.Background(),
|
||||
config.ResolverConfig{
|
||||
Uris: []string{"env:"},
|
||||
ProviderFactories: []config.ProviderFactory{
|
||||
envprovider.NewFactory(),
|
||||
},
|
||||
},
|
||||
[]factory.ConfigFactory{
|
||||
NewConfigFactory(),
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
actual := &Config{}
|
||||
err = conf.Unmarshal("alertmanager", actual)
|
||||
require.NoError(t, err)
|
||||
|
||||
def := NewConfigFactory().New().(Config)
|
||||
|
||||
expected := &Config{
|
||||
Provider: "legacy",
|
||||
Legacy: Legacy{
|
||||
ApiURL: "http://localhost:9093/api",
|
||||
},
|
||||
Signoz: def.Signoz,
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, actual)
|
||||
assert.NoError(t, actual.Validate())
|
||||
}
|
||||
@@ -7,8 +7,10 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
"go.signoz.io/signoz/pkg/alertmanager"
|
||||
"go.signoz.io/signoz/pkg/alertmanager/alertmanagerbatcher"
|
||||
"go.signoz.io/signoz/pkg/alertmanager/alertmanagerstore/sqlalertmanagerstore"
|
||||
@@ -17,6 +19,11 @@ import (
|
||||
"go.signoz.io/signoz/pkg/types/alertmanagertypes"
|
||||
)
|
||||
|
||||
type postableAlert struct {
|
||||
*alertmanagertypes.PostableAlert
|
||||
Receivers []string `json:"receivers"`
|
||||
}
|
||||
|
||||
const (
|
||||
alertsPath string = "/v1/alerts"
|
||||
routesPath string = "/v1/routes"
|
||||
@@ -29,6 +36,7 @@ type provider struct {
|
||||
client *http.Client
|
||||
configStore alertmanagertypes.ConfigStore
|
||||
batcher *alertmanagerbatcher.Batcher
|
||||
url *url.URL
|
||||
}
|
||||
|
||||
func NewFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[alertmanager.Alertmanager, alertmanager.Config] {
|
||||
@@ -41,6 +49,11 @@ func New(ctx context.Context, providerSettings factory.ProviderSettings, config
|
||||
settings := factory.NewScopedProviderSettings(providerSettings, "go.signoz.io/signoz/pkg/alertmanager/legacyalertmanager")
|
||||
configStore := sqlalertmanagerstore.NewConfigStore(sqlstore)
|
||||
|
||||
url, err := url.Parse(config.Legacy.ApiURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &provider{
|
||||
config: config,
|
||||
settings: settings,
|
||||
@@ -49,6 +62,7 @@ func New(ctx context.Context, providerSettings factory.ProviderSettings, config
|
||||
},
|
||||
configStore: configStore,
|
||||
batcher: alertmanagerbatcher.New(settings.Logger(), alertmanagerbatcher.NewConfig()),
|
||||
url: url,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -57,22 +71,18 @@ func (provider *provider) Start(ctx context.Context) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer provider.batcher.Stop(ctx)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case alerts := <-provider.batcher.C:
|
||||
if err := provider.putAlerts(ctx, "", alerts); err != nil {
|
||||
provider.settings.Logger().Error("failed to send alerts to alertmanager", "error", err)
|
||||
}
|
||||
for alerts := range provider.batcher.C {
|
||||
if err := provider.putAlerts(ctx, "", alerts); err != nil {
|
||||
provider.settings.Logger().Error("failed to send alerts to alertmanager", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *provider) GetAlerts(ctx context.Context, orgID string, params alertmanagertypes.GettableAlertsParams) (alertmanagertypes.GettableAlerts, error) {
|
||||
url := provider.config.Legacy.URL.JoinPath(alertsPath)
|
||||
func (provider *provider) GetAlerts(ctx context.Context, orgID string, params alertmanagertypes.GettableAlertsParams) (alertmanagertypes.DeprecatedGettableAlerts, error) {
|
||||
url := provider.url.JoinPath(alertsPath)
|
||||
url.RawQuery = params.RawQuery
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url.String(), nil)
|
||||
@@ -92,23 +102,45 @@ func (provider *provider) GetAlerts(ctx context.Context, orgID string, params al
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var alerts alertmanagertypes.GettableAlerts
|
||||
if err := json.Unmarshal(body, &alerts); err != nil {
|
||||
if resp.StatusCode/100 != 2 {
|
||||
return nil, fmt.Errorf("bad response status %v", resp.Status)
|
||||
}
|
||||
|
||||
var alerts alertmanagertypes.DeprecatedGettableAlerts
|
||||
if err := json.Unmarshal([]byte(gjson.GetBytes(body, "data").Raw), &alerts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return alerts, nil
|
||||
}
|
||||
|
||||
func (provider *provider) PutAlerts(ctx context.Context, _ string, alerts alertmanagertypes.PostableAlerts) error {
|
||||
provider.batcher.Send(ctx, alerts...)
|
||||
func (provider *provider) PutAlerts(ctx context.Context, orgID string, alerts alertmanagertypes.PostableAlerts) error {
|
||||
provider.batcher.Add(ctx, alerts...)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *provider) putAlerts(ctx context.Context, _ string, alerts alertmanagertypes.PostableAlerts) error {
|
||||
url := provider.config.Legacy.URL.JoinPath(alertsPath)
|
||||
func (provider *provider) putAlerts(ctx context.Context, orgID string, alerts alertmanagertypes.PostableAlerts) error {
|
||||
cfg, err := provider.configStore.Get(ctx, orgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
body, err := json.Marshal(alerts)
|
||||
legacyAlerts := make([]postableAlert, len(alerts))
|
||||
for i, alert := range alerts {
|
||||
receivers, err := cfg.ReceiverNamesFromRuleID(alert.Alert.Labels["ruleID"])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
legacyAlerts[i] = postableAlert{
|
||||
PostableAlert: alert,
|
||||
Receivers: receivers,
|
||||
}
|
||||
}
|
||||
|
||||
url := provider.url.JoinPath(alertsPath)
|
||||
|
||||
body, err := json.Marshal(legacyAlerts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -135,7 +167,7 @@ func (provider *provider) putAlerts(ctx context.Context, _ string, alerts alertm
|
||||
}
|
||||
|
||||
func (provider *provider) TestReceiver(ctx context.Context, orgID string, receiver alertmanagertypes.Receiver) error {
|
||||
url := provider.config.Legacy.URL.JoinPath(testReceiverPath)
|
||||
url := provider.url.JoinPath(testReceiverPath)
|
||||
|
||||
body, err := json.Marshal(receiver)
|
||||
if err != nil {
|
||||
@@ -163,49 +195,74 @@ func (provider *provider) TestReceiver(ctx context.Context, orgID string, receiv
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *provider) ListChannels(ctx context.Context, orgID string) ([]*alertmanagertypes.Channel, error) {
|
||||
config, err := provider.configStore.Get(ctx, orgID)
|
||||
func (provider *provider) TestAlert(ctx context.Context, orgID string, alert *alertmanagertypes.PostableAlert, receivers []string) error {
|
||||
url := provider.url.JoinPath(alertsPath)
|
||||
|
||||
legacyAlert := postableAlert{
|
||||
PostableAlert: alert,
|
||||
Receivers: receivers,
|
||||
}
|
||||
|
||||
body, err := json.Marshal(legacyAlert)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
channels := config.Channels()
|
||||
channelList := make([]*alertmanagertypes.Channel, 0, len(channels))
|
||||
for _, channel := range channels {
|
||||
channelList = append(channelList, channel)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url.String(), bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
|
||||
resp, err := provider.client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return channelList, nil
|
||||
defer resp.Body.Close() //nolint:errcheck
|
||||
|
||||
// Any HTTP status 2xx is OK.
|
||||
if resp.StatusCode/100 != 2 {
|
||||
return fmt.Errorf("bad response status %v", resp.Status)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *provider) ListChannels(ctx context.Context, orgID string) ([]*alertmanagertypes.Channel, error) {
|
||||
return provider.configStore.ListChannels(ctx, orgID)
|
||||
}
|
||||
|
||||
func (provider *provider) ListAllChannels(ctx context.Context) ([]*alertmanagertypes.Channel, error) {
|
||||
return provider.configStore.ListAllChannels(ctx)
|
||||
}
|
||||
|
||||
func (provider *provider) GetChannelByID(ctx context.Context, orgID string, channelID int) (*alertmanagertypes.Channel, error) {
|
||||
config, err := provider.configStore.Get(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
channels := config.Channels()
|
||||
channel, err := alertmanagertypes.GetChannelByID(channels, channelID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return channel, nil
|
||||
return provider.configStore.GetChannelByID(ctx, orgID, channelID)
|
||||
}
|
||||
|
||||
func (provider *provider) UpdateChannelByReceiver(ctx context.Context, orgID string, receiver alertmanagertypes.Receiver) error {
|
||||
func (provider *provider) UpdateChannelByReceiverAndID(ctx context.Context, orgID string, receiver alertmanagertypes.Receiver, id int) error {
|
||||
channel, err := provider.configStore.GetChannelByID(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = channel.Update(receiver)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config, err := provider.configStore.Get(ctx, orgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = config.UpdateReceiver(alertmanagertypes.NewRouteFromReceiver(receiver), receiver)
|
||||
if err != nil {
|
||||
if err := config.UpdateReceiver(receiver); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = provider.configStore.Set(ctx, config, func(ctx context.Context) error {
|
||||
url := provider.config.Legacy.URL.JoinPath(routesPath)
|
||||
err = provider.configStore.UpdateChannel(ctx, orgID, channel, alertmanagertypes.WithCb(func(ctx context.Context) error {
|
||||
url := provider.url.JoinPath(routesPath)
|
||||
|
||||
body, err := json.Marshal(receiver)
|
||||
if err != nil {
|
||||
@@ -230,8 +287,12 @@ func (provider *provider) UpdateChannelByReceiver(ctx context.Context, orgID str
|
||||
return fmt.Errorf("bad response status %v", resp.Status)
|
||||
}
|
||||
|
||||
if err := provider.configStore.Set(ctx, config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -240,18 +301,19 @@ func (provider *provider) UpdateChannelByReceiver(ctx context.Context, orgID str
|
||||
}
|
||||
|
||||
func (provider *provider) CreateChannel(ctx context.Context, orgID string, receiver alertmanagertypes.Receiver) error {
|
||||
channel := alertmanagertypes.NewChannelFromReceiver(receiver, orgID)
|
||||
|
||||
config, err := provider.configStore.Get(ctx, orgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = config.CreateReceiver(alertmanagertypes.NewRouteFromReceiver(receiver), receiver)
|
||||
if err != nil {
|
||||
if err := config.CreateReceiver(receiver); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = provider.configStore.Set(ctx, config, func(ctx context.Context) error {
|
||||
url := provider.config.Legacy.URL.JoinPath(routesPath)
|
||||
return provider.configStore.CreateChannel(ctx, channel, alertmanagertypes.WithCb(func(ctx context.Context) error {
|
||||
url := provider.url.JoinPath(routesPath)
|
||||
|
||||
body, err := json.Marshal(receiver)
|
||||
if err != nil {
|
||||
@@ -276,34 +338,31 @@ func (provider *provider) CreateChannel(ctx context.Context, orgID string, recei
|
||||
return fmt.Errorf("bad response status %v", resp.Status)
|
||||
}
|
||||
|
||||
if err := provider.configStore.Set(ctx, config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
func (provider *provider) DeleteChannelByID(ctx context.Context, orgID string, channelID int) error {
|
||||
channel, err := provider.configStore.GetChannelByID(ctx, orgID, channelID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *provider) DeleteChannelByID(ctx context.Context, orgID string, channelID int) error {
|
||||
config, err := provider.configStore.Get(ctx, orgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
channels := config.Channels()
|
||||
channel, err := alertmanagertypes.GetChannelByID(channels, channelID)
|
||||
if err != nil {
|
||||
if err := config.DeleteReceiver(channel.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = config.DeleteReceiver(channel.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = provider.configStore.Set(ctx, config, func(ctx context.Context) error {
|
||||
url := provider.config.Legacy.URL.JoinPath(routesPath)
|
||||
return provider.configStore.DeleteChannelByID(ctx, orgID, channelID, alertmanagertypes.WithCb(func(ctx context.Context) error {
|
||||
url := provider.url.JoinPath(routesPath)
|
||||
|
||||
body, err := json.Marshal(map[string]string{"name": channel.Name})
|
||||
if err != nil {
|
||||
@@ -328,15 +387,23 @@ func (provider *provider) DeleteChannelByID(ctx context.Context, orgID string, c
|
||||
return fmt.Errorf("bad response status %v", resp.Status)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := provider.configStore.Set(ctx, config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return nil
|
||||
}))
|
||||
}
|
||||
|
||||
func (provider *provider) SetConfig(ctx context.Context, config *alertmanagertypes.Config) error {
|
||||
return provider.configStore.Set(ctx, config)
|
||||
}
|
||||
|
||||
func (provider *provider) Stop(ctx context.Context) error {
|
||||
provider.batcher.Stop(ctx)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *provider) GetConfig(ctx context.Context, orgID string) (*alertmanagertypes.Config, error) {
|
||||
return provider.configStore.Get(ctx, orgID)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
|
||||
type Service struct {
|
||||
// config is the config for the alertmanager service
|
||||
config Config
|
||||
config alertmanagerserver.Config
|
||||
|
||||
// stateStore is the state store for the alertmanager service
|
||||
stateStore alertmanagertypes.StateStore
|
||||
@@ -30,7 +30,7 @@ type Service struct {
|
||||
serversMtx sync.RWMutex
|
||||
}
|
||||
|
||||
func New(ctx context.Context, settings factory.ScopedProviderSettings, config Config, stateStore alertmanagertypes.StateStore, configStore alertmanagertypes.ConfigStore) *Service {
|
||||
func New(ctx context.Context, settings factory.ScopedProviderSettings, config alertmanagerserver.Config, stateStore alertmanagertypes.StateStore, configStore alertmanagertypes.ConfigStore) *Service {
|
||||
service := &Service{
|
||||
config: config,
|
||||
stateStore: stateStore,
|
||||
@@ -53,13 +53,23 @@ func (service *Service) SyncServers(ctx context.Context) error {
|
||||
for _, orgID := range orgIDs {
|
||||
config, err := service.getConfig(ctx, orgID)
|
||||
if err != nil {
|
||||
service.settings.Logger().Error("failed to get alertmanagerconfig for org", "orgID", orgID, "error", err)
|
||||
service.settings.Logger().Error("failed to get alertmanager config for org", "orgID", orgID, "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
service.servers[orgID], err = alertmanagerserver.New(ctx, service.settings.Logger(), service.settings.PrometheusRegisterer(), service.config.Config, orgID, service.stateStore)
|
||||
if err != nil {
|
||||
service.settings.Logger().Error("failed to create alertmanagerserver", "orgID", orgID, "error", err)
|
||||
// If the server is not present, create it and sync the config
|
||||
if _, ok := service.servers[orgID]; !ok {
|
||||
server, err := service.newServer(ctx, orgID)
|
||||
if err != nil {
|
||||
service.settings.Logger().Error("failed to create alertmanager server", "orgID", orgID, "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
service.servers[orgID] = server
|
||||
}
|
||||
|
||||
if service.servers[orgID].Hash() == config.StoreableConfig().Hash {
|
||||
service.settings.Logger().Debug("skipping alertmanager sync for org", "orgID", orgID, "hash", config.StoreableConfig().Hash)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -74,13 +84,18 @@ func (service *Service) SyncServers(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *Service) GetAlerts(ctx context.Context, orgID string, params alertmanagertypes.GettableAlertsParams) (alertmanagertypes.GettableAlerts, error) {
|
||||
func (service *Service) GetAlerts(ctx context.Context, orgID string, params alertmanagertypes.GettableAlertsParams) (alertmanagertypes.DeprecatedGettableAlerts, error) {
|
||||
server, err := service.getServer(orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return server.GetAlerts(ctx, params)
|
||||
alerts, err := server.GetAlerts(ctx, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return alertmanagertypes.NewDeprecatedGettableAlertsFromGettableAlerts(alerts), nil
|
||||
}
|
||||
|
||||
func (service *Service) PutAlerts(ctx context.Context, orgID string, alerts alertmanagertypes.PostableAlerts) error {
|
||||
@@ -101,6 +116,15 @@ func (service *Service) TestReceiver(ctx context.Context, orgID string, receiver
|
||||
return server.TestReceiver(ctx, receiver)
|
||||
}
|
||||
|
||||
func (service *Service) TestAlert(ctx context.Context, orgID string, alert *alertmanagertypes.PostableAlert, receivers []string) error {
|
||||
server, err := service.getServer(orgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return server.TestAlert(ctx, alert, receivers)
|
||||
}
|
||||
|
||||
func (service *Service) Stop(ctx context.Context) error {
|
||||
for _, server := range service.servers {
|
||||
server.Stop(ctx)
|
||||
@@ -109,6 +133,36 @@ func (service *Service) Stop(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *Service) newServer(ctx context.Context, orgID string) (*alertmanagerserver.Server, error) {
|
||||
config, err := service.getConfig(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
server, err := alertmanagerserver.New(ctx, service.settings.Logger(), service.settings.PrometheusRegisterer(), service.config, orgID, service.stateStore)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
beforeCompareAndSelectHash := config.StoreableConfig().Hash
|
||||
config, err = service.compareAndSelectConfig(ctx, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if beforeCompareAndSelectHash == config.StoreableConfig().Hash {
|
||||
service.settings.Logger().Debug("skipping config store update for org", "orgID", orgID, "hash", config.StoreableConfig().Hash)
|
||||
return server, nil
|
||||
}
|
||||
|
||||
err = service.configStore.Set(ctx, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return server, nil
|
||||
}
|
||||
|
||||
func (service *Service) getConfig(ctx context.Context, orgID string) (*alertmanagertypes.Config, error) {
|
||||
config, err := service.configStore.Get(ctx, orgID)
|
||||
if err != nil {
|
||||
@@ -121,13 +175,55 @@ func (service *Service) getConfig(ctx context.Context, orgID string) (*alertmana
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return config, err
|
||||
config.SetGlobalConfig(service.config.Global)
|
||||
if config.AlertmanagerConfig().Route == nil {
|
||||
config.SetRouteConfig(service.config.Route)
|
||||
} else {
|
||||
config.UpdateRouteConfig(service.config.Route)
|
||||
}
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// compareAndSelectConfig compares the existing config with the config derived from channels.
|
||||
// If the hash of the config and the channels mismatch, the config derived from channels is returned.
|
||||
func (service *Service) compareAndSelectConfig(ctx context.Context, incomingConfig *alertmanagertypes.Config) (*alertmanagertypes.Config, error) {
|
||||
channels, err := service.configStore.ListChannels(ctx, incomingConfig.StoreableConfig().OrgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
matchers, err := service.configStore.GetMatchers(ctx, incomingConfig.StoreableConfig().OrgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config, err := alertmanagertypes.NewConfigFromChannels(service.config.Global, service.config.Route, channels, incomingConfig.StoreableConfig().OrgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for ruleID, receivers := range matchers {
|
||||
err = config.CreateRuleIDMatcher(ruleID, receivers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if incomingConfig.StoreableConfig().Hash != config.StoreableConfig().Hash {
|
||||
service.settings.Logger().InfoContext(ctx, "mismatch found, updating config to match channels and matchers")
|
||||
return config, nil
|
||||
}
|
||||
|
||||
return incomingConfig, nil
|
||||
|
||||
}
|
||||
|
||||
func (service *Service) getServer(orgID string) (*alertmanagerserver.Server, error) {
|
||||
service.serversMtx.RLock()
|
||||
defer service.serversMtx.RUnlock()
|
||||
|
||||
server, ok := service.servers[orgID]
|
||||
if !ok {
|
||||
return nil, errors.Newf(errors.TypeNotFound, ErrCodeAlertmanagerNotFound, "alertmanager not found for org %s", orgID)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"go.signoz.io/signoz/pkg/alertmanager"
|
||||
"go.signoz.io/signoz/pkg/alertmanager/alertmanagerstore/sqlalertmanagerstore"
|
||||
"go.signoz.io/signoz/pkg/errors"
|
||||
"go.signoz.io/signoz/pkg/factory"
|
||||
"go.signoz.io/signoz/pkg/sqlstore"
|
||||
"go.signoz.io/signoz/pkg/types/alertmanagertypes"
|
||||
@@ -17,6 +18,7 @@ type provider struct {
|
||||
settings factory.ScopedProviderSettings
|
||||
configStore alertmanagertypes.ConfigStore
|
||||
stateStore alertmanagertypes.StateStore
|
||||
stopC chan struct{}
|
||||
}
|
||||
|
||||
func NewFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[alertmanager.Alertmanager, alertmanager.Config] {
|
||||
@@ -26,15 +28,15 @@ func NewFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[alertmanager
|
||||
}
|
||||
|
||||
func New(ctx context.Context, providerSettings factory.ProviderSettings, config alertmanager.Config, sqlstore sqlstore.SQLStore) (*provider, error) {
|
||||
settings := factory.NewScopedProviderSettings(providerSettings, "go.signoz.io/signoz/pkg/alertmanager/internalalertmanager")
|
||||
settings := factory.NewScopedProviderSettings(providerSettings, "go.signoz.io/signoz/pkg/alertmanager/signozalertmanager")
|
||||
configStore := sqlalertmanagerstore.NewConfigStore(sqlstore)
|
||||
stateStore := sqlalertmanagerstore.NewStateStore(sqlstore)
|
||||
|
||||
return &provider{
|
||||
p := &provider{
|
||||
service: alertmanager.New(
|
||||
ctx,
|
||||
settings,
|
||||
config,
|
||||
config.Signoz.Config,
|
||||
stateStore,
|
||||
configStore,
|
||||
),
|
||||
@@ -42,15 +44,23 @@ func New(ctx context.Context, providerSettings factory.ProviderSettings, config
|
||||
config: config,
|
||||
configStore: configStore,
|
||||
stateStore: stateStore,
|
||||
}, nil
|
||||
stopC: make(chan struct{}),
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (provider *provider) Start(ctx context.Context) error {
|
||||
if err := provider.service.SyncServers(ctx); err != nil {
|
||||
provider.settings.Logger().ErrorContext(ctx, "failed to sync alertmanager servers", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(provider.config.Signoz.PollInterval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case <-provider.stopC:
|
||||
return nil
|
||||
case <-ticker.C:
|
||||
if err := provider.service.SyncServers(ctx); err != nil {
|
||||
@@ -61,10 +71,11 @@ func (provider *provider) Start(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (provider *provider) Stop(ctx context.Context) error {
|
||||
close(provider.stopC)
|
||||
return provider.service.Stop(ctx)
|
||||
}
|
||||
|
||||
func (provider *provider) GetAlerts(ctx context.Context, orgID string, params alertmanagertypes.GettableAlertsParams) (alertmanagertypes.GettableAlerts, error) {
|
||||
func (provider *provider) GetAlerts(ctx context.Context, orgID string, params alertmanagertypes.GettableAlertsParams) (alertmanagertypes.DeprecatedGettableAlerts, error) {
|
||||
return provider.service.GetAlerts(ctx, orgID, params)
|
||||
}
|
||||
|
||||
@@ -76,78 +87,64 @@ func (provider *provider) TestReceiver(ctx context.Context, orgID string, receiv
|
||||
return provider.service.TestReceiver(ctx, orgID, receiver)
|
||||
}
|
||||
|
||||
func (provider *provider) TestAlert(ctx context.Context, orgID string, alert *alertmanagertypes.PostableAlert, receivers []string) error {
|
||||
return provider.service.TestAlert(ctx, orgID, alert, receivers)
|
||||
}
|
||||
|
||||
func (provider *provider) ListChannels(ctx context.Context, orgID string) ([]*alertmanagertypes.Channel, error) {
|
||||
config, err := provider.configStore.Get(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return provider.configStore.ListChannels(ctx, orgID)
|
||||
}
|
||||
|
||||
channels := config.Channels()
|
||||
channelList := make([]*alertmanagertypes.Channel, 0, len(channels))
|
||||
for _, channel := range channels {
|
||||
channelList = append(channelList, channel)
|
||||
}
|
||||
|
||||
return channelList, nil
|
||||
func (provider *provider) ListAllChannels(ctx context.Context) ([]*alertmanagertypes.Channel, error) {
|
||||
return nil, errors.Newf(errors.TypeUnsupported, errors.CodeUnsupported, "not supported by provider signoz")
|
||||
}
|
||||
|
||||
func (provider *provider) GetChannelByID(ctx context.Context, orgID string, channelID int) (*alertmanagertypes.Channel, error) {
|
||||
config, err := provider.configStore.Get(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
channels := config.Channels()
|
||||
channel, err := alertmanagertypes.GetChannelByID(channels, channelID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return channel, nil
|
||||
return provider.configStore.GetChannelByID(ctx, orgID, channelID)
|
||||
}
|
||||
|
||||
func (provider *provider) UpdateChannelByReceiver(ctx context.Context, orgID string, receiver alertmanagertypes.Receiver) error {
|
||||
func (provider *provider) UpdateChannelByReceiverAndID(ctx context.Context, orgID string, receiver alertmanagertypes.Receiver, id int) error {
|
||||
channel, err := provider.configStore.GetChannelByID(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := channel.Update(receiver); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config, err := provider.configStore.Get(ctx, orgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = config.UpdateReceiver(alertmanagertypes.NewRouteFromReceiver(receiver), receiver)
|
||||
if err != nil {
|
||||
if err := config.UpdateReceiver(receiver); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = provider.configStore.Set(ctx, config, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return provider.configStore.UpdateChannel(ctx, orgID, channel, alertmanagertypes.WithCb(func(ctx context.Context) error {
|
||||
return provider.configStore.Set(ctx, config)
|
||||
}))
|
||||
}
|
||||
|
||||
func (provider *provider) DeleteChannelByID(ctx context.Context, orgID string, channelID int) error {
|
||||
channel, err := provider.configStore.GetChannelByID(ctx, orgID, channelID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config, err := provider.configStore.Get(ctx, orgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
channels := config.Channels()
|
||||
channel, err := alertmanagertypes.GetChannelByID(channels, channelID)
|
||||
if err != nil {
|
||||
if err := config.DeleteReceiver(channel.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = config.DeleteReceiver(channel.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = provider.configStore.Set(ctx, config, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return provider.configStore.DeleteChannelByID(ctx, orgID, channelID, alertmanagertypes.WithCb(func(ctx context.Context) error {
|
||||
return provider.configStore.Set(ctx, config)
|
||||
}))
|
||||
}
|
||||
|
||||
func (provider *provider) CreateChannel(ctx context.Context, orgID string, receiver alertmanagertypes.Receiver) error {
|
||||
@@ -156,15 +153,20 @@ func (provider *provider) CreateChannel(ctx context.Context, orgID string, recei
|
||||
return err
|
||||
}
|
||||
|
||||
err = config.CreateReceiver(alertmanagertypes.NewRouteFromReceiver(receiver), receiver)
|
||||
if err != nil {
|
||||
if err := config.CreateReceiver(receiver); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = provider.configStore.Set(ctx, config, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
channel := alertmanagertypes.NewChannelFromReceiver(receiver, orgID)
|
||||
return provider.configStore.CreateChannel(ctx, channel, alertmanagertypes.WithCb(func(ctx context.Context) error {
|
||||
return provider.configStore.Set(ctx, config)
|
||||
}))
|
||||
}
|
||||
|
||||
func (provider *provider) SetConfig(ctx context.Context, config *alertmanagertypes.Config) error {
|
||||
return provider.configStore.Set(ctx, config)
|
||||
}
|
||||
|
||||
func (provider *provider) GetConfig(ctx context.Context, orgID string) (*alertmanagertypes.Config, error) {
|
||||
return provider.configStore.Get(ctx, orgID)
|
||||
}
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"go.signoz.io/signoz/pkg/query-service/dao"
|
||||
"go.signoz.io/signoz/pkg/query-service/model"
|
||||
"go.signoz.io/signoz/pkg/types/authtypes"
|
||||
)
|
||||
|
||||
func (aH *APIHandler) setApdexSettings(w http.ResponseWriter, r *http.Request) {
|
||||
claims, ok := authtypes.ClaimsFromContext(r.Context())
|
||||
if !ok {
|
||||
RespondError(w, &model.ApiError{Err: errors.New("unauthorized"), Typ: model.ErrorUnauthorized}, nil)
|
||||
return
|
||||
}
|
||||
req, err := parseSetApdexScoreRequest(r)
|
||||
if aH.HandleError(w, err, http.StatusBadRequest) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := dao.DB().SetApdexSettings(context.Background(), req); err != nil {
|
||||
if err := dao.DB().SetApdexSettings(r.Context(), claims.OrgID, req); err != nil {
|
||||
RespondError(w, &model.ApiError{Err: err, Typ: model.ErrorInternal}, nil)
|
||||
return
|
||||
}
|
||||
@@ -25,7 +31,12 @@ func (aH *APIHandler) setApdexSettings(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (aH *APIHandler) getApdexSettings(w http.ResponseWriter, r *http.Request) {
|
||||
services := r.URL.Query().Get("services")
|
||||
apdexSet, err := dao.DB().GetApdexSettings(context.Background(), strings.Split(strings.TrimSpace(services), ","))
|
||||
claims, ok := authtypes.ClaimsFromContext(r.Context())
|
||||
if !ok {
|
||||
RespondError(w, &model.ApiError{Err: errors.New("unauthorized"), Typ: model.ErrorUnauthorized}, nil)
|
||||
return
|
||||
}
|
||||
apdexSet, err := dao.DB().GetApdexSettings(r.Context(), claims.OrgID, strings.Split(strings.TrimSpace(services), ","))
|
||||
if err != nil {
|
||||
RespondError(w, &model.ApiError{Err: err, Typ: model.ErrorInternal}, nil)
|
||||
return
|
||||
|
||||
@@ -9,13 +9,14 @@ import (
|
||||
"go.signoz.io/signoz/pkg/query-service/auth"
|
||||
"go.signoz.io/signoz/pkg/query-service/constants"
|
||||
"go.signoz.io/signoz/pkg/query-service/model"
|
||||
"go.signoz.io/signoz/pkg/types"
|
||||
)
|
||||
|
||||
type AuthMiddleware struct {
|
||||
GetUserFromRequest func(r context.Context) (*model.UserPayload, error)
|
||||
GetUserFromRequest func(r context.Context) (*types.GettableUser, error)
|
||||
}
|
||||
|
||||
func NewAuthMiddleware(f func(ctx context.Context) (*model.UserPayload, error)) *AuthMiddleware {
|
||||
func NewAuthMiddleware(f func(ctx context.Context) (*types.GettableUser, error)) *AuthMiddleware {
|
||||
return &AuthMiddleware{
|
||||
GetUserFromRequest: f,
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ func (c *Controller) GenerateConnectionUrl(
|
||||
}
|
||||
|
||||
// TODO(Raj): parameterized this in follow up changes
|
||||
agentVersion := "0.0.1"
|
||||
agentVersion := "0.0.2"
|
||||
|
||||
connectionUrl := fmt.Sprintf(
|
||||
"https://%s.console.aws.amazon.com/cloudformation/home?region=%s#/stacks/quickcreate?",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user