Compare commits

...

37 Commits

Author SHA1 Message Date
Prashant Shahi
3f7adeb040 Merge branch 'develop' into release/v0.58.x 2024-11-13 12:01:54 +05:30
Srikanth Chekuri
323da3494b chore: add experimental rate/increase calc (#6432) 2024-11-13 11:47:56 +05:30
Vikrant Gupta
01fda51959 chore: return proper http codes on unique constraint error (#6428) 2024-11-13 00:25:00 +05:30
Srikanth Chekuri
85ac21f253 fix: update request payload for span metrics queries (#6323) 2024-11-12 17:22:42 +00:00
Srikanth Chekuri
fd9e9f0fb3 chore: add k8s {deployment, daemonset, statefulset, job} resources (#6401) 2024-11-12 15:23:40 +00:00
Nityananda Gohain
d5523fc092 fix: ignore ts for panel type table (#6419) 2024-11-12 08:04:45 +00:00
Nityananda Gohain
2ec641b99e fix: add severity_text legend (#6415) 2024-11-12 05:54:22 +00:00
Ekansh Gupta
d1503f1418 feat: fixProducerAPI (#6422)
chore: bugfix
2024-11-12 05:30:36 +00:00
Vikrant Gupta
e974e9d47f feat: consume the new licenses v3 structure. (#6341)
* feat: setup for licenses v3 integration

* feat: added some more logic

* feat: validator changes

* chore: added a couple of todos

* feat: added config parameter for licenses v3 and the boot option

* feat: some typo fix

* feat: added refresh licenses handler

* feat: handle the start manager license activation

* chore: text updates

* feat: added list licenses call

* chore: refactor the entire code to cleanup interfaces

* fix: nil pointer error

* chore: some minor edits

* feat: model changes

* feat: model changes

* fix: utilise factory pattern

* feat: added default basic plan

* chore: added test cases for new license function

* feat: added more test cases

* chore: make the licenses id not null

* feat: cosmetic changes

* feat: cosmetic changes

* feat: update zeus URL

* chore: license testing fixes

* feat: added license status and category handling for query-service

* chore: added v3 support in v2 endpoint

* chore: http response codes and some code cleanup

* chore: added detailed test cases

* chore: address review comments

* chore: some misc cleanup
2024-11-12 01:40:10 +05:30
Shaheer Kochai
577a169508 feat: alert rename interaction (#6208)
* feat: alert rename interaction

* feat: add support for enter and escape shortcuts to submit and cancel rename

* chore: add missing alert field

* chore: update the style similar to dashboard rename

* refactor: remove buttonProps

* chore: add missing alert property to fix the build

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2024-11-11 19:13:07 +00:00
Shaheer Kochai
939e2a3570 fix: fix the issue of adding new query in new alert page changing the data source (#6286)
Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2024-11-11 07:34:20 +00:00
Yunus M
b64326070c [Snyk] Fix for 2 vulnerabilities (#6215)
* fix: frontend/package.json & frontend/yarn.lock to reduce vulnerabilities

The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-UPLOT-6209224
- https://snyk.io/vuln/SNYK-JS-VUETEMPLATECOMPILER-8219888

* chore: upgrade design tokens to 1.1.3

---------

Co-authored-by: snyk-bot <snyk-bot@snyk.io>
Co-authored-by: ahmadshaheer <ashaheerki@gmail.com>
2024-11-11 06:45:43 +00:00
SagarRajput-7
63872983c6 feat: added metric page in messaging queues (#6399)
* feat: added metric page in messaging queues

* feat: added misc fixes

* feat: removed a class name from mqcards

* feat: added lightMode styles for kafka 2.0 (#6400)

* feat: resolved comments and used strings
2024-11-09 13:04:43 +05:30
Srikanth Chekuri
831540eaf0 fix: test notification missing for anomaly alert (#6391) 2024-11-08 15:40:09 +00:00
Prashant Shahi
48f3b9cacb chore(signoz): pin versions: SigNoz 0.58.0, SigNoz OtelCollector 0.111.8
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-11-08 16:02:59 +05:30
Prashant Shahi
eaf8571fe9 Merge branch 'main' into release/v0.58.x 2024-11-08 15:49:41 +05:30
Nityananda Gohain
22c10f9479 Issue 6367 (#6376)
* fix: issue with orderby by materialized column

* fix: tests

* fix: order by issue in old explorer as well
2024-11-08 07:05:32 +00:00
Yunus M
e748fb0655 chore: update events for onboarding part 2 (#6397) 2024-11-08 06:52:39 +00:00
SagarRajput-7
fdc54a62a9 fix: kafka - misc fix and features (#6379)
* feat: fixed multiple fixes and chores in kafka 2.0

* feat: fixed producer latency - producer-detail call

* feat: fixed mq-detail page layout and pagination

* feat: resolved comments
2024-11-07 23:49:47 +05:30
SagarRajput-7
abe0ab69b0 feat: added kafka - scenario - 4 - drop rate table (#6380)
* feat: added kafka - scenario - 4 - drop rate table

* feat: added api, new table and traceid redirection

* feat: code refactor
2024-11-07 23:37:54 +05:30
Shaheer Kochai
e623c92615 fix: adding the key requires double enter before it gets added as label key after the first label (#6296) 2024-11-07 15:03:59 +00:00
Shaheer Kochai
dc5917db01 chore: setup router compatibility package (#6285) 2024-11-07 19:02:23 +04:30
dependabot[bot]
d6a7f0b6f4 chore(deps): bump express from 4.19.2 to 4.21.1 in /frontend (#6166)
Bumps [express](https://github.com/expressjs/express) from 4.19.2 to 4.21.1.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.21.1/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.19.2...4.21.1)

---
updated-dependencies:
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-07 11:48:05 +05:30
Vikrant Gupta
471803115e feat: added support for instrumentation scope in logs (#6378)
* feat: added support for instrumentation scope in logs

* chore: remove console logs

* fix: the logic for rendering prefix

* feat: address review comments

---------

Co-authored-by: SagarRajput-7 <162284829+SagarRajput-7@users.noreply.github.com>
2024-11-07 11:47:35 +05:30
SagarRajput-7
8403a3362d feat: corrected the handling of relativeTime as null in alertHistory page (#6392) 2024-11-07 02:34:11 +05:30
Nityananda Gohain
64d46bc855 feat: support for scope in logs old and new qb (#6339) 2024-11-06 20:47:04 +05:30
SagarRajput-7
c9fee27604 feat: updated the design for Messaging Queue - summary section (#6319)
* feat: updated the design for Messaging Queue - summary section

* feat: resolved comments

* feat: added better logic for switch options and resolved query
2024-11-06 14:44:39 +05:30
SagarRajput-7
f1b6b2d3d8 feat: added onboarding detail for consumer setup (#6372)
* feat: added onboarding detail for consumer setup

* feat: corrected spelling

* feat: reorders imports correctly
2024-11-06 14:43:54 +05:30
SagarRajput-7
468f056530 feat: added kafka - scenario - 4 (#6370)
* feat: added kafka - scenario - 4

* feat: added error handling and correct api handler for kafka apis
2024-11-06 14:24:38 +05:30
SagarRajput-7
7086470ce2 feat: added healthcheck and attribute checklist component for Kafka (#6371)
* feat: added healthcheck and attribute checklist component for Kafka

* feat: corrected the onboardingapi payload

* feat: added missing configuration button at overview and onboarding flow
2024-11-06 14:23:51 +05:30
Yunus M
352296c6cd fix: initialize target to 3 in anomaly detection alert (#6362) 2024-11-05 22:13:12 +05:30
SagarRajput-7
975307a8b8 feat: added onboarding setup for Producer for Messaging queues (#6236)
* fix: added onboarding setup for producer/consumer for Messaging queues

* fix: polled onboarding status api

* feat: added onboarding status api with useQueury functions and updated endpoints

* feat: added onboarding status api util for attribute data

* feat: refactoring and url query changes

* feat: changed start and end time to nanosecond for api payload

* feat: added comment description
2024-11-05 19:40:23 +05:30
SagarRajput-7
12377be809 feat: added generic UI for scenario 1,3,4 (#6287)
* feat: added generic table component for scenario 1,3,4

* feat: added generic logic for mq detail tables and consumed for sc-1,2

* feat: added overview and details table for scenario-3

* feat: added table row clicks func

* feat: resolved comments
2024-11-05 19:26:41 +05:30
Prashant Shahi
b10c22223b Merge pull request #6300 from SigNoz/release/signoz-0.57.0
Release/signoz 0.57.0
2024-10-29 00:45:14 +05:30
Prashant Shahi
cdde369748 Merge branch 'develop' into release/signoz-0.57.0 2024-10-28 21:26:47 +05:30
Prashant Shahi
523cbcd6fc chore: go mod tidy
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-10-28 19:40:23 +05:30
Prashant Shahi
eeadc021e1 chore(signoz): pin versions: SigNoz 0.57.0
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-10-28 19:40:10 +05:30
129 changed files with 8671 additions and 1172 deletions

View File

@@ -146,7 +146,7 @@ services:
condition: on-failure
query-service:
image: signoz/query-service:0.56.0
image: signoz/query-service:0.58.0
command:
[
"-config=/root/config/prometheus.yml",
@@ -186,7 +186,7 @@ services:
<<: *db-depend
frontend:
image: signoz/frontend:0.56.0
image: signoz/frontend:0.58.0
deploy:
restart_policy:
condition: on-failure
@@ -199,7 +199,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
image: signoz/signoz-otel-collector:0.111.5
image: signoz/signoz-otel-collector:0.111.8
command:
[
"--config=/etc/otel-collector-config.yaml",
@@ -237,7 +237,7 @@ services:
- query-service
otel-collector-migrator:
image: signoz/signoz-schema-migrator:0.111.5
image: signoz/signoz-schema-migrator:0.111.8
deploy:
restart_policy:
condition: on-failure

View File

@@ -69,7 +69,7 @@ services:
- --storage.path=/data
otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.5}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.8}
container_name: otel-migrator
command:
- "--dsn=tcp://clickhouse:9000"
@@ -84,7 +84,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
otel-collector:
container_name: signoz-otel-collector
image: signoz/signoz-otel-collector:0.111.5
image: signoz/signoz-otel-collector:0.111.8
command:
[
"--config=/etc/otel-collector-config.yaml",

View File

@@ -162,7 +162,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
query-service:
image: signoz/query-service:${DOCKER_TAG:-0.56.0}
image: signoz/query-service:${DOCKER_TAG:-0.58.0}
container_name: signoz-query-service
command:
[
@@ -201,7 +201,7 @@ services:
<<: *db-depend
frontend:
image: signoz/frontend:${DOCKER_TAG:-0.56.0}
image: signoz/frontend:${DOCKER_TAG:-0.58.0}
container_name: signoz-frontend
restart: on-failure
depends_on:
@@ -213,7 +213,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector-migrator-sync:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.5}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.8}
container_name: otel-migrator-sync
command:
- "sync"
@@ -228,7 +228,7 @@ services:
# condition: service_healthy
otel-collector-migrator-async:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.5}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.8}
container_name: otel-migrator-async
command:
- "async"
@@ -245,7 +245,7 @@ services:
# condition: service_healthy
otel-collector:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.5}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.8}
container_name: signoz-otel-collector
command:
[

View File

@@ -167,7 +167,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
query-service:
image: signoz/query-service:${DOCKER_TAG:-0.56.0}
image: signoz/query-service:${DOCKER_TAG:-0.58.0}
container_name: signoz-query-service
command:
[
@@ -208,7 +208,7 @@ services:
<<: *db-depend
frontend:
image: signoz/frontend:${DOCKER_TAG:-0.56.0}
image: signoz/frontend:${DOCKER_TAG:-0.58.0}
container_name: signoz-frontend
restart: on-failure
depends_on:
@@ -220,7 +220,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.5}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.8}
container_name: otel-migrator
command:
- "--dsn=tcp://clickhouse:9000"
@@ -234,7 +234,7 @@ services:
otel-collector:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.5}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.8}
container_name: signoz-otel-collector
command:
[

View File

@@ -40,6 +40,7 @@ type APIHandlerOptions struct {
// Querier Influx Interval
FluxInterval time.Duration
UseLogsNewSchema bool
UseLicensesV3 bool
}
type APIHandler struct {
@@ -65,6 +66,7 @@ func NewAPIHandler(opts APIHandlerOptions) (*APIHandler, error) {
Cache: opts.Cache,
FluxInterval: opts.FluxInterval,
UseLogsNewSchema: opts.UseLogsNewSchema,
UseLicensesV3: opts.UseLicensesV3,
})
if err != nil {
@@ -173,10 +175,25 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *baseapp.AuthMiddlew
router.HandleFunc("/api/v1/dashboards/{uuid}/lock", am.EditAccess(ah.lockDashboard)).Methods(http.MethodPut)
router.HandleFunc("/api/v1/dashboards/{uuid}/unlock", am.EditAccess(ah.unlockDashboard)).Methods(http.MethodPut)
// v2
router.HandleFunc("/api/v2/licenses",
am.ViewAccess(ah.listLicensesV2)).
Methods(http.MethodGet)
// v3
router.HandleFunc("/api/v3/licenses",
am.ViewAccess(ah.listLicensesV3)).
Methods(http.MethodGet)
router.HandleFunc("/api/v3/licenses",
am.AdminAccess(ah.applyLicenseV3)).
Methods(http.MethodPost)
router.HandleFunc("/api/v3/licenses",
am.AdminAccess(ah.refreshLicensesV3)).
Methods(http.MethodPut)
// v4
router.HandleFunc("/api/v4/query_range", am.ViewAccess(ah.queryRangeV4)).Methods(http.MethodPost)
// Gateway

View File

@@ -9,6 +9,7 @@ import (
"go.signoz.io/signoz/ee/query-service/constants"
"go.signoz.io/signoz/ee/query-service/model"
"go.signoz.io/signoz/pkg/http/render"
"go.uber.org/zap"
)
@@ -59,6 +60,21 @@ type billingDetails struct {
} `json:"data"`
}
type ApplyLicenseRequest struct {
LicenseKey string `json:"key"`
}
type ListLicenseResponse map[string]interface{}
func convertLicenseV3ToListLicenseResponse(licensesV3 []*model.LicenseV3) []ListLicenseResponse {
listLicenses := []ListLicenseResponse{}
for _, license := range licensesV3 {
listLicenses = append(listLicenses, license.Data)
}
return listLicenses
}
func (ah *APIHandler) listLicenses(w http.ResponseWriter, r *http.Request) {
licenses, apiError := ah.LM().GetLicenses(context.Background())
if apiError != nil {
@@ -88,6 +104,51 @@ func (ah *APIHandler) applyLicense(w http.ResponseWriter, r *http.Request) {
ah.Respond(w, license)
}
func (ah *APIHandler) listLicensesV3(w http.ResponseWriter, r *http.Request) {
licenses, apiError := ah.LM().GetLicensesV3(r.Context())
if apiError != nil {
RespondError(w, apiError, nil)
return
}
ah.Respond(w, convertLicenseV3ToListLicenseResponse(licenses))
}
// this function is called by zeus when inserting licenses in the query-service
func (ah *APIHandler) applyLicenseV3(w http.ResponseWriter, r *http.Request) {
var licenseKey ApplyLicenseRequest
if err := json.NewDecoder(r.Body).Decode(&licenseKey); err != nil {
RespondError(w, model.BadRequest(err), nil)
return
}
if licenseKey.LicenseKey == "" {
RespondError(w, model.BadRequest(fmt.Errorf("license key is required")), nil)
return
}
_, apiError := ah.LM().ActivateV3(r.Context(), licenseKey.LicenseKey)
if apiError != nil {
RespondError(w, apiError, nil)
return
}
render.Success(w, http.StatusAccepted, nil)
}
func (ah *APIHandler) refreshLicensesV3(w http.ResponseWriter, r *http.Request) {
apiError := ah.LM().RefreshLicense(r.Context())
if apiError != nil {
RespondError(w, apiError, nil)
return
}
render.Success(w, http.StatusNoContent, nil)
}
func (ah *APIHandler) checkout(w http.ResponseWriter, r *http.Request) {
type checkoutResponse struct {
@@ -154,11 +215,45 @@ func (ah *APIHandler) getBilling(w http.ResponseWriter, r *http.Request) {
ah.Respond(w, billingResponse.Data)
}
func convertLicenseV3ToLicenseV2(licenses []*model.LicenseV3) []model.License {
licensesV2 := []model.License{}
for _, l := range licenses {
licenseV2 := model.License{
Key: l.Key,
ActivationId: "",
PlanDetails: "",
FeatureSet: l.Features,
ValidationMessage: "",
IsCurrent: l.IsCurrent,
LicensePlan: model.LicensePlan{
PlanKey: l.PlanName,
ValidFrom: l.ValidFrom,
ValidUntil: l.ValidUntil,
Status: l.Status},
}
licensesV2 = append(licensesV2, licenseV2)
}
return licensesV2
}
func (ah *APIHandler) listLicensesV2(w http.ResponseWriter, r *http.Request) {
licenses, apiError := ah.LM().GetLicenses(context.Background())
if apiError != nil {
RespondError(w, apiError, nil)
var licenses []model.License
if ah.UseLicensesV3 {
licensesV3, err := ah.LM().GetLicensesV3(r.Context())
if err != nil {
RespondError(w, err, nil)
return
}
licenses = convertLicenseV3ToLicenseV2(licensesV3)
} else {
_licenses, apiError := ah.LM().GetLicenses(r.Context())
if apiError != nil {
RespondError(w, apiError, nil)
return
}
licenses = _licenses
}
resp := model.Licenses{

View File

@@ -31,7 +31,6 @@ import (
"go.signoz.io/signoz/ee/query-service/rules"
baseauth "go.signoz.io/signoz/pkg/query-service/auth"
"go.signoz.io/signoz/pkg/query-service/migrate"
"go.signoz.io/signoz/pkg/query-service/model"
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
licensepkg "go.signoz.io/signoz/ee/query-service/license"
@@ -78,6 +77,7 @@ type ServerOptions struct {
Cluster string
GatewayUrl string
UseLogsNewSchema bool
UseLicensesV3 bool
}
// Server runs HTTP api service
@@ -134,7 +134,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
}
// initiate license manager
lm, err := licensepkg.StartManager("sqlite", localDB)
lm, err := licensepkg.StartManager("sqlite", localDB, serverOptions.UseLicensesV3)
if err != nil {
return nil, err
}
@@ -270,6 +270,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
FluxInterval: fluxInterval,
Gateway: gatewayProxy,
UseLogsNewSchema: serverOptions.UseLogsNewSchema,
UseLicensesV3: serverOptions.UseLicensesV3,
}
apiHandler, err := api.NewAPIHandler(apiOpts)
@@ -348,7 +349,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e
}
if user.User.OrgId == "" {
return nil, model.UnauthorizedError(errors.New("orgId is missing in the claims"))
return nil, basemodel.UnauthorizedError(errors.New("orgId is missing in the claims"))
}
return user, nil
@@ -765,8 +766,9 @@ func makeRulesManager(
Cache: cache,
EvalDelay: baseconst.GetEvalDelay(),
PrepareTaskFunc: rules.PrepareTaskFunc,
UseLogsNewSchema: useLogsNewSchema,
PrepareTaskFunc: rules.PrepareTaskFunc,
PrepareTestRuleFunc: rules.TestNotification,
UseLogsNewSchema: useLogsNewSchema,
}
// create Manager

View File

@@ -13,6 +13,7 @@ var LicenseAPIKey = GetOrDefaultEnv("SIGNOZ_LICENSE_API_KEY", "")
var SaasSegmentKey = GetOrDefaultEnv("SIGNOZ_SAAS_SEGMENT_KEY", "")
var FetchFeatures = GetOrDefaultEnv("FETCH_FEATURES", "false")
var ZeusFeaturesURL = GetOrDefaultEnv("ZEUS_FEATURES_URL", "ZeusFeaturesURL")
var ZeusURL = GetOrDefaultEnv("ZEUS_URL", "ZeusURL")
func GetOrDefaultEnv(key string, fallback string) string {
v := os.Getenv(key)

View File

@@ -13,3 +13,8 @@ type ActivationResponse struct {
ActivationId string `json:"ActivationId"`
PlanDetails string `json:"PlanDetails"`
}
type ValidateLicenseResponse struct {
Status status `json:"status"`
Data map[string]interface{} `json:"data"`
}

View File

@@ -7,6 +7,7 @@ import (
"fmt"
"io"
"net/http"
"time"
"github.com/pkg/errors"
"go.uber.org/zap"
@@ -23,12 +24,14 @@ const (
)
type Client struct {
Prefix string
Prefix string
GatewayUrl string
}
func New() *Client {
return &Client{
Prefix: constants.LicenseSignozIo,
Prefix: constants.LicenseSignozIo,
GatewayUrl: constants.ZeusURL,
}
}
@@ -116,6 +119,60 @@ func ValidateLicense(activationId string) (*ActivationResponse, *model.ApiError)
}
func ValidateLicenseV3(licenseKey string) (*model.LicenseV3, *model.ApiError) {
// Creating an HTTP client with a timeout for better control
client := &http.Client{
Timeout: 10 * time.Second,
}
req, err := http.NewRequest("GET", C.GatewayUrl+"/v2/licenses/me", nil)
if err != nil {
return nil, model.BadRequest(errors.Wrap(err, fmt.Sprintf("failed to create request: %w", err)))
}
// Setting the custom header
req.Header.Set("X-Signoz-Cloud-Api-Key", licenseKey)
response, err := client.Do(req)
if err != nil {
return nil, model.BadRequest(errors.Wrap(err, fmt.Sprintf("failed to make post request: %w", err)))
}
body, err := io.ReadAll(response.Body)
if err != nil {
return nil, model.BadRequest(errors.Wrap(err, fmt.Sprintf("failed to read validation response from %v", C.GatewayUrl)))
}
defer response.Body.Close()
switch response.StatusCode {
case 200:
a := ValidateLicenseResponse{}
err = json.Unmarshal(body, &a)
if err != nil {
return nil, model.BadRequest(errors.Wrap(err, "failed to marshal license validation response"))
}
license, err := model.NewLicenseV3(a.Data)
if err != nil {
return nil, model.BadRequest(errors.Wrap(err, "failed to generate new license v3"))
}
return license, nil
case 400:
return nil, model.BadRequest(errors.Wrap(fmt.Errorf(string(body)),
fmt.Sprintf("bad request error received from %v", C.GatewayUrl)))
case 401:
return nil, model.Unauthorized(errors.Wrap(fmt.Errorf(string(body)),
fmt.Sprintf("unauthorized request error received from %v", C.GatewayUrl)))
default:
return nil, model.InternalError(errors.Wrap(fmt.Errorf(string(body)),
fmt.Sprintf("internal request error received from %v", C.GatewayUrl)))
}
}
func NewPostRequestWithCtx(ctx context.Context, url string, contentType string, body io.Reader) (*http.Request, error) {
req, err := http.NewRequestWithContext(ctx, POST, url, body)
if err != nil {

View File

@@ -3,10 +3,12 @@ package license
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"time"
"github.com/jmoiron/sqlx"
"github.com/mattn/go-sqlite3"
"go.signoz.io/signoz/ee/query-service/license/sqlite"
"go.signoz.io/signoz/ee/query-service/model"
@@ -48,6 +50,34 @@ func (r *Repo) GetLicenses(ctx context.Context) ([]model.License, error) {
return licenses, nil
}
func (r *Repo) GetLicensesV3(ctx context.Context) ([]*model.LicenseV3, error) {
licensesData := []model.LicenseDB{}
licenseV3Data := []*model.LicenseV3{}
query := "SELECT id,key,data FROM licenses_v3"
err := r.db.Select(&licensesData, query)
if err != nil {
return nil, fmt.Errorf("failed to get licenses from db: %v", err)
}
for _, l := range licensesData {
var licenseData map[string]interface{}
err := json.Unmarshal([]byte(l.Data), &licenseData)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal data into licenseData : %v", err)
}
license, err := model.NewLicenseV3WithIDAndKey(l.ID, l.Key, licenseData)
if err != nil {
return nil, fmt.Errorf("failed to get licenses v3 schema : %v", err)
}
licenseV3Data = append(licenseV3Data, license)
}
return licenseV3Data, nil
}
// GetActiveLicense fetches the latest active license from DB.
// If the license is not present, expect a nil license and a nil error in the output.
func (r *Repo) GetActiveLicense(ctx context.Context) (*model.License, *basemodel.ApiError) {
@@ -79,6 +109,45 @@ func (r *Repo) GetActiveLicense(ctx context.Context) (*model.License, *basemodel
return active, nil
}
func (r *Repo) GetActiveLicenseV3(ctx context.Context) (*model.LicenseV3, error) {
var err error
licenses := []model.LicenseDB{}
query := "SELECT id,key,data FROM licenses_v3"
err = r.db.Select(&licenses, query)
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf("failed to get active licenses from db: %v", err))
}
var active *model.LicenseV3
for _, l := range licenses {
var licenseData map[string]interface{}
err := json.Unmarshal([]byte(l.Data), &licenseData)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal data into licenseData : %v", err)
}
license, err := model.NewLicenseV3WithIDAndKey(l.ID, l.Key, licenseData)
if err != nil {
return nil, fmt.Errorf("failed to get licenses v3 schema : %v", err)
}
if active == nil &&
(license.ValidFrom != 0) &&
(license.ValidUntil == -1 || license.ValidUntil > time.Now().Unix()) {
active = license
}
if active != nil &&
license.ValidFrom > active.ValidFrom &&
(license.ValidUntil == -1 || license.ValidUntil > time.Now().Unix()) {
active = license
}
}
return active, nil
}
// InsertLicense inserts a new license in db
func (r *Repo) InsertLicense(ctx context.Context, l *model.License) error {
@@ -204,3 +273,59 @@ func (r *Repo) InitFeatures(req basemodel.FeatureSet) error {
}
return nil
}
// InsertLicenseV3 inserts a new license v3 in db
func (r *Repo) InsertLicenseV3(ctx context.Context, l *model.LicenseV3) *model.ApiError {
query := `INSERT INTO licenses_v3 (id, key, data) VALUES ($1, $2, $3)`
// licsense is the entity of zeus so putting the entire license here without defining schema
licenseData, err := json.Marshal(l.Data)
if err != nil {
return &model.ApiError{Typ: basemodel.ErrorBadData, Err: err}
}
_, err = r.db.ExecContext(ctx,
query,
l.ID,
l.Key,
string(licenseData),
)
if err != nil {
if sqliteErr, ok := err.(sqlite3.Error); ok {
if sqliteErr.ExtendedCode == sqlite3.ErrConstraintUnique {
zap.L().Error("error in inserting license data: ", zap.Error(sqliteErr))
return &model.ApiError{Typ: model.ErrorConflict, Err: sqliteErr}
}
}
zap.L().Error("error in inserting license data: ", zap.Error(err))
return &model.ApiError{Typ: basemodel.ErrorExec, Err: err}
}
return nil
}
// UpdateLicenseV3 updates a new license v3 in db
func (r *Repo) UpdateLicenseV3(ctx context.Context, l *model.LicenseV3) error {
// the key and id for the license can't change so only update the data here!
query := `UPDATE licenses_v3 SET data=$1 WHERE id=$2;`
license, err := json.Marshal(l.Data)
if err != nil {
return fmt.Errorf("insert license failed: license marshal error")
}
_, err = r.db.ExecContext(ctx,
query,
license,
l.ID,
)
if err != nil {
zap.L().Error("error in updating license data: ", zap.Error(err))
return fmt.Errorf("failed to update license in db: %v", err)
}
return nil
}

View File

@@ -7,6 +7,7 @@ import (
"time"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
"sync"
@@ -45,11 +46,12 @@ type Manager struct {
failedAttempts uint64
// keep track of active license and features
activeLicense *model.License
activeFeatures basemodel.FeatureSet
activeLicense *model.License
activeLicenseV3 *model.LicenseV3
activeFeatures basemodel.FeatureSet
}
func StartManager(dbType string, db *sqlx.DB, features ...basemodel.Feature) (*Manager, error) {
func StartManager(dbType string, db *sqlx.DB, useLicensesV3 bool, features ...basemodel.Feature) (*Manager, error) {
if LM != nil {
return LM, nil
}
@@ -65,7 +67,7 @@ func StartManager(dbType string, db *sqlx.DB, features ...basemodel.Feature) (*M
repo: &repo,
}
if err := m.start(features...); err != nil {
if err := m.start(useLicensesV3, features...); err != nil {
return m, err
}
LM = m
@@ -73,8 +75,14 @@ func StartManager(dbType string, db *sqlx.DB, features ...basemodel.Feature) (*M
}
// start loads active license in memory and initiates validator
func (lm *Manager) start(features ...basemodel.Feature) error {
err := lm.LoadActiveLicense(features...)
func (lm *Manager) start(useLicensesV3 bool, features ...basemodel.Feature) error {
var err error
if useLicensesV3 {
err = lm.LoadActiveLicenseV3(features...)
} else {
err = lm.LoadActiveLicense(features...)
}
return err
}
@@ -108,6 +116,31 @@ func (lm *Manager) SetActive(l *model.License, features ...basemodel.Feature) {
go lm.Validator(context.Background())
}
}
func (lm *Manager) SetActiveV3(l *model.LicenseV3, features ...basemodel.Feature) {
lm.mutex.Lock()
defer lm.mutex.Unlock()
if l == nil {
return
}
lm.activeLicenseV3 = l
lm.activeFeatures = append(l.Features, features...)
// set default features
setDefaultFeatures(lm)
err := lm.InitFeatures(lm.activeFeatures)
if err != nil {
zap.L().Panic("Couldn't activate features", zap.Error(err))
}
if !lm.validatorRunning {
// we want to make sure only one validator runs,
// we already have lock() so good to go
lm.validatorRunning = true
go lm.ValidatorV3(context.Background())
}
}
func setDefaultFeatures(lm *Manager) {
@@ -137,6 +170,28 @@ func (lm *Manager) LoadActiveLicense(features ...basemodel.Feature) error {
return nil
}
func (lm *Manager) LoadActiveLicenseV3(features ...basemodel.Feature) error {
active, err := lm.repo.GetActiveLicenseV3(context.Background())
if err != nil {
return err
}
if active != nil {
lm.SetActiveV3(active, features...)
} else {
zap.L().Info("No active license found, defaulting to basic plan")
// if no active license is found, we default to basic(free) plan with all default features
lm.activeFeatures = model.BasicPlan
setDefaultFeatures(lm)
err := lm.InitFeatures(lm.activeFeatures)
if err != nil {
zap.L().Error("Couldn't initialize features", zap.Error(err))
return err
}
}
return nil
}
func (lm *Manager) GetLicenses(ctx context.Context) (response []model.License, apiError *model.ApiError) {
licenses, err := lm.repo.GetLicenses(ctx)
@@ -163,6 +218,23 @@ func (lm *Manager) GetLicenses(ctx context.Context) (response []model.License, a
return
}
func (lm *Manager) GetLicensesV3(ctx context.Context) (response []*model.LicenseV3, apiError *model.ApiError) {
licenses, err := lm.repo.GetLicensesV3(ctx)
if err != nil {
return nil, model.InternalError(err)
}
for _, l := range licenses {
if lm.activeLicenseV3 != nil && l.Key == lm.activeLicenseV3.Key {
l.IsCurrent = true
}
response = append(response, l)
}
return response, nil
}
// Validator validates license after an epoch of time
func (lm *Manager) Validator(ctx context.Context) {
defer close(lm.terminated)
@@ -187,6 +259,30 @@ func (lm *Manager) Validator(ctx context.Context) {
}
}
// Validator validates license after an epoch of time
func (lm *Manager) ValidatorV3(ctx context.Context) {
defer close(lm.terminated)
tick := time.NewTicker(validationFrequency)
defer tick.Stop()
lm.ValidateV3(ctx)
for {
select {
case <-lm.done:
return
default:
select {
case <-lm.done:
return
case <-tick.C:
lm.ValidateV3(ctx)
}
}
}
}
// Validate validates the current active license
func (lm *Manager) Validate(ctx context.Context) (reterr error) {
zap.L().Info("License validation started")
@@ -254,6 +350,54 @@ func (lm *Manager) Validate(ctx context.Context) (reterr error) {
return nil
}
// todo[vikrantgupta25]: check the comparison here between old and new license!
func (lm *Manager) RefreshLicense(ctx context.Context) *model.ApiError {
license, apiError := validate.ValidateLicenseV3(lm.activeLicenseV3.Key)
if apiError != nil {
zap.L().Error("failed to validate license", zap.Error(apiError.Err))
return apiError
}
err := lm.repo.UpdateLicenseV3(ctx, license)
if err != nil {
return model.BadRequest(errors.Wrap(err, "failed to update the new license"))
}
lm.SetActiveV3(license)
return nil
}
func (lm *Manager) ValidateV3(ctx context.Context) (reterr error) {
zap.L().Info("License validation started")
if lm.activeLicenseV3 == nil {
return nil
}
defer func() {
lm.mutex.Lock()
lm.lastValidated = time.Now().Unix()
if reterr != nil {
zap.L().Error("License validation completed with error", zap.Error(reterr))
atomic.AddUint64(&lm.failedAttempts, 1)
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_LICENSE_CHECK_FAILED,
map[string]interface{}{"err": reterr.Error()}, "", true, false)
} else {
zap.L().Info("License validation completed with no errors")
}
lm.mutex.Unlock()
}()
err := lm.RefreshLicense(ctx)
if err != nil {
return err
}
return nil
}
// Activate activates a license key with signoz server
func (lm *Manager) Activate(ctx context.Context, key string) (licenseResponse *model.License, errResponse *model.ApiError) {
defer func() {
@@ -298,6 +442,35 @@ func (lm *Manager) Activate(ctx context.Context, key string) (licenseResponse *m
return l, nil
}
func (lm *Manager) ActivateV3(ctx context.Context, licenseKey string) (licenseResponse *model.LicenseV3, errResponse *model.ApiError) {
defer func() {
if errResponse != nil {
userEmail, err := auth.GetEmailFromJwt(ctx)
if err == nil {
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_LICENSE_ACT_FAILED,
map[string]interface{}{"err": errResponse.Err.Error()}, userEmail, true, false)
}
}
}()
license, apiError := validate.ValidateLicenseV3(licenseKey)
if apiError != nil {
zap.L().Error("failed to get the license", zap.Error(apiError.Err))
return nil, apiError
}
// insert the new license to the sqlite db
err := lm.repo.InsertLicenseV3(ctx, license)
if err != nil {
zap.L().Error("failed to activate license", zap.Error(err))
return nil, err
}
// license is valid, activate it
lm.SetActiveV3(license)
return license, nil
}
// CheckFeature will be internally used by backend routines
// for feature gating
func (lm *Manager) CheckFeature(featureKey string) error {

View File

@@ -48,5 +48,16 @@ func InitDB(db *sqlx.DB) error {
return fmt.Errorf("error in creating feature_status table: %s", err.Error())
}
table_schema = `CREATE TABLE IF NOT EXISTS licenses_v3 (
id TEXT PRIMARY KEY,
key TEXT NOT NULL UNIQUE,
data TEXT
);`
_, err = db.Exec(table_schema)
if err != nil {
return fmt.Errorf("error in creating licenses_v3 table: %s", err.Error())
}
return nil
}

View File

@@ -94,6 +94,7 @@ func main() {
var cluster string
var useLogsNewSchema bool
var useLicensesV3 bool
var cacheConfigPath, fluxInterval string
var enableQueryServiceLogOTLPExport bool
var preferSpanMetrics bool
@@ -104,6 +105,7 @@ func main() {
var gatewayUrl string
flag.BoolVar(&useLogsNewSchema, "use-logs-new-schema", false, "use logs_v2 schema for logs")
flag.BoolVar(&useLicensesV3, "use-licenses-v3", false, "use licenses_v3 schema for licenses")
flag.StringVar(&promConfigPath, "config", "./config/prometheus.yml", "(prometheus config to read metrics)")
flag.StringVar(&skipTopLvlOpsPath, "skip-top-level-ops", "", "(config file to skip top level operations)")
flag.BoolVar(&disableRules, "rules.disable", false, "(disable rule evaluation)")
@@ -143,6 +145,7 @@ func main() {
Cluster: cluster,
GatewayUrl: gatewayUrl,
UseLogsNewSchema: useLogsNewSchema,
UseLicensesV3: useLicensesV3,
}
// Read the jwt secret key

View File

@@ -46,6 +46,13 @@ func BadRequest(err error) *ApiError {
}
}
func Unauthorized(err error) *ApiError {
return &ApiError{
Typ: basemodel.ErrorUnauthorized,
Err: err,
}
}
// BadRequestStr returns a ApiError object of bad request for string input
func BadRequestStr(s string) *ApiError {
return &ApiError{

View File

@@ -3,6 +3,8 @@ package model
import (
"encoding/base64"
"encoding/json"
"fmt"
"reflect"
"time"
"github.com/pkg/errors"
@@ -104,3 +106,144 @@ type SubscriptionServerResp struct {
Status string `json:"status"`
Data Licenses `json:"data"`
}
type Plan struct {
Name string `json:"name"`
}
type LicenseDB struct {
ID string `json:"id"`
Key string `json:"key"`
Data string `json:"data"`
}
type LicenseV3 struct {
ID string
Key string
Data map[string]interface{}
PlanName string
Features basemodel.FeatureSet
Status string
IsCurrent bool
ValidFrom int64
ValidUntil int64
}
func extractKeyFromMapStringInterface[T any](data map[string]interface{}, key string) (T, error) {
var zeroValue T
if val, ok := data[key]; ok {
if value, ok := val.(T); ok {
return value, nil
}
return zeroValue, fmt.Errorf("%s key is not a valid %s", key, reflect.TypeOf(zeroValue))
}
return zeroValue, fmt.Errorf("%s key is missing", key)
}
func NewLicenseV3(data map[string]interface{}) (*LicenseV3, error) {
var features basemodel.FeatureSet
// extract id from data
licenseID, err := extractKeyFromMapStringInterface[string](data, "id")
if err != nil {
return nil, err
}
delete(data, "id")
// extract key from data
licenseKey, err := extractKeyFromMapStringInterface[string](data, "key")
if err != nil {
return nil, err
}
delete(data, "key")
// extract status from data
status, err := extractKeyFromMapStringInterface[string](data, "status")
if err != nil {
return nil, err
}
planMap, err := extractKeyFromMapStringInterface[map[string]any](data, "plan")
if err != nil {
return nil, err
}
planName, err := extractKeyFromMapStringInterface[string](planMap, "name")
if err != nil {
return nil, err
}
// if license status is inactive then default it to basic
if status == LicenseStatusInactive {
planName = PlanNameBasic
}
featuresFromZeus := basemodel.FeatureSet{}
if _features, ok := data["features"]; ok {
featuresData, err := json.Marshal(_features)
if err != nil {
return nil, errors.Wrap(err, "failed to marshal features data")
}
if err := json.Unmarshal(featuresData, &featuresFromZeus); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal features data")
}
}
switch planName {
case PlanNameTeams:
features = append(features, ProPlan...)
case PlanNameEnterprise:
features = append(features, EnterprisePlan...)
case PlanNameBasic:
features = append(features, BasicPlan...)
default:
features = append(features, BasicPlan...)
}
if len(featuresFromZeus) > 0 {
for _, feature := range featuresFromZeus {
exists := false
for i, existingFeature := range features {
if existingFeature.Name == feature.Name {
features[i] = feature // Replace existing feature
exists = true
break
}
}
if !exists {
features = append(features, feature) // Append if it doesn't exist
}
}
}
data["features"] = features
_validFrom, err := extractKeyFromMapStringInterface[float64](data, "valid_from")
if err != nil {
_validFrom = 0
}
validFrom := int64(_validFrom)
_validUntil, err := extractKeyFromMapStringInterface[float64](data, "valid_until")
if err != nil {
_validUntil = 0
}
validUntil := int64(_validUntil)
return &LicenseV3{
ID: licenseID,
Key: licenseKey,
Data: data,
PlanName: planName,
Features: features,
ValidFrom: validFrom,
ValidUntil: validUntil,
Status: status,
}, nil
}
func NewLicenseV3WithIDAndKey(id string, key string, data map[string]interface{}) (*LicenseV3, error) {
licenseDataWithIdAndKey := data
licenseDataWithIdAndKey["id"] = id
licenseDataWithIdAndKey["key"] = key
return NewLicenseV3(licenseDataWithIdAndKey)
}

View File

@@ -0,0 +1,170 @@
package model
import (
"encoding/json"
"testing"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.signoz.io/signoz/pkg/query-service/model"
)
func TestNewLicenseV3(t *testing.T) {
testCases := []struct {
name string
data []byte
pass bool
expected *LicenseV3
error error
}{
{
name: "Error for missing license id",
data: []byte(`{}`),
pass: false,
error: errors.New("id key is missing"),
},
{
name: "Error for license id not being a valid string",
data: []byte(`{"id": 10}`),
pass: false,
error: errors.New("id key is not a valid string"),
},
{
name: "Error for missing license key",
data: []byte(`{"id":"does-not-matter"}`),
pass: false,
error: errors.New("key key is missing"),
},
{
name: "Error for invalid string license key",
data: []byte(`{"id":"does-not-matter","key":10}`),
pass: false,
error: errors.New("key key is not a valid string"),
},
{
name: "Error for missing license status",
data: []byte(`{"id":"does-not-matter", "key": "does-not-matter","category":"FREE"}`),
pass: false,
error: errors.New("status key is missing"),
},
{
name: "Error for invalid string license status",
data: []byte(`{"id":"does-not-matter","key": "does-not-matter", "category":"FREE", "status":10}`),
pass: false,
error: errors.New("status key is not a valid string"),
},
{
name: "Error for missing license plan",
data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"ACTIVE"}`),
pass: false,
error: errors.New("plan key is missing"),
},
{
name: "Error for invalid json license plan",
data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"ACTIVE","plan":10}`),
pass: false,
error: errors.New("plan key is not a valid map[string]interface {}"),
},
{
name: "Error for invalid license plan",
data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"ACTIVE","plan":{}}`),
pass: false,
error: errors.New("name key is missing"),
},
{
name: "Parse the entire license properly",
data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"ACTIVE","plan":{"name":"TEAMS"},"valid_from": 1730899309,"valid_until": -1}`),
pass: true,
expected: &LicenseV3{
ID: "does-not-matter",
Key: "does-not-matter-key",
Data: map[string]interface{}{
"plan": map[string]interface{}{
"name": "TEAMS",
},
"category": "FREE",
"status": "ACTIVE",
"valid_from": float64(1730899309),
"valid_until": float64(-1),
},
PlanName: PlanNameTeams,
ValidFrom: 1730899309,
ValidUntil: -1,
Status: "ACTIVE",
IsCurrent: false,
Features: model.FeatureSet{},
},
},
{
name: "Fallback to basic plan if license status is inactive",
data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"INACTIVE","plan":{"name":"TEAMS"},"valid_from": 1730899309,"valid_until": -1}`),
pass: true,
expected: &LicenseV3{
ID: "does-not-matter",
Key: "does-not-matter-key",
Data: map[string]interface{}{
"plan": map[string]interface{}{
"name": "TEAMS",
},
"category": "FREE",
"status": "INACTIVE",
"valid_from": float64(1730899309),
"valid_until": float64(-1),
},
PlanName: PlanNameBasic,
ValidFrom: 1730899309,
ValidUntil: -1,
Status: "INACTIVE",
IsCurrent: false,
Features: model.FeatureSet{},
},
},
{
name: "fallback states for validFrom and validUntil",
data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"ACTIVE","plan":{"name":"TEAMS"},"valid_from":1234.456,"valid_until":5678.567}`),
pass: true,
expected: &LicenseV3{
ID: "does-not-matter",
Key: "does-not-matter-key",
Data: map[string]interface{}{
"plan": map[string]interface{}{
"name": "TEAMS",
},
"valid_from": 1234.456,
"valid_until": 5678.567,
"category": "FREE",
"status": "ACTIVE",
},
PlanName: PlanNameTeams,
ValidFrom: 1234,
ValidUntil: 5678,
Status: "ACTIVE",
IsCurrent: false,
Features: model.FeatureSet{},
},
},
}
for _, tc := range testCases {
var licensePayload map[string]interface{}
err := json.Unmarshal(tc.data, &licensePayload)
require.NoError(t, err)
license, err := NewLicenseV3(licensePayload)
if license != nil {
license.Features = make(model.FeatureSet, 0)
delete(license.Data, "features")
}
if tc.pass {
require.NoError(t, err)
require.NotNil(t, license)
assert.Equal(t, tc.expected, license)
} else {
require.Error(t, err)
assert.EqualError(t, err, tc.error.Error())
require.Nil(t, license)
}
}
}

View File

@@ -9,6 +9,17 @@ const SSO = "SSO"
const Basic = "BASIC_PLAN"
const Pro = "PRO_PLAN"
const Enterprise = "ENTERPRISE_PLAN"
var (
PlanNameEnterprise = "ENTERPRISE"
PlanNameTeams = "TEAMS"
PlanNameBasic = "BASIC"
)
var (
LicenseStatusInactive = "INACTIVE"
)
const DisableUpsell = "DISABLE_UPSELL"
const Onboarding = "ONBOARDING"
const ChatSupport = "CHAT_SUPPORT"

View File

@@ -1,10 +1,15 @@
package rules
import (
"context"
"fmt"
"time"
"github.com/google/uuid"
basemodel "go.signoz.io/signoz/pkg/query-service/model"
baserules "go.signoz.io/signoz/pkg/query-service/rules"
"go.signoz.io/signoz/pkg/query-service/utils/labels"
"go.uber.org/zap"
)
func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error) {
@@ -79,6 +84,106 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
return task, nil
}
// TestNotification prepares a dummy rule for given rule parameters and
// sends a test notification. returns alert count and error (if any)
func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.ApiError) {
ctx := context.Background()
if opts.Rule == nil {
return 0, basemodel.BadRequest(fmt.Errorf("rule is required"))
}
parsedRule := opts.Rule
var alertname = parsedRule.AlertName
if alertname == "" {
// alertname is not mandatory for testing, so picking
// a random string here
alertname = uuid.New().String()
}
// append name to indicate this is test alert
parsedRule.AlertName = fmt.Sprintf("%s%s", alertname, baserules.TestAlertPostFix)
var rule baserules.Rule
var err error
if parsedRule.RuleType == baserules.RuleTypeThreshold {
// add special labels for test alerts
parsedRule.Annotations[labels.AlertSummaryLabel] = fmt.Sprintf("The rule threshold is set to %.4f, and the observed metric value is {{$value}}.", *parsedRule.RuleCondition.Target)
parsedRule.Labels[labels.RuleSourceLabel] = ""
parsedRule.Labels[labels.AlertRuleIdLabel] = ""
// create a threshold rule
rule, err = baserules.NewThresholdRule(
alertname,
parsedRule,
opts.FF,
opts.Reader,
opts.UseLogsNewSchema,
baserules.WithSendAlways(),
baserules.WithSendUnmatched(),
)
if err != nil {
zap.L().Error("failed to prepare a new threshold rule for test", zap.String("name", rule.Name()), zap.Error(err))
return 0, basemodel.BadRequest(err)
}
} else if parsedRule.RuleType == baserules.RuleTypeProm {
// create promql rule
rule, err = baserules.NewPromRule(
alertname,
parsedRule,
opts.Logger,
opts.Reader,
opts.ManagerOpts.PqlEngine,
baserules.WithSendAlways(),
baserules.WithSendUnmatched(),
)
if err != nil {
zap.L().Error("failed to prepare a new promql rule for test", zap.String("name", rule.Name()), zap.Error(err))
return 0, basemodel.BadRequest(err)
}
} else if parsedRule.RuleType == baserules.RuleTypeAnomaly {
// create anomaly rule
rule, err = NewAnomalyRule(
alertname,
parsedRule,
opts.FF,
opts.Reader,
opts.Cache,
baserules.WithSendAlways(),
baserules.WithSendUnmatched(),
)
if err != nil {
zap.L().Error("failed to prepare a new anomaly rule for test", zap.String("name", rule.Name()), zap.Error(err))
return 0, basemodel.BadRequest(err)
}
} else {
return 0, basemodel.BadRequest(fmt.Errorf("failed to derive ruletype with given information"))
}
// set timestamp to current utc time
ts := time.Now().UTC()
count, err := rule.Eval(ctx, ts)
if err != nil {
zap.L().Error("evaluating rule failed", zap.String("rule", rule.Name()), zap.Error(err))
return 0, basemodel.InternalError(fmt.Errorf("rule evaluation failed"))
}
alertsFound, ok := count.(int)
if !ok {
return 0, basemodel.InternalError(fmt.Errorf("something went wrong"))
}
rule.SendAlerts(ctx, ts, 0, time.Duration(1*time.Minute), opts.NotifyFunc)
return alertsFound, nil
}
// newTask returns an appropriate group for
// rule type
func newTask(taskType baserules.TaskType, name string, frequency time.Duration, rules []baserules.Rule, opts *baserules.ManagerOptions, notify baserules.NotifyFunc, ruleDB baserules.RuleDB) baserules.Task {

View File

@@ -42,7 +42,7 @@
"@radix-ui/react-tooltip": "1.0.7",
"@sentry/react": "7.102.1",
"@sentry/webpack-plugin": "2.16.0",
"@signozhq/design-tokens": "0.0.8",
"@signozhq/design-tokens": "1.1.4",
"@uiw/react-md-editor": "3.23.5",
"@visx/group": "3.3.0",
"@visx/shape": "3.5.0",
@@ -87,6 +87,8 @@
"lodash-es": "^4.17.21",
"lucide-react": "0.379.0",
"mini-css-extract-plugin": "2.4.5",
"overlayscrollbars": "^2.8.1",
"overlayscrollbars-react": "^0.5.6",
"papaparse": "5.4.1",
"posthog-js": "1.160.3",
"rc-tween-one": "3.0.6",
@@ -107,11 +109,10 @@
"react-query": "3.39.3",
"react-redux": "^7.2.2",
"react-router-dom": "^5.2.0",
"react-router-dom-v5-compat": "6.27.0",
"react-syntax-highlighter": "15.5.0",
"react-use": "^17.3.2",
"react-virtuoso": "4.0.3",
"overlayscrollbars-react": "^0.5.6",
"overlayscrollbars": "^2.8.1",
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
"rehype-raw": "7.0.0",

View File

@@ -0,0 +1,24 @@
{
"metricGraphCategory": {
"brokerMetrics": {
"title": "Broker Metrics",
"description": "The Kafka Broker metrics here inform you of data loss/delay through unclean leader elections and network throughputs, as well as request fails through request purgatories and timeouts metrics"
},
"consumerMetrics": {
"title": "Consumer Metrics",
"description": "Kafka Consumer metrics provide insights into lag between message production and consumption, success rates and latency of message delivery, and the volume of data consumed."
},
"producerMetrics": {
"title": "Producer Metrics",
"description": "Kafka Producers send messages to brokers for storage and distribution by topic. These metrics inform you of the volume and rate of data sent, and the success rate of message delivery."
},
"brokerJVMMetrics": {
"title": "Broker JVM Metrics",
"description": "Kafka brokers are Java applications that expose JVM metrics to inform on the broker's system health. Garbage collection metrics like those below provide key insights into free memory, broker performance, and heap size. You need to enable new_gc_metrics for this section to populate."
},
"partitionMetrics": {
"title": "Partition Metrics",
"description": "Kafka partitions are the unit of parallelism in Kafka. These metrics inform you of the number of partitions per topic, the current offset of each partition, the oldest offset, and the number of in-sync replicas."
}
}
}

View File

@@ -1,30 +1,54 @@
{
"breadcrumb": "Messaging Queues",
"header": "Kafka / Overview",
"overview": {
"title": "Start sending data in as little as 20 minutes",
"subtitle": "Connect and Monitor Your Data Streams"
},
"configureConsumer": {
"title": "Configure Consumer",
"description": "Add consumer data sources to gain insights and enhance monitoring.",
"button": "Get Started"
},
"configureProducer": {
"title": "Configure Producer",
"description": "Add producer data sources to gain insights and enhance monitoring.",
"button": "Get Started"
},
"monitorKafka": {
"title": "Monitor kafka",
"description": "Add your Kafka source to gain insights and enhance activity tracking.",
"button": "Get Started"
},
"summarySection": {
"viewDetailsButton": "View Details"
},
"confirmModal": {
"content": "Before navigating to the details page, please make sure you have configured all the required setup to ensure correct data monitoring.",
"okText": "Proceed"
}
}
"breadcrumb": "Messaging Queues",
"header": "Kafka / Overview",
"overview": {
"title": "Start sending data in as little as 20 minutes",
"subtitle": "Connect and Monitor Your Data Streams"
},
"configureConsumer": {
"title": "Configure Consumer",
"description": "Add consumer data sources to gain insights and enhance monitoring.",
"button": "Get Started"
},
"configureProducer": {
"title": "Configure Producer",
"description": "Add producer data sources to gain insights and enhance monitoring.",
"button": "Get Started"
},
"monitorKafka": {
"title": "Monitor kafka",
"description": "Add your Kafka source to gain insights and enhance activity tracking.",
"button": "Get Started"
},
"summarySection": {
"viewDetailsButton": "View Details",
"consumer": {
"title": "Consumer lag view",
"description": "Connect and Monitor Your Data Streams"
},
"producer": {
"title": "Producer latency view",
"description": "Connect and Monitor Your Data Streams"
},
"partition": {
"title": "Partition Latency view",
"description": "Connect and Monitor Your Data Streams"
},
"dropRate": {
"title": "Drop Rate view",
"description": "Connect and Monitor Your Data Streams"
},
"metricPage": {
"title": "Metric View",
"description": "Connect and Monitor Your Data Streams"
}
},
"confirmModal": {
"content": "Before navigating to the details page, please make sure you have configured all the required setup to ensure correct data monitoring.",
"okText": "Proceed"
},
"overviewSummarySection": {
"title": "Monitor Your Data Streams",
"subtitle": "Monitor key Kafka metrics like consumer lag and latency to ensure efficient data flow and troubleshoot in real time."
}
}

View File

@@ -0,0 +1,24 @@
{
"metricGraphCategory": {
"brokerMetrics": {
"title": "Broker Metrics",
"description": "The Kafka Broker metrics here inform you of data loss/delay through unclean leader elections and network throughputs, as well as request fails through request purgatories and timeouts metrics"
},
"consumerMetrics": {
"title": "Consumer Metrics",
"description": "Kafka Consumer metrics provide insights into lag between message production and consumption, success rates and latency of message delivery, and the volume of data consumed."
},
"producerMetrics": {
"title": "Producer Metrics",
"description": "Kafka Producers send messages to brokers for storage and distribution by topic. These metrics inform you of the volume and rate of data sent, and the success rate of message delivery."
},
"brokerJVMMetrics": {
"title": "Broker JVM Metrics",
"description": "Kafka brokers are Java applications that expose JVM metrics to inform on the broker's system health. Garbage collection metrics like those below provide key insights into free memory, broker performance, and heap size. You need to enable new_gc_metrics for this section to populate."
},
"partitionMetrics": {
"title": "Partition Metrics",
"description": "Kafka partitions are the unit of parallelism in Kafka. These metrics inform you of the number of partitions per topic, the current offset of each partition, the oldest offset, and the number of in-sync replicas."
}
}
}

View File

@@ -1,30 +1,54 @@
{
"breadcrumb": "Messaging Queues",
"header": "Kafka / Overview",
"overview": {
"title": "Start sending data in as little as 20 minutes",
"subtitle": "Connect and Monitor Your Data Streams"
},
"configureConsumer": {
"title": "Configure Consumer",
"description": "Add consumer data sources to gain insights and enhance monitoring.",
"button": "Get Started"
},
"configureProducer": {
"title": "Configure Producer",
"description": "Add producer data sources to gain insights and enhance monitoring.",
"button": "Get Started"
},
"monitorKafka": {
"title": "Monitor kafka",
"description": "Add your Kafka source to gain insights and enhance activity tracking.",
"button": "Get Started"
},
"summarySection": {
"viewDetailsButton": "View Details"
},
"confirmModal": {
"content": "Before navigating to the details page, please make sure you have configured all the required setup to ensure correct data monitoring.",
"okText": "Proceed"
}
}
"breadcrumb": "Messaging Queues",
"header": "Kafka / Overview",
"overview": {
"title": "Start sending data in as little as 20 minutes",
"subtitle": "Connect and Monitor Your Data Streams"
},
"configureConsumer": {
"title": "Configure Consumer",
"description": "Add consumer data sources to gain insights and enhance monitoring.",
"button": "Get Started"
},
"configureProducer": {
"title": "Configure Producer",
"description": "Add producer data sources to gain insights and enhance monitoring.",
"button": "Get Started"
},
"monitorKafka": {
"title": "Monitor kafka",
"description": "Add your Kafka source to gain insights and enhance activity tracking.",
"button": "Get Started"
},
"summarySection": {
"viewDetailsButton": "View Details",
"consumer": {
"title": "Consumer lag view",
"description": "Connect and Monitor Your Data Streams"
},
"producer": {
"title": "Producer latency view",
"description": "Connect and Monitor Your Data Streams"
},
"partition": {
"title": "Partition Latency view",
"description": "Connect and Monitor Your Data Streams"
},
"dropRate": {
"title": "Drop Rate view",
"description": "Connect and Monitor Your Data Streams"
},
"metricPage": {
"title": "Metric View",
"description": "Connect and Monitor Your Data Streams"
}
},
"confirmModal": {
"content": "Before navigating to the details page, please make sure you have configured all the required setup to ensure correct data monitoring.",
"okText": "Proceed"
},
"overviewSummarySection": {
"title": "Monitor Your Data Streams",
"subtitle": "Monitor key Kafka metrics like consumer lag and latency to ensure efficient data flow and troubleshoot in real time."
}
}

View File

@@ -28,6 +28,7 @@ import { Suspense, useEffect, useState } from 'react';
import { useQuery } from 'react-query';
import { useDispatch, useSelector } from 'react-redux';
import { Route, Router, Switch } from 'react-router-dom';
import { CompatRouter } from 'react-router-dom-v5-compat';
import { Dispatch } from 'redux';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
@@ -292,36 +293,38 @@ function App(): JSX.Element {
return (
<ConfigProvider theme={themeConfig}>
<Router history={history}>
<NotificationProvider>
<PrivateRoute>
<ResourceProvider>
<QueryBuilderProvider>
<DashboardProvider>
<KeyboardHotkeysProvider>
<AlertRuleProvider>
<AppLayout>
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
<Switch>
{routes.map(({ path, component, exact }) => (
<Route
key={`${path}`}
exact={exact}
path={path}
component={component}
/>
))}
<CompatRouter>
<NotificationProvider>
<PrivateRoute>
<ResourceProvider>
<QueryBuilderProvider>
<DashboardProvider>
<KeyboardHotkeysProvider>
<AlertRuleProvider>
<AppLayout>
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
<Switch>
{routes.map(({ path, component, exact }) => (
<Route
key={`${path}`}
exact={exact}
path={path}
component={component}
/>
))}
<Route path="*" component={NotFound} />
</Switch>
</Suspense>
</AppLayout>
</AlertRuleProvider>
</KeyboardHotkeysProvider>
</DashboardProvider>
</QueryBuilderProvider>
</ResourceProvider>
</PrivateRoute>
</NotificationProvider>
<Route path="*" component={NotFound} />
</Switch>
</Suspense>
</AppLayout>
</AlertRuleProvider>
</KeyboardHotkeysProvider>
</DashboardProvider>
</QueryBuilderProvider>
</ResourceProvider>
</PrivateRoute>
</NotificationProvider>
</CompatRouter>
</Router>
</ConfigProvider>
);

View File

@@ -0,0 +1,39 @@
import { ApiBaseInstance } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { ErrorResponse, SuccessResponse } from 'types/api';
export interface OnboardingStatusResponse {
status: string;
data: {
attribute?: string;
error_message?: string;
status?: string;
}[];
}
const getOnboardingStatus = async (props: {
start: number;
end: number;
endpointService?: string;
}): Promise<SuccessResponse<OnboardingStatusResponse> | ErrorResponse> => {
const { endpointService, ...rest } = props;
try {
const response = await ApiBaseInstance.post(
`/messaging-queues/kafka/onboarding/${endpointService || 'consumers'}`,
rest,
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler((error as AxiosError) || SOMETHING_WENT_WRONG);
}
};
export default getOnboardingStatus;

View File

@@ -17,6 +17,7 @@ describe('getLogIndicatorType', () => {
body: 'Sample log Message',
resources_string: {},
attributesString: {},
scope_string: {},
attributes_string: {},
attributesInt: {},
attributesFloat: {},
@@ -40,6 +41,7 @@ describe('getLogIndicatorType', () => {
body: 'Sample log Message',
resources_string: {},
attributesString: {},
scope_string: {},
attributes_string: {},
attributesInt: {},
attributesFloat: {},
@@ -62,6 +64,7 @@ describe('getLogIndicatorType', () => {
body: 'Sample log Message',
resources_string: {},
attributesString: {},
scope_string: {},
attributes_string: {},
attributesInt: {},
attributesFloat: {},
@@ -83,6 +86,7 @@ describe('getLogIndicatorType', () => {
body: 'Sample log',
resources_string: {},
attributesString: {},
scope_string: {},
attributes_string: {
log_level: 'INFO' as never,
},
@@ -112,6 +116,7 @@ describe('getLogIndicatorTypeForTable', () => {
attributesString: {},
attributes_string: {},
attributesInt: {},
scope_string: {},
attributesFloat: {},
severity_text: 'WARN',
};
@@ -130,6 +135,7 @@ describe('getLogIndicatorTypeForTable', () => {
severity_number: 0,
body: 'Sample log message',
resources_string: {},
scope_string: {},
attributesString: {},
attributes_string: {},
attributesInt: {},
@@ -166,6 +172,7 @@ describe('logIndicatorBySeverityNumber', () => {
body: 'Sample log Message',
resources_string: {},
attributesString: {},
scope_string: {},
attributes_string: {},
attributesInt: {},
attributesFloat: {},

View File

@@ -37,4 +37,8 @@ export enum QueryParams {
partition = 'partition',
selectedTimelineQuery = 'selectedTimelineQuery',
ruleType = 'ruleType',
configDetail = 'configDetail',
getStartedSource = 'getStartedSource',
getStartedSourceService = 'getStartedSourceService',
mqServiceView = 'mqServiceView',
}

View File

@@ -18,4 +18,5 @@ export const REACT_QUERY_KEY = {
GET_ALL_ALLERTS: 'GET_ALL_ALLERTS',
REMOVE_ALERT_RULE: 'REMOVE_ALERT_RULE',
DUPLICATE_ALERT_RULE: 'DUPLICATE_ALERT_RULE',
UPDATE_ALERT_RULE: 'UPDATE_ALERT_RULE',
};

View File

@@ -57,6 +57,7 @@ export const alertDefaults: AlertDef = {
},
annotations: defaultAnnotations,
evalWindow: defaultEvalWindow,
alert: '',
};
export const anamolyAlertDefaults: AlertDef = {
@@ -94,12 +95,14 @@ export const anamolyAlertDefaults: AlertDef = {
matchType: defaultMatchType,
algorithm: defaultAlgorithm,
seasonality: defaultSeasonality,
target: 3,
},
labels: {
severity: 'warning',
},
annotations: defaultAnnotations,
evalWindow: defaultEvalWindow,
alert: '',
};
export const logAlertDefaults: AlertDef = {
@@ -131,6 +134,7 @@ export const logAlertDefaults: AlertDef = {
},
annotations: defaultAnnotations,
evalWindow: defaultEvalWindow,
alert: '',
};
export const traceAlertDefaults: AlertDef = {
@@ -162,6 +166,7 @@ export const traceAlertDefaults: AlertDef = {
},
annotations: defaultAnnotations,
evalWindow: defaultEvalWindow,
alert: '',
};
export const exceptionAlertDefaults: AlertDef = {
@@ -193,6 +198,7 @@ export const exceptionAlertDefaults: AlertDef = {
},
annotations: defaultAnnotations,
evalWindow: defaultEvalWindow,
alert: '',
};
export const ALERTS_VALUES_MAP: Record<AlertTypes, AlertDef> = {

View File

@@ -1,4 +1,4 @@
import { Color } from '@signozhq/design-tokens';
import { Color, ColorType } from '@signozhq/design-tokens';
import { showErrorNotification } from 'components/ExplorerCard/utils';
import { LOCALSTORAGE } from 'constants/localStorage';
import { QueryParams } from 'constants/query';
@@ -8,7 +8,7 @@ import { DataSource } from 'types/common/queryBuilder';
import { SaveNewViewHandlerProps } from './types';
export const getRandomColor = (): Color => {
export const getRandomColor = (): ColorType => {
const colorKeys = Object.keys(Color) as (keyof typeof Color)[];
const randomKey = colorKeys[Math.floor(Math.random() * colorKeys.length)];
return Color[randomKey];

View File

@@ -53,6 +53,7 @@ import {
QueryFunctionProps,
} from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import BasicInfo from './BasicInfo';
@@ -105,6 +106,11 @@ function FormAlertRules({
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
const dataSource = useMemo(
() => urlQuery.get(QueryParams.alertType) as DataSource,
[urlQuery],
);
// In case of alert the panel types should always be "Graph" only
const panelType = PANEL_TYPES.TIME_SERIES;
@@ -114,13 +120,12 @@ function FormAlertRules({
handleSetQueryData,
handleRunQuery,
handleSetConfig,
initialDataSource,
redirectWithQueryBuilderData,
} = useQueryBuilder();
useEffect(() => {
handleSetConfig(panelType || PANEL_TYPES.TIME_SERIES, initialDataSource);
}, [handleSetConfig, initialDataSource, panelType]);
handleSetConfig(panelType || PANEL_TYPES.TIME_SERIES, dataSource);
}, [handleSetConfig, dataSource, panelType]);
// use query client
const ruleCache = useQueryClient();

View File

@@ -138,6 +138,9 @@ function LabelSelect({
if (e.key === 'Enter' || e.code === 'Enter' || e.key === ':') {
send('NEXT');
}
if (state.value === 'Idle') {
send('NEXT');
}
}}
bordered={false}
value={currentVal as never}

View File

@@ -157,6 +157,11 @@ export const getFieldAttributes = (field: string): IFieldAttributes => {
const stringWithoutPrefix = field.slice('resources_'.length);
const parts = splitOnce(stringWithoutPrefix, '.');
[dataType, newField] = parts;
} else if (field.startsWith('scope_string')) {
logType = MetricsType.Scope;
const stringWithoutPrefix = field.slice('scope_'.length);
const parts = splitOnce(stringWithoutPrefix, '.');
[dataType, newField] = parts;
}
return { dataType, newField, logType };
@@ -187,6 +192,7 @@ export const aggregateAttributesResourcesToString = (logData: ILog): string => {
traceId: logData.traceId,
attributes: {},
resources: {},
scope: {},
severity_text: logData.severity_text,
severity_number: logData.severity_number,
};
@@ -198,6 +204,9 @@ export const aggregateAttributesResourcesToString = (logData: ILog): string => {
} else if (key.startsWith('resources_')) {
outputJson.resources = outputJson.resources || {};
Object.assign(outputJson.resources, logData[key as keyof ILog]);
} else if (key.startsWith('scope_string')) {
outputJson.scope = outputJson.scope || {};
Object.assign(outputJson.scope, logData[key as keyof ILog]);
} else {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore

View File

@@ -202,6 +202,7 @@ function LogsExplorerViews({
id: 'severity_text--string----true',
},
],
legend: '{{severity_text}}',
};
const modifiedQuery: Query = {

View File

@@ -58,12 +58,17 @@ export const databaseCallsRPS = ({
const legends = [legend];
const dataSource = DataSource.METRICS;
const timeAggregateOperators = [MetricAggregateOperator.RATE];
const spaceAggregateOperators = [MetricAggregateOperator.SUM];
return getQueryBuilderQueries({
autocompleteData,
groupBy,
legends,
filterItems,
dataSource,
timeAggregateOperators,
spaceAggregateOperators,
});
};

View File

@@ -213,12 +213,17 @@ export const externalCallRpsByAddress = ({
const legends = [legend];
const dataSource = DataSource.METRICS;
const timeAggregateOperators = [MetricAggregateOperator.RATE];
const spaceAggregateOperators = [MetricAggregateOperator.SUM];
return getQueryBuilderQueries({
autocompleteData,
groupBy,
legends,
filterItems,
dataSource,
timeAggregateOperators,
spaceAggregateOperators,
});
};

View File

@@ -25,6 +25,8 @@ export const getQueryBuilderQueries = ({
aggregateOperator,
dataSource,
queryNameAndExpression,
timeAggregateOperators,
spaceAggregateOperators,
}: BuilderQueriesProps): QueryBuilderData => ({
queryFormulas: [],
queryData: autocompleteData.map((item, index) => {
@@ -50,6 +52,8 @@ export const getQueryBuilderQueries = ({
op: 'AND',
},
reduceTo: 'avg',
spaceAggregation: spaceAggregateOperators[index],
timeAggregation: timeAggregateOperators[index],
dataSource,
};

View File

@@ -83,6 +83,17 @@ export const latency = ({
const dataSource = isSpanMetricEnable ? DataSource.METRICS : DataSource.TRACES;
const queryNameAndExpression = QUERYNAME_AND_EXPRESSION;
const timeAggregateOperators = [
MetricAggregateOperator.EMPTY,
MetricAggregateOperator.EMPTY,
MetricAggregateOperator.EMPTY,
];
const spaceAggregateOperators = [
MetricAggregateOperator.P50,
MetricAggregateOperator.P90,
MetricAggregateOperator.P99,
];
return getQueryBuilderQueries({
autocompleteData,
legends,
@@ -90,6 +101,8 @@ export const latency = ({
aggregateOperator,
dataSource,
queryNameAndExpression,
timeAggregateOperators,
spaceAggregateOperators,
});
};
@@ -510,11 +523,16 @@ export const operationPerSec = ({
const legends = OPERATION_LEGENDS;
const dataSource = DataSource.METRICS;
const timeAggregateOperators = [MetricAggregateOperator.RATE];
const spaceAggregateOperators = [MetricAggregateOperator.SUM];
return getQueryBuilderQueries({
autocompleteData,
legends,
filterItems,
dataSource,
timeAggregateOperators,
spaceAggregateOperators,
});
};

View File

@@ -29,6 +29,8 @@ export interface BuilderQueriesProps {
aggregateOperator?: string[];
dataSource: DataSource;
queryNameAndExpression?: string[];
timeAggregateOperators: MetricAggregateOperator[];
spaceAggregateOperators: MetricAggregateOperator[];
}
export interface BuilderQuerieswithFormulaProps {

View File

@@ -2,18 +2,27 @@
import { DownloadOptions } from 'container/Download/Download.types';
import { MenuItemKeys } from 'container/GridCardLayout/WidgetHeader/contants';
import {
MetricAggregateOperator,
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
export const legend = {
address: '{{address}}',
};
export const QUERYNAME_AND_EXPRESSION = ['A', 'B', 'C'];
export const LATENCY_AGGREGATEOPERATOR = ['p50', 'p90', 'p99'];
export const LATENCY_AGGREGATEOPERATOR_SPAN_METRICS = [
'hist_quantile_50',
'hist_quantile_90',
'hist_quantile_99',
export const LATENCY_AGGREGATEOPERATOR = [
TracesAggregatorOperator.P50,
TracesAggregatorOperator.P90,
TracesAggregatorOperator.P99,
];
export const LATENCY_AGGREGATEOPERATOR_SPAN_METRICS = [
MetricAggregateOperator.P50,
MetricAggregateOperator.P90,
MetricAggregateOperator.P99,
];
export const OPERATION_LEGENDS = ['Operations'];
export const MENU_ITEMS = [MenuItemKeys.View, MenuItemKeys.CreateAlerts];
@@ -21,8 +30,21 @@ export const MENU_ITEMS = [MenuItemKeys.View, MenuItemKeys.CreateAlerts];
export enum FORMULA {
ERROR_PERCENTAGE = 'A*100/B',
DATABASE_CALLS_AVG_DURATION = 'A/B',
// The apdex formula is (satisfied_count + 0.5 * tolerating_count + 0 * frustating_count) / total_count
// The satisfied_count is B, tolerating_count is C, total_count is A
// But why do we have (B+C)/2 instead of B + C/2?
// The way we issue the query is latency <= threshold, which means we over count i.e
// query B => durationNano <= 500ms
// query C => durationNano <= 2000ms
// Since <= 2000ms includes <= 500ms, we over count, to correct we subtract B/2
// so the full expression would be (B + C/2) - B/2 = (B+C)/2
APDEX_TRACES = '((B + C)/2)/A',
APDEX_DELTA_SPAN_METRICS = '((B + C)/2)/A',
// Does the same not apply for delta span metrics?
// No, because the delta metrics store the counts just for the current bucket
// so we don't need to subtract anything
APDEX_DELTA_SPAN_METRICS = '(B + C)/A',
// Cumulative span metrics store the counts for all buckets
// so we need to subtract B/2 to correct the over counting
APDEX_CUMULATIVE_SPAN_METRICS = '((B + C)/2)/A',
}
@@ -53,6 +75,7 @@ export enum KeyOperationTableHeader {
export enum MetricsType {
Tag = 'tag',
Resource = 'resource',
Scope = 'scope',
}
export enum WidgetKeys {

View File

@@ -0,0 +1,32 @@
&nbsp;
Once you are done instrumenting your Java application, you can run it using the below commands
**Note:**
- Ensure you have Java and Maven installed. Compile your Java consumer applications: Ensure your consumer apps are compiled and ready to run.
**Run Consumer App with Java Agent:**
```bash
java -javaagent:/path/to/opentelemetry-javaagent.jar \
-Dotel.service.name=consumer-svc \
-Dotel.traces.exporter=otlp \
-Dotel.metrics.exporter=otlp \
-Dotel.logs.exporter=otlp \
-Dotel.instrumentation.kafka.producer-propagation.enabled=true \
-Dotel.instrumentation.kafka.experimental-span-attributes=true \
-Dotel.instrumentation.kafka.metric-reporter.enabled=true \
-jar /path/to/your/consumer.jar
```
<path> - update it to the path where you downloaded the Java JAR agent in previous step
<my-app> - Jar file of your application
&nbsp;
**Note:**
- In case you're dockerising your application, make sure to dockerise it along with OpenTelemetry instrumentation done in previous step.
&nbsp;
If you encounter any difficulties, please consult the [troubleshooting section](https://signoz.io/docs/instrumentation/springboot/#troubleshooting-your-installation) for assistance.

View File

@@ -0,0 +1,29 @@
&nbsp;
Once you are done instrumenting your Java application, you can run it using the below commands
**Note:**
- Ensure you have Java and Maven installed. Compile your Java producer applications: Ensure your producer apps are compiled and ready to run.
**Run Producer App with Java Agent:**
```bash
java -javaagent:/path/to/opentelemetry-javaagent.jar \
-Dotel.service.name=producer-svc \
-Dotel.traces.exporter=otlp \
-Dotel.metrics.exporter=otlp \
-Dotel.logs.exporter=otlp \
-jar /path/to/your/producer.jar
```
<path> - update it to the path where you downloaded the Java JAR agent in previous step
<my-app> - Jar file of your application
&nbsp;
**Note:**
- In case you're dockerising your application, make sure to dockerise it along with OpenTelemetry instrumentation done in previous step.
&nbsp;
If you encounter any difficulties, please consult the [troubleshooting section](https://signoz.io/docs/instrumentation/springboot/#troubleshooting-your-installation) for assistance.

View File

@@ -6,11 +6,16 @@ import {
LoadingOutlined,
} from '@ant-design/icons';
import logEvent from 'api/common/logEvent';
import { QueryParams } from 'constants/query';
import Header from 'container/OnboardingContainer/common/Header/Header';
import { useOnboardingContext } from 'container/OnboardingContainer/context/OnboardingContext';
import { useOnboardingStatus } from 'hooks/messagingQueue / onboarding/useOnboardingStatus';
import { useQueryService } from 'hooks/useQueryService';
import useResourceAttribute from 'hooks/useResourceAttribute';
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
import useUrlQuery from 'hooks/useUrlQuery';
import MessagingQueueHealthCheck from 'pages/MessagingQueues/MessagingQueueHealthCheck/MessagingQueueHealthCheck';
import { getAttributeDataFromOnboardingStatus } from 'pages/MessagingQueues/MessagingQueuesUtils';
import { useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
@@ -27,6 +32,12 @@ export default function ConnectionStatus(): JSX.Element {
GlobalReducer
>((state) => state.globalTime);
const urlQuery = useUrlQuery();
const getStartedSource = urlQuery.get(QueryParams.getStartedSource);
const getStartedSourceService = urlQuery.get(
QueryParams.getStartedSourceService,
);
const {
serviceName,
selectedDataSource,
@@ -57,8 +68,69 @@ export default function ConnectionStatus(): JSX.Element {
maxTime,
selectedTime,
selectedTags,
options: {
enabled: getStartedSource !== 'kafka',
},
});
const [pollInterval, setPollInterval] = useState<number | false>(10000);
const {
data: onbData,
error: onbErr,
isFetching: onbFetching,
} = useOnboardingStatus(
{
enabled: getStartedSource === 'kafka',
refetchInterval: pollInterval,
},
getStartedSourceService || '',
'query-key-onboarding-status',
);
const [
shouldRetryOnboardingCall,
setShouldRetryOnboardingCall,
] = useState<boolean>(false);
useEffect(() => {
// runs only when the caller is coming from 'kafka' i.e. coming from Messaging Queues - setup helper
if (getStartedSource === 'kafka') {
if (onbData?.statusCode !== 200) {
setShouldRetryOnboardingCall(true);
} else if (onbData?.payload?.status === 'success') {
const attributeData = getAttributeDataFromOnboardingStatus(
onbData?.payload,
);
if (attributeData.overallStatus === 'success') {
setLoading(false);
setIsReceivingData(true);
setPollInterval(false);
} else {
setShouldRetryOnboardingCall(true);
}
}
}
}, [
shouldRetryOnboardingCall,
onbData,
onbErr,
onbFetching,
getStartedSource,
]);
useEffect(() => {
if (retryCount < 0 && getStartedSource === 'kafka') {
setPollInterval(false);
setLoading(false);
}
}, [retryCount, getStartedSource]);
useEffect(() => {
if (getStartedSource === 'kafka' && !onbFetching) {
setRetryCount((prevCount) => prevCount - 1);
}
}, [getStartedSource, onbData, onbFetching]);
const renderDocsReference = (): JSX.Element => {
switch (selectedDataSource?.name) {
case 'java':
@@ -192,25 +264,27 @@ export default function ConnectionStatus(): JSX.Element {
useEffect(() => {
let pollingTimer: string | number | NodeJS.Timer | undefined;
if (loading) {
pollingTimer = setInterval(() => {
// Trigger a refetch with the updated parameters
const updatedMinTime = (Date.now() - 15 * 60 * 1000) * 1000000;
const updatedMaxTime = Date.now() * 1000000;
if (getStartedSource !== 'kafka') {
if (loading) {
pollingTimer = setInterval(() => {
// Trigger a refetch with the updated parameters
const updatedMinTime = (Date.now() - 15 * 60 * 1000) * 1000000;
const updatedMaxTime = Date.now() * 1000000;
const payload = {
maxTime: updatedMaxTime,
minTime: updatedMinTime,
selectedTime,
};
const payload = {
maxTime: updatedMaxTime,
minTime: updatedMinTime,
selectedTime,
};
dispatch({
type: UPDATE_TIME_INTERVAL,
payload,
});
}, pollingInterval); // Same interval as pollingInterval
} else if (!loading && pollingTimer) {
clearInterval(pollingTimer);
dispatch({
type: UPDATE_TIME_INTERVAL,
payload,
});
}, pollingInterval); // Same interval as pollingInterval
} else if (!loading && pollingTimer) {
clearInterval(pollingTimer);
}
}
// Clean up the interval when the component unmounts
@@ -221,15 +295,24 @@ export default function ConnectionStatus(): JSX.Element {
}, [refetch, selectedTags, selectedTime, loading]);
useEffect(() => {
verifyApplicationData(data);
if (getStartedSource !== 'kafka') {
verifyApplicationData(data);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isServiceLoading, data, error, isError]);
useEffect(() => {
refetch();
if (getStartedSource !== 'kafka') {
refetch();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const isQueryServiceLoading = useMemo(
() => isServiceLoading || loading || onbFetching,
[isServiceLoading, loading, onbFetching],
);
return (
<div className="connection-status-container">
<div className="full-docs-link">{renderDocsReference()}</div>
@@ -250,30 +333,42 @@ export default function ConnectionStatus(): JSX.Element {
<div className="label"> Status </div>
<div className="status">
{(loading || isServiceLoading) && <LoadingOutlined />}
{!(loading || isServiceLoading) && isReceivingData && (
<>
<CheckCircleTwoTone twoToneColor="#52c41a" />
<span> Success </span>
</>
)}
{!(loading || isServiceLoading) && !isReceivingData && (
<>
<CloseCircleTwoTone twoToneColor="#e84749" />
<span> Failed </span>
</>
)}
{isQueryServiceLoading && <LoadingOutlined />}
{!isQueryServiceLoading &&
isReceivingData &&
(getStartedSource !== 'kafka' ? (
<>
<CheckCircleTwoTone twoToneColor="#52c41a" />
<span> Success </span>
</>
) : (
<MessagingQueueHealthCheck
serviceToInclude={[getStartedSourceService || '']}
/>
))}
{!isQueryServiceLoading &&
!isReceivingData &&
(getStartedSource !== 'kafka' ? (
<>
<CloseCircleTwoTone twoToneColor="#e84749" />
<span> Failed </span>
</>
) : (
<MessagingQueueHealthCheck
serviceToInclude={[getStartedSourceService || '']}
/>
))}
</div>
</div>
<div className="details-info">
<div className="label"> Details </div>
<div className="details">
{(loading || isServiceLoading) && <div> Waiting for Update </div>}
{!(loading || isServiceLoading) && isReceivingData && (
{isQueryServiceLoading && <div> Waiting for Update </div>}
{!isQueryServiceLoading && isReceivingData && (
<div> Received data from the application successfully. </div>
)}
{!(loading || isServiceLoading) && !isReceivingData && (
{!isQueryServiceLoading && !isReceivingData && (
<div> Could not detect the install </div>
)}
</div>

View File

@@ -74,4 +74,11 @@ div[class*='-setup-instructions-container'] {
.dataSourceName {
color: var(--bg-slate-500);
}
}
}
.supported-languages-container {
.disabled {
cursor: not-allowed;
opacity: 0.5;
}
}

View File

@@ -6,15 +6,21 @@ import { LoadingOutlined } from '@ant-design/icons';
import { Button, Card, Form, Input, Select, Space, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { useOnboardingContext } from 'container/OnboardingContainer/context/OnboardingContext';
import { useCases } from 'container/OnboardingContainer/OnboardingContainer';
import {
ModulesMap,
useCases,
} from 'container/OnboardingContainer/OnboardingContainer';
import {
getDataSources,
getSupportedFrameworks,
hasFrameworks,
messagingQueueKakfaSupportedDataSources,
} from 'container/OnboardingContainer/utils/dataSourceUtils';
import { useNotifications } from 'hooks/useNotifications';
import useUrlQuery from 'hooks/useUrlQuery';
import { Blocks, Check } from 'lucide-react';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
@@ -33,6 +39,8 @@ export default function DataSource(): JSX.Element {
const { t } = useTranslation(['common']);
const history = useHistory();
const getStartedSource = useUrlQuery().get(QueryParams.getStartedSource);
const {
serviceName,
selectedModule,
@@ -44,6 +52,9 @@ export default function DataSource(): JSX.Element {
updateSelectedFramework,
} = useOnboardingContext();
const isKafkaAPM =
getStartedSource === 'kafka' && selectedModule?.id === ModulesMap.APM;
const [supportedDataSources, setSupportedDataSources] = useState<
DataSourceType[]
>([]);
@@ -150,13 +161,19 @@ export default function DataSource(): JSX.Element {
className={cx(
'supported-language',
selectedDataSource?.name === dataSource.name ? 'selected' : '',
isKafkaAPM &&
!messagingQueueKakfaSupportedDataSources.includes(dataSource?.id || '')
? 'disabled'
: '',
)}
key={dataSource.name}
onClick={(): void => {
updateSelectedFramework(null);
updateSelectedEnvironment(null);
updateSelectedDataSource(dataSource);
form.setFieldsValue({ selectFramework: null });
if (!isKafkaAPM) {
updateSelectedFramework(null);
updateSelectedEnvironment(null);
updateSelectedDataSource(dataSource);
form.setFieldsValue({ selectFramework: null });
}
}}
>
<div>

View File

@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import { MarkdownRenderer } from 'components/MarkdownRenderer/MarkdownRenderer';
import { QueryParams } from 'constants/query';
import { ApmDocFilePaths } from 'container/OnboardingContainer/constants/apmDocFilePaths';
import { AwsMonitoringDocFilePaths } from 'container/OnboardingContainer/constants/awsMonitoringDocFilePaths';
import { AzureMonitoringDocFilePaths } from 'container/OnboardingContainer/constants/azureMonitoringDocFilePaths';
@@ -10,6 +11,7 @@ import {
useOnboardingContext,
} from 'container/OnboardingContainer/context/OnboardingContext';
import { ModulesMap } from 'container/OnboardingContainer/OnboardingContainer';
import useUrlQuery from 'hooks/useUrlQuery';
import { useEffect, useState } from 'react';
export interface IngestionInfoProps {
@@ -31,6 +33,12 @@ export default function MarkdownStep(): JSX.Element {
const [markdownContent, setMarkdownContent] = useState('');
const urlQuery = useUrlQuery();
const getStartedSource = urlQuery.get(QueryParams.getStartedSource);
const getStartedSourceService = urlQuery.get(
QueryParams.getStartedSourceService,
);
const { step } = activeStep;
const getFilePath = (): any => {
@@ -54,6 +62,12 @@ export default function MarkdownStep(): JSX.Element {
path += `_${step?.id}`;
if (
getStartedSource === 'kafka' &&
path === 'APM_java_springBoot_kubernetes_recommendedSteps_runApplication' // todo: Sagar - Make this generic logic in followup PRs
) {
path += `_${getStartedSourceService}`;
}
return path;
};

View File

@@ -252,6 +252,8 @@ import APM_java_springBoot_docker_recommendedSteps_runApplication from '../Modul
import APM_java_springBoot_kubernetes_recommendedSteps_setupOtelCollector from '../Modules/APM/Java/md-docs/SpringBoot/Kubernetes/springBoot-kubernetes-installOtelCollector.md';
import APM_java_springBoot_kubernetes_recommendedSteps_instrumentApplication from '../Modules/APM/Java/md-docs/SpringBoot/Kubernetes/springBoot-kubernetes-instrumentApplication.md';
import APM_java_springBoot_kubernetes_recommendedSteps_runApplication from '../Modules/APM/Java/md-docs/SpringBoot/Kubernetes/springBoot-kubernetes-runApplication.md';
import APM_java_springBoot_kubernetes_recommendedSteps_runApplication_consumers from '../Modules/APM/Java/md-docs/SpringBoot/Kubernetes/springBoot-kubernetes-runApplication-consumers.md';
import APM_java_springBoot_kubernetes_recommendedSteps_runApplication_producers from '../Modules/APM/Java/md-docs/SpringBoot/Kubernetes/springBoot-kubernetes-runApplication-producers.md';
// SpringBoot-LinuxAMD64-quickstart
import APM_java_springBoot_linuxAMD64_quickStart_instrumentApplication from '../Modules/APM/Java/md-docs/SpringBoot/LinuxAMD64/QuickStart/springBoot-linuxamd64-quickStart-instrumentApplication.md';
import APM_java_springBoot_linuxAMD64_quickStart_runApplication from '../Modules/APM/Java/md-docs/SpringBoot/LinuxAMD64/QuickStart/springBoot-linuxamd64-quickStart-runApplication.md';
@@ -1053,6 +1055,8 @@ export const ApmDocFilePaths = {
APM_java_springBoot_kubernetes_recommendedSteps_setupOtelCollector,
APM_java_springBoot_kubernetes_recommendedSteps_instrumentApplication,
APM_java_springBoot_kubernetes_recommendedSteps_runApplication,
APM_java_springBoot_kubernetes_recommendedSteps_runApplication_producers,
APM_java_springBoot_kubernetes_recommendedSteps_runApplication_consumers,
// SpringBoot-LinuxAMD64-recommended
APM_java_springBoot_linuxAMD64_recommendedSteps_setupOtelCollector,

View File

@@ -399,3 +399,5 @@ export const moduleRouteMap = {
[ModulesMap.AwsMonitoring]: ROUTES.GET_STARTED_AWS_MONITORING,
[ModulesMap.AzureMonitoring]: ROUTES.GET_STARTED_AZURE_MONITORING,
};
export const messagingQueueKakfaSupportedDataSources = ['java'];

View File

@@ -82,7 +82,7 @@ export function AboutSigNozQuestions({
otherInterestInSignoz,
});
logEvent('User Onboarding: About SigNoz Questions Answered', {
logEvent('Org Onboarding: Answered', {
hearAboutSignoz,
otherAboutSignoz,
interestInSignoz,

View File

@@ -161,6 +161,13 @@ function InviteTeamMembers({
setInviteUsersSuccessResponse(successfulInvites);
logEvent('Org Onboarding: Invite Team Members Success', {
teamMembers: teamMembersToInvite,
totalInvites: inviteUsersResponse.summary.total_invites,
successfulInvites: inviteUsersResponse.summary.successful_invites,
failedInvites: inviteUsersResponse.summary.failed_invites,
});
setTimeout(() => {
setDisableNextButton(false);
onNext();
@@ -172,6 +179,13 @@ function InviteTeamMembers({
setInviteUsersSuccessResponse(successfulInvites);
logEvent('Org Onboarding: Invite Team Members Partial Success', {
teamMembers: teamMembersToInvite,
totalInvites: inviteUsersResponse.summary.total_invites,
successfulInvites: inviteUsersResponse.summary.successful_invites,
failedInvites: inviteUsersResponse.summary.failed_invites,
});
if (inviteUsersResponse.failed_invites.length > 0) {
setHasErrors(true);
@@ -182,27 +196,21 @@ function InviteTeamMembers({
}
};
const {
mutate: sendInvites,
isLoading: isSendingInvites,
data: inviteUsersApiResponseData,
} = useMutation(inviteUsers, {
onSuccess: (response: SuccessResponse<InviteUsersResponse>): void => {
logEvent('User Onboarding: Invite Team Members Sent', {
teamMembers: teamMembersToInvite,
});
const { mutate: sendInvites, isLoading: isSendingInvites } = useMutation(
inviteUsers,
{
onSuccess: (response: SuccessResponse<InviteUsersResponse>): void => {
handleInviteUsersSuccess(response);
},
onError: (error: AxiosError): void => {
logEvent('Org Onboarding: Invite Team Members Failed', {
teamMembers: teamMembersToInvite,
});
handleInviteUsersSuccess(response);
handleError(error);
},
},
onError: (error: AxiosError): void => {
logEvent('User Onboarding: Invite Team Members Failed', {
teamMembers: teamMembersToInvite,
error,
});
handleError(error);
},
});
);
const handleNext = (): void => {
if (validateAllUsers()) {
@@ -254,9 +262,8 @@ function InviteTeamMembers({
};
const handleDoLater = (): void => {
logEvent('User Onboarding: Invite Team Members Skipped', {
teamMembers: teamMembersToInvite,
apiResponse: inviteUsersApiResponseData,
logEvent('Org Onboarding: Clicked Do Later', {
currentPageID: 4,
});
onNext();

View File

@@ -122,7 +122,7 @@ function OptimiseSignozNeeds({
}, [services, hostsPerDay, logsPerDay]);
const handleOnNext = (): void => {
logEvent('User Onboarding: Optimise SigNoz Needs Answered', {
logEvent('Org Onboarding: Answered', {
logsPerDay,
hostsPerDay,
services,
@@ -144,10 +144,8 @@ function OptimiseSignozNeeds({
onWillDoLater();
logEvent('User Onboarding: Optimise SigNoz Needs Skipped', {
logsPerDay: 0,
hostsPerDay: 0,
services: 0,
logEvent('Org Onboarding: Clicked Do Later', {
currentPageID: 3,
});
};

View File

@@ -94,6 +94,13 @@ function OrgQuestions({
organisationName === '' ||
orgDetails.organisationName === organisationName
) {
logEvent('Org Onboarding: Answered', {
usesObservability,
observabilityTool,
otherTool,
familiarity,
});
onNext({
organisationName,
usesObservability,
@@ -121,10 +128,17 @@ function OrgQuestions({
},
});
logEvent('User Onboarding: Org Name Updated', {
logEvent('Org Onboarding: Org Name Updated', {
organisationName: orgDetails.organisationName,
});
logEvent('Org Onboarding: Answered', {
usesObservability,
observabilityTool,
otherTool,
familiarity,
});
onNext({
organisationName,
usesObservability,
@@ -133,7 +147,7 @@ function OrgQuestions({
familiarity,
});
} else {
logEvent('User Onboarding: Org Name Update Failed', {
logEvent('Org Onboarding: Org Name Update Failed', {
organisationName: orgDetails.organisationName,
});

View File

@@ -1,6 +1,7 @@
import './OnboardingQuestionaire.styles.scss';
import { NotificationInstance } from 'antd/es/notification/interface';
import logEvent from 'api/common/logEvent';
import updateProfileAPI from 'api/onboarding/updateProfile';
import getAllOrgPreferences from 'api/preferences/getAllOrgPreferences';
import updateOrgPreferenceAPI from 'api/preferences/updateOrgPreference';
@@ -61,6 +62,10 @@ const INITIAL_OPTIMISE_SIGNOZ_DETAILS: OptimiseSignozDetails = {
services: 0,
};
const BACK_BUTTON_EVENT_NAME = 'Org Onboarding: Back Button Clicked';
const NEXT_BUTTON_EVENT_NAME = 'Org Onboarding: Next Button Clicked';
const ONBOARDING_COMPLETE_EVENT_NAME = 'Org Onboarding: Complete';
function OnboardingQuestionaire(): JSX.Element {
const { notifications } = useNotifications();
const { org } = useSelector<AppState, AppReducer>((state) => state.app);
@@ -98,6 +103,13 @@ function OnboardingQuestionaire(): JSX.Element {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [org]);
useEffect(() => {
logEvent('Org Onboarding: Started', {
org_id: org?.[0]?.id,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const { refetch: refetchOrgPreferences } = useQuery({
queryFn: () => getAllOrgPreferences(),
queryKey: ['getOrgPreferences'],
@@ -120,6 +132,8 @@ function OnboardingQuestionaire(): JSX.Element {
setUpdatingOrgOnboardingStatus(false);
logEvent('Org Onboarding: Redirecting to Get Started', {});
history.push(ROUTES.GET_STARTED);
},
onError: () => {
@@ -156,6 +170,11 @@ function OnboardingQuestionaire(): JSX.Element {
});
const handleUpdateProfile = (): void => {
logEvent(NEXT_BUTTON_EVENT_NAME, {
currentPageID: 3,
nextPageID: 4,
});
updateProfile({
familiarity_with_observability: orgDetails?.familiarity as string,
has_existing_observability_tool: orgDetails?.usesObservability as boolean,
@@ -180,6 +199,10 @@ function OnboardingQuestionaire(): JSX.Element {
};
const handleOnboardingComplete = (): void => {
logEvent(ONBOARDING_COMPLETE_EVENT_NAME, {
currentPageID: 4,
});
setUpdatingOrgOnboardingStatus(true);
updateOrgPreference({
preferenceID: 'ORG_ONBOARDING',
@@ -199,6 +222,11 @@ function OnboardingQuestionaire(): JSX.Element {
currentOrgData={currentOrgData}
orgDetails={orgDetails}
onNext={(orgDetails: OrgDetails): void => {
logEvent(NEXT_BUTTON_EVENT_NAME, {
currentPageID: 1,
nextPageID: 2,
});
setOrgDetails(orgDetails);
setCurrentStep(2);
}}
@@ -209,8 +237,20 @@ function OnboardingQuestionaire(): JSX.Element {
<AboutSigNozQuestions
signozDetails={signozDetails}
setSignozDetails={setSignozDetails}
onBack={(): void => setCurrentStep(1)}
onNext={(): void => setCurrentStep(3)}
onBack={(): void => {
logEvent(BACK_BUTTON_EVENT_NAME, {
currentPageID: 2,
prevPageID: 1,
});
setCurrentStep(1);
}}
onNext={(): void => {
logEvent(NEXT_BUTTON_EVENT_NAME, {
currentPageID: 2,
nextPageID: 3,
});
setCurrentStep(3);
}}
/>
)}
@@ -220,9 +260,15 @@ function OnboardingQuestionaire(): JSX.Element {
isUpdatingProfile={isUpdatingProfile}
optimiseSignozDetails={optimiseSignozDetails}
setOptimiseSignozDetails={setOptimiseSignozDetails}
onBack={(): void => setCurrentStep(2)}
onBack={(): void => {
logEvent(BACK_BUTTON_EVENT_NAME, {
currentPageID: 3,
prevPageID: 2,
});
setCurrentStep(2);
}}
onNext={handleUpdateProfile}
onWillDoLater={(): void => setCurrentStep(4)} // This is temporary, only to skip gateway api call as it's not setup on staging yet
onWillDoLater={(): void => setCurrentStep(4)}
/>
)}
@@ -231,7 +277,13 @@ function OnboardingQuestionaire(): JSX.Element {
isLoading={updatingOrgOnboardingStatus}
teamMembers={teamMembers}
setTeamMembers={setTeamMembers}
onBack={(): void => setCurrentStep(3)}
onBack={(): void => {
logEvent(BACK_BUTTON_EVENT_NAME, {
currentPageID: 4,
prevPageID: 3,
});
setCurrentStep(3);
}}
onNext={handleOnboardingComplete}
/>
)}

View File

@@ -81,8 +81,10 @@ export const AggregatorFilter = memo(function AggregatorFilter({
prefix: item.type || '',
condition: !item.isColumn,
}),
!item.isColumn && item.type ? item.type : '',
)}
dataType={item.dataType}
type={item.type || ''}
/>
),
value: `${item.key}${selectValueDivider}${createIdFromObjectFields(
@@ -187,6 +189,9 @@ export const AggregatorFilter = memo(function AggregatorFilter({
prefix: query.aggregateAttribute.type || '',
condition: !query.aggregateAttribute.isColumn,
}),
!query.aggregateAttribute.isColumn && query.aggregateAttribute.type
? query.aggregateAttribute.type
: '',
);
return (

View File

@@ -75,8 +75,10 @@ export const GroupByFilter = memo(function GroupByFilter({
prefix: item.type || '',
condition: !item.isColumn,
}),
!item.isColumn && item.type ? item.type : '',
)}
dataType={item.dataType || ''}
type={item.type || ''}
/>
),
value: `${item.id}`,
@@ -166,6 +168,7 @@ export const GroupByFilter = memo(function GroupByFilter({
prefix: item.type || '',
condition: !item.isColumn,
}),
!item.isColumn && item.type ? item.type : '',
)}`,
value: `${item.id}`,
}),

View File

@@ -1,8 +1,9 @@
import { MetricsType } from 'container/MetricsApplication/constant';
export function removePrefix(str: string): string {
export function removePrefix(str: string, type: string): string {
const tagPrefix = `${MetricsType.Tag}_`;
const resourcePrefix = `${MetricsType.Resource}_`;
const scopePrefix = `${MetricsType.Scope}_`;
if (str.startsWith(tagPrefix)) {
return str.slice(tagPrefix.length);
@@ -10,5 +11,9 @@ export function removePrefix(str: string): string {
if (str.startsWith(resourcePrefix)) {
return str.slice(resourcePrefix.length);
}
if (str.startsWith(scopePrefix) && type === MetricsType.Scope) {
return str.slice(scopePrefix.length);
}
return str;
}

View File

@@ -3,25 +3,23 @@ import './QueryBuilderSearch.styles.scss';
import { Tooltip } from 'antd';
import { TagContainer, TagLabel, TagValue } from './style';
import { getOptionType } from './utils';
function OptionRenderer({
label,
value,
dataType,
type,
}: OptionRendererProps): JSX.Element {
const optionType = getOptionType(label);
return (
<span className="option">
{optionType ? (
{type ? (
<Tooltip title={`${value}`} placement="topLeft">
<div className="selectOptionContainer">
<div className="option-value">{value}</div>
<div className="option-meta-data-container">
<TagContainer>
<TagLabel>Type: </TagLabel>
<TagValue>{optionType}</TagValue>
<TagValue>{type}</TagValue>
</TagContainer>
<TagContainer>
<TagLabel>Data type: </TagLabel>
@@ -43,6 +41,7 @@ interface OptionRendererProps {
label: string;
value: string;
dataType: string;
type: string;
}
export default OptionRenderer;

View File

@@ -410,6 +410,7 @@ function QueryBuilderSearch({
label={option.label}
value={option.value}
dataType={option.dataType || ''}
type={option.type || ''}
/>
{option.selected && <StyledCheckOutlined />}
</Select.Option>

View File

@@ -260,6 +260,20 @@
background: rgba(189, 153, 121, 0.1);
}
}
&.scope {
border: 1px solid rgba(113, 144, 249, 0.2);
.ant-typography {
color: var(--bg-robin-400);
background: rgba(113, 144, 249, 0.1);
font-size: 14px;
}
.ant-tag-close-icon {
background: rgba(113, 144, 249, 0.1);
}
}
}
}
}

View File

@@ -94,6 +94,25 @@
letter-spacing: -0.06px;
}
}
&.scope {
border-radius: 50px;
background: rgba(113, 144, 249, 0.1) !important;
color: var(--bg-robin-400) !important;
.dot {
background-color: var(--bg-robin-400);
}
.text {
color: var(--bg-robin-400);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 150% */
letter-spacing: -0.06px;
}
}
}
}
.option-meta-data-container {

View File

@@ -16,4 +16,5 @@ export type Option = {
selected?: boolean;
dataType?: string;
isIndexed?: boolean;
type?: string;
};

View File

@@ -0,0 +1,29 @@
import getOnboardingStatus, {
OnboardingStatusResponse,
} from 'api/messagingQueues/onboarding/getOnboardingStatus';
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
import { ErrorResponse, SuccessResponse } from 'types/api';
type UseOnboardingStatus = (
options?: UseQueryOptions<
SuccessResponse<OnboardingStatusResponse> | ErrorResponse
>,
endpointService?: string,
queryKey?: string,
) => UseQueryResult<SuccessResponse<OnboardingStatusResponse> | ErrorResponse>;
export const useOnboardingStatus: UseOnboardingStatus = (
options,
endpointService,
queryKey,
) =>
useQuery<SuccessResponse<OnboardingStatusResponse> | ErrorResponse>({
queryKey: [queryKey || `onboardingStatus-${endpointService}`],
queryFn: () =>
getOnboardingStatus({
start: (Date.now() - 15 * 60 * 1000) * 1_000_000,
end: Date.now() * 1_000_000,
endpointService,
}),
...options,
});

View File

@@ -46,6 +46,7 @@ export const useOptions = (
value: item.key,
dataType: item.dataType,
isIndexed: item?.isIndexed,
type: item?.type || '',
})),
[getLabel],
);

View File

@@ -2,82 +2,90 @@ import './ActionButtons.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Divider, Dropdown, MenuProps, Switch, Tooltip } from 'antd';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import { Copy, Ellipsis, PenLine, Trash2 } from 'lucide-react';
import {
useAlertRuleDelete,
useAlertRuleDuplicate,
useAlertRuleStatusToggle,
useAlertRuleUpdate,
} from 'pages/AlertDetails/hooks';
import CopyToClipboard from 'periscope/components/CopyToClipboard';
import { useAlertRule } from 'providers/Alert';
import React, { useEffect, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { CSSProperties } from 'styled-components';
import { AlertDef } from 'types/api/alerts/def';
import { AlertHeaderProps } from '../AlertHeader';
import RenameModal from './RenameModal';
const menuItemStyle: CSSProperties = {
fontSize: '14px',
letterSpacing: '0.14px',
};
function AlertActionButtons({
ruleId,
alertDetails,
setUpdatedName,
}: {
ruleId: string;
alertDetails: AlertHeaderProps['alertDetails'];
setUpdatedName: (name: string) => void;
}): JSX.Element {
const { alertRuleState, setAlertRuleState } = useAlertRule();
const { handleAlertStateToggle } = useAlertRuleStatusToggle({ ruleId });
const [intermediateName, setIntermediateName] = useState<string>(
alertDetails.alert,
);
const [isRenameAlertOpen, setIsRenameAlertOpen] = useState<boolean>(false);
const isDarkMode = useIsDarkMode();
const { handleAlertStateToggle } = useAlertRuleStatusToggle({ ruleId });
const { handleAlertDuplicate } = useAlertRuleDuplicate({
alertDetails: (alertDetails as unknown) as AlertDef,
});
const { handleAlertDelete } = useAlertRuleDelete({ ruleId: Number(ruleId) });
const { handleAlertUpdate, isLoading } = useAlertRuleUpdate({
alertDetails: (alertDetails as unknown) as AlertDef,
setUpdatedName,
intermediateName,
});
const params = useUrlQuery();
const handleRename = useCallback(() => {
setIsRenameAlertOpen(true);
}, []);
const handleRename = React.useCallback(() => {
params.set(QueryParams.ruleId, String(ruleId));
history.push(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`);
}, [params, ruleId]);
const onNameChangeHandler = useCallback(() => {
handleAlertUpdate();
setIsRenameAlertOpen(false);
}, [handleAlertUpdate]);
const menu: MenuProps['items'] = React.useMemo(
() => [
{
key: 'rename-rule',
label: 'Rename',
icon: <PenLine size={16} color={Color.BG_VANILLA_400} />,
onClick: (): void => handleRename(),
style: menuItemStyle,
const menuItems: MenuProps['items'] = [
{
key: 'rename-rule',
label: 'Rename',
icon: <PenLine size={16} color={Color.BG_VANILLA_400} />,
onClick: handleRename,
style: menuItemStyle,
},
{
key: 'duplicate-rule',
label: 'Duplicate',
icon: <Copy size={16} color={Color.BG_VANILLA_400} />,
onClick: handleAlertDuplicate,
style: menuItemStyle,
},
{
key: 'delete-rule',
label: 'Delete',
icon: <Trash2 size={16} color={Color.BG_CHERRY_400} />,
onClick: handleAlertDelete,
style: {
...menuItemStyle,
color: Color.BG_CHERRY_400,
},
{
key: 'duplicate-rule',
label: 'Duplicate',
icon: <Copy size={16} color={Color.BG_VANILLA_400} />,
onClick: (): void => handleAlertDuplicate(),
style: menuItemStyle,
},
{ type: 'divider' },
{
key: 'delete-rule',
label: 'Delete',
icon: <Trash2 size={16} color={Color.BG_CHERRY_400} />,
onClick: (): void => handleAlertDelete(),
style: {
...menuItemStyle,
color: Color.BG_CHERRY_400,
},
},
],
[handleAlertDelete, handleAlertDuplicate, handleRename],
);
const isDarkMode = useIsDarkMode();
},
];
// state for immediate UI feedback rather than waiting for onSuccess of handleAlertStateTiggle to updating the alertRuleState
const [isAlertRuleDisabled, setIsAlertRuleDisabled] = useState<
@@ -95,35 +103,48 @@ function AlertActionButtons({
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => (): void => setAlertRuleState(undefined), []);
const toggleAlertRule = useCallback(() => {
setIsAlertRuleDisabled((prev) => !prev);
handleAlertStateToggle();
}, [handleAlertStateToggle]);
return (
<div className="alert-action-buttons">
<Tooltip title={alertRuleState ? 'Enable alert' : 'Disable alert'}>
{isAlertRuleDisabled !== undefined && (
<Switch
size="small"
onChange={(): void => {
setIsAlertRuleDisabled((prev) => !prev);
handleAlertStateToggle();
}}
checked={!isAlertRuleDisabled}
/>
)}
</Tooltip>
<CopyToClipboard textToCopy={window.location.href} />
<Divider type="vertical" />
<Dropdown trigger={['click']} menu={{ items: menu }}>
<Tooltip title="More options">
<Ellipsis
size={16}
color={isDarkMode ? Color.BG_VANILLA_400 : Color.BG_INK_400}
cursor="pointer"
className="dropdown-icon"
/>
<>
<div className="alert-action-buttons">
<Tooltip title={alertRuleState ? 'Enable alert' : 'Disable alert'}>
{isAlertRuleDisabled !== undefined && (
<Switch
size="small"
onChange={toggleAlertRule}
checked={!isAlertRuleDisabled}
/>
)}
</Tooltip>
</Dropdown>
</div>
<CopyToClipboard textToCopy={window.location.href} />
<Divider type="vertical" />
<Dropdown trigger={['click']} menu={{ items: menuItems }}>
<Tooltip title="More options">
<Ellipsis
size={16}
color={isDarkMode ? Color.BG_VANILLA_400 : Color.BG_INK_400}
cursor="pointer"
className="dropdown-icon"
/>
</Tooltip>
</Dropdown>
</div>
<RenameModal
isOpen={isRenameAlertOpen}
setIsOpen={setIsRenameAlertOpen}
isLoading={isLoading}
onNameChangeHandler={onNameChangeHandler}
intermediateName={intermediateName}
setIntermediateName={setIntermediateName}
/>
</>
);
}

View File

@@ -0,0 +1,138 @@
.rename-alert {
.ant-modal-content {
width: 384px;
flex-shrink: 0;
border-radius: 4px;
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
padding: 0px;
.ant-modal-header {
height: 52px;
padding: 16px;
background: var(--bg-ink-400);
border-bottom: 1px solid var(--bg-slate-500);
margin-bottom: 0px;
.ant-modal-title {
color: var(--bg-vanilla-100);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
width: 349px;
height: 20px;
}
}
.ant-modal-body {
padding: 16px;
.alert-content {
display: flex;
flex-direction: column;
gap: 8px;
.name-text {
color: var(--bg-vanilla-100);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 142.857% */
}
.alert-name-input {
display: flex;
padding: 6px 6px 6px 8px;
align-items: center;
gap: 4px;
align-self: stretch;
border-radius: 0px 2px 2px 0px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
}
}
}
.ant-modal-footer {
padding: 16px;
margin-top: 0px;
.alert-rename {
display: flex;
flex-direction: row-reverse;
gap: 12px;
.cancel-btn {
display: flex;
padding: 4px 8px;
justify-content: center;
align-items: center;
gap: 4px;
border-radius: 2px;
background: var(--bg-slate-500);
.ant-btn-icon {
margin-inline-end: 0px;
}
}
.rename-btn {
display: flex;
align-items: center;
display: flex;
padding: 4px 8px;
justify-content: center;
align-items: center;
gap: 4px;
border-radius: 2px;
background: var(--bg-robin-500);
.ant-btn-icon {
margin-inline-end: 0px;
}
}
}
}
}
}
.lightMode {
.rename-alert {
.ant-modal-content {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
.ant-modal-header {
background: var(--bg-vanilla-100);
border-bottom: 1px solid var(--bg-vanilla-300);
.ant-modal-title {
color: var(--bg-ink-300);
}
}
.ant-modal-body {
.alert-content {
.name-text {
color: var(--bg-ink-300);
}
.alert-name-input {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
}
}
}
.ant-modal-footer {
.alert-rename {
.cancel-btn {
background: var(--bg-vanilla-300);
}
}
}
}
}
}

View File

@@ -0,0 +1,95 @@
import './RenameModal.styles.scss';
import { Button, Input, InputRef, Modal, Typography } from 'antd';
import { Check, X } from 'lucide-react';
import { useCallback, useEffect, useRef } from 'react';
type Props = {
isOpen: boolean;
setIsOpen: (isOpen: boolean) => void;
onNameChangeHandler: () => void;
isLoading: boolean;
intermediateName: string;
setIntermediateName: (name: string) => void;
};
function RenameModal({
isOpen,
setIsOpen,
onNameChangeHandler,
isLoading,
intermediateName,
setIntermediateName,
}: Props): JSX.Element {
const inputRef = useRef<InputRef>(null);
useEffect(() => {
if (isOpen && inputRef.current) {
inputRef.current.focus();
}
}, [isOpen]);
const handleClose = useCallback((): void => setIsOpen(false), [setIsOpen]);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent): void => {
if (isOpen) {
if (e.key === 'Enter') {
onNameChangeHandler();
} else if (e.key === 'Escape') {
handleClose();
}
}
};
document.addEventListener('keydown', handleKeyDown);
return (): void => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [isOpen, onNameChangeHandler, handleClose]);
return (
<Modal
open={isOpen}
title="Rename Alert"
onOk={onNameChangeHandler}
onCancel={handleClose}
rootClassName="rename-alert"
footer={
<div className="alert-rename">
<Button
type="primary"
icon={<Check size={14} />}
className="rename-btn"
onClick={onNameChangeHandler}
disabled={isLoading}
>
Rename Alert
</Button>
<Button
type="text"
icon={<X size={14} />}
className="cancel-btn"
onClick={handleClose}
>
Cancel
</Button>
</div>
}
>
<div className="alert-content">
<Typography.Text className="name-text">Enter a new name</Typography.Text>
<Input
ref={inputRef}
data-testid="alert-name"
className="alert-name-input"
value={intermediateName}
onChange={(e): void => setIntermediateName(e.target.value)}
/>
</div>
</Modal>
);
}
export default RenameModal;

View File

@@ -2,7 +2,7 @@ import './AlertHeader.styles.scss';
import LineClampedText from 'periscope/components/LineClampedText/LineClampedText';
import { useAlertRule } from 'providers/Alert';
import { useMemo } from 'react';
import { useMemo, useState } from 'react';
import AlertActionButtons from './ActionButtons/ActionButtons';
import AlertLabels from './AlertLabels/AlertLabels';
@@ -19,7 +19,9 @@ export type AlertHeaderProps = {
};
};
function AlertHeader({ alertDetails }: AlertHeaderProps): JSX.Element {
const { state, alert, labels } = alertDetails;
const { state, alert: alertName, labels } = alertDetails;
const { alertRuleState } = useAlertRule();
const [updatedName, setUpdatedName] = useState(alertName);
const labelsWithoutSeverity = useMemo(
() =>
@@ -29,8 +31,6 @@ function AlertHeader({ alertDetails }: AlertHeaderProps): JSX.Element {
[labels],
);
const { alertRuleState } = useAlertRule();
return (
<div className="alert-info">
<div className="alert-info__info-wrapper">
@@ -38,7 +38,7 @@ function AlertHeader({ alertDetails }: AlertHeaderProps): JSX.Element {
<div className="alert-title-wrapper">
<AlertState state={alertRuleState ?? state} />
<div className="alert-title">
<LineClampedText text={alert} />
<LineClampedText text={updatedName || alertName} />
</div>
</div>
</div>
@@ -54,7 +54,11 @@ function AlertHeader({ alertDetails }: AlertHeaderProps): JSX.Element {
</div>
</div>
<div className="alert-info__action-buttons">
<AlertActionButtons alertDetails={alertDetails} ruleId={alertDetails.id} />
<AlertActionButtons
alertDetails={alertDetails}
ruleId={alertDetails.id}
setUpdatedName={setUpdatedName}
/>
</div>
</div>
);

View File

@@ -57,8 +57,11 @@ export const useAlertHistoryQueryParams = (): {
const startTime = params.get(QueryParams.startTime);
const endTime = params.get(QueryParams.endTime);
const relativeTimeParam = params.get(QueryParams.relativeTime);
const relativeTime =
params.get(QueryParams.relativeTime) ?? RelativeTimeMap['6hr'];
(relativeTimeParam === 'null' ? null : relativeTimeParam) ??
RelativeTimeMap['6hr'];
const intStartTime = parseInt(startTime || '0', 10);
const intEndTime = parseInt(endTime || '0', 10);
@@ -464,6 +467,44 @@ export const useAlertRuleDuplicate = ({
return { handleAlertDuplicate };
};
export const useAlertRuleUpdate = ({
alertDetails,
setUpdatedName,
intermediateName,
}: {
alertDetails: AlertDef;
setUpdatedName: (name: string) => void;
intermediateName: string;
}): {
handleAlertUpdate: () => void;
isLoading: boolean;
} => {
const { notifications } = useNotifications();
const handleError = useAxiosError();
const { mutate: updateAlertRule, isLoading } = useMutation(
[REACT_QUERY_KEY.UPDATE_ALERT_RULE, alertDetails.id],
save,
{
onMutate: () => setUpdatedName(intermediateName),
onSuccess: () =>
notifications.success({ message: 'Alert renamed successfully' }),
onError: (error) => {
setUpdatedName(alertDetails.alert);
handleError(error);
},
},
);
const handleAlertUpdate = (): void => {
updateAlertRule({
data: { ...alertDetails, alert: intermediateName },
id: alertDetails.id,
});
};
return { handleAlertUpdate, isLoading };
};
export const useAlertRuleDelete = ({
ruleId,

View File

@@ -1,26 +1,70 @@
/* eslint-disable no-nested-ternary */
import '../MessagingQueues.styles.scss';
import { Select, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import useUrlQuery from 'hooks/useUrlQuery';
import { ListMinus } from 'lucide-react';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { MessagingQueuesViewType } from '../MessagingQueuesUtils';
import { SelectLabelWithComingSoon } from '../MQCommon/MQCommon';
import {
MessagingQueuesViewType,
MessagingQueuesViewTypeOptions,
ProducerLatencyOptions,
} from '../MessagingQueuesUtils';
import DropRateView from '../MQDetails/DropRateView/DropRateView';
import MessagingQueueOverview from '../MQDetails/MessagingQueueOverview';
import MetricPage from '../MQDetails/MetricPage/MetricPage';
import MessagingQueuesDetails from '../MQDetails/MQDetails';
import MessagingQueuesConfigOptions from '../MQGraph/MQConfigOptions';
import MessagingQueuesGraph from '../MQGraph/MQGraph';
function MQDetailPage(): JSX.Element {
const history = useHistory();
const [
selectedView,
setSelectedView,
] = useState<MessagingQueuesViewTypeOptions>(
MessagingQueuesViewType.consumerLag.value,
);
const [
producerLatencyOption,
setproducerLatencyOption,
] = useState<ProducerLatencyOptions>(ProducerLatencyOptions.Producers);
const mqServiceView = useUrlQuery().get(
QueryParams.mqServiceView,
) as MessagingQueuesViewTypeOptions;
useEffect(() => {
logEvent('Messaging Queues: Detail page visited', {});
}, []);
useEffect(() => {
if (mqServiceView) {
setSelectedView(mqServiceView);
}
}, [mqServiceView]);
const updateUrlQuery = (query: Record<string, string | number>): void => {
const searchParams = new URLSearchParams(history.location.search);
Object.keys(query).forEach((key) => {
searchParams.set(key, query[key].toString());
});
history.push({
search: searchParams.toString(),
});
};
const showMessagingQueueDetails =
selectedView !== MessagingQueuesViewType.dropRate.value &&
selectedView !== MessagingQueuesViewType.metricPage.value;
return (
<div className="messaging-queue-container">
<div className="messaging-breadcrumb">
@@ -39,50 +83,61 @@ function MQDetailPage(): JSX.Element {
className="messaging-queue-options"
defaultValue={MessagingQueuesViewType.consumerLag.value}
popupClassName="messaging-queue-options-popup"
onChange={(value): void => {
setSelectedView(value);
updateUrlQuery({ [QueryParams.mqServiceView]: value });
}}
value={selectedView}
options={[
{
label: MessagingQueuesViewType.consumerLag.label,
value: MessagingQueuesViewType.consumerLag.value,
},
{
label: (
<SelectLabelWithComingSoon
label={MessagingQueuesViewType.partitionLatency.label}
/>
),
label: MessagingQueuesViewType.partitionLatency.label,
value: MessagingQueuesViewType.partitionLatency.value,
disabled: true,
},
{
label: (
<SelectLabelWithComingSoon
label={MessagingQueuesViewType.producerLatency.label}
/>
),
label: MessagingQueuesViewType.producerLatency.label,
value: MessagingQueuesViewType.producerLatency.value,
disabled: true,
},
{
label: (
<SelectLabelWithComingSoon
label={MessagingQueuesViewType.consumerLatency.label}
/>
),
value: MessagingQueuesViewType.consumerLatency.value,
disabled: true,
label: MessagingQueuesViewType.dropRate.label,
value: MessagingQueuesViewType.dropRate.value,
},
{
label: MessagingQueuesViewType.metricPage.label,
value: MessagingQueuesViewType.metricPage.value,
},
]}
/>
</div>
<DateTimeSelectionV2 showAutoRefresh={false} hideShareModal />
</div>
<div className="messaging-queue-main-graph">
<MessagingQueuesConfigOptions />
<MessagingQueuesGraph />
</div>
<div className="messaging-queue-details">
<MessagingQueuesDetails />
</div>
{selectedView === MessagingQueuesViewType.consumerLag.value ? (
<div className="messaging-queue-main-graph">
<MessagingQueuesConfigOptions />
<MessagingQueuesGraph />
</div>
) : selectedView === MessagingQueuesViewType.dropRate.value ? (
<DropRateView />
) : selectedView === MessagingQueuesViewType.metricPage.value ? (
<MetricPage />
) : (
<MessagingQueueOverview
selectedView={selectedView}
option={producerLatencyOption}
setOption={setproducerLatencyOption}
/>
)}
{showMessagingQueueDetails && (
<div className="messaging-queue-details">
<MessagingQueuesDetails
selectedView={selectedView}
producerLatencyOption={producerLatencyOption}
/>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,43 @@
.evaluation-time-selector {
display: flex;
align-items: center;
gap: 8px;
.eval-title {
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 28px;
color: var(--bg-vanilla-200);
}
.ant-selector {
background-color: var(--bg-ink-400);
border-radius: 4px;
border: 1px solid var(--bg-slate-400);
box-shadow: none;
}
}
.select-dropdown-render {
padding: 8px;
display: flex;
justify-content: center;
align-items: center;
width: 200px;
margin: 6px;
}
.lightMode {
.evaluation-time-selector {
.eval-title {
color: var(--bg-ink-400);
}
.ant-selector {
background-color: var(--bg-vanilla-200);
border: 1px solid var(--bg-ink-400);
}
}
}

View File

@@ -0,0 +1,249 @@
/* eslint-disable sonarjs/no-duplicate-string */
import '../MQDetails.style.scss';
import { Table, Typography } from 'antd';
import axios from 'axios';
import cx from 'classnames';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import ROUTES from 'constants/routes';
import { useNotifications } from 'hooks/useNotifications';
import { isNumber } from 'lodash-es';
import {
convertToTitleCase,
MessagingQueuesViewType,
RowData,
} from 'pages/MessagingQueues/MessagingQueuesUtils';
import { useEffect, useMemo, useState } from 'react';
import { useMutation } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { MessagingQueueServicePayload } from '../MQTables/getConsumerLagDetails';
import { getKafkaSpanEval } from '../MQTables/getKafkaSpanEval';
import {
convertToMilliseconds,
DropRateAPIResponse,
DropRateResponse,
} from './dropRateViewUtils';
import EvaluationTimeSelector from './EvaluationTimeSelector';
export function getTableData(data: DropRateResponse[]): RowData[] {
if (data?.length === 0) {
return [];
}
const tableData: RowData[] =
data?.map(
(row: DropRateResponse, index: number): RowData => ({
...(row.data as any), // todo-sagar
key: index,
}),
) || [];
return tableData;
}
// eslint-disable-next-line sonarjs/cognitive-complexity
export function getColumns(
data: DropRateResponse[],
visibleCounts: Record<number, number>,
handleShowMore: (index: number) => void,
): any[] {
if (data?.length === 0) {
return [];
}
const columnsOrder = [
'producer_service',
'consumer_service',
'breach_percentage',
'top_traceIDs',
'breached_spans',
'total_spans',
];
const columns: {
title: string;
dataIndex: string;
key: string;
}[] = columnsOrder.map((column) => ({
title: convertToTitleCase(column),
dataIndex: column,
key: column,
render: (
text: string | string[],
_record: any,
index: number,
): JSX.Element => {
if (Array.isArray(text)) {
const visibleCount = visibleCounts[index] || 4;
const visibleItems = text.slice(0, visibleCount);
const remainingCount = (text || []).length - visibleCount;
return (
<div>
<div className="trace-id-list">
{visibleItems.map((item, idx) => {
const shouldShowMore = remainingCount > 0 && idx === visibleCount - 1;
return (
<div key={item} className="traceid-style">
<Typography.Text
key={item}
className="traceid-text"
onClick={(): void => {
window.open(`${ROUTES.TRACE}/${item}`, '_blank');
}}
>
{item}
</Typography.Text>
{shouldShowMore && (
<Typography
onClick={(): void => handleShowMore(index)}
className="remaing-count"
>
+ {remainingCount} more
</Typography>
)}
</div>
);
})}
</div>
</div>
);
}
if (column === 'consumer_service' || column === 'producer_service') {
return (
<Typography.Link
onClick={(e): void => {
e.preventDefault();
e.stopPropagation();
window.open(`/services/${encodeURIComponent(text)}`, '_blank');
}}
>
{text}
</Typography.Link>
);
}
if (column === 'breach_percentage' && text) {
if (!isNumber(text))
return <Typography.Text>{text.toString()}</Typography.Text>;
return (
<Typography.Text>
{(typeof text === 'string' ? parseFloat(text) : text).toFixed(2)} %
</Typography.Text>
);
}
return <Typography.Text>{text}</Typography.Text>;
},
}));
return columns;
}
const showPaginationItem = (total: number, range: number[]): JSX.Element => (
<>
<Typography.Text className="numbers">
{range[0]} &#8212; {range[1]}
</Typography.Text>
<Typography.Text className="total"> of {total}</Typography.Text>
</>
);
function DropRateView(): JSX.Element {
const [columns, setColumns] = useState<any[]>([]);
const [tableData, setTableData] = useState<any[]>([]);
const { notifications } = useNotifications();
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const [data, setData] = useState<
DropRateAPIResponse['data']['result'][0]['list']
>([]);
const [interval, setInterval] = useState<string>('');
const [visibleCounts, setVisibleCounts] = useState<Record<number, number>>({});
const paginationConfig = useMemo(
() =>
tableData?.length > 10 && {
pageSize: 10,
showTotal: showPaginationItem,
showSizeChanger: false,
hideOnSinglePage: true,
},
[tableData],
);
const evaluationTime = useMemo(() => convertToMilliseconds(interval), [
interval,
]);
const tableApiPayload: MessagingQueueServicePayload = useMemo(
() => ({
start: minTime,
end: maxTime,
evalTime: evaluationTime * 1e6,
}),
[evaluationTime, maxTime, minTime],
);
const handleOnError = (error: Error): void => {
notifications.error({
message: axios.isAxiosError(error) ? error?.message : SOMETHING_WENT_WRONG,
});
};
const handleShowMore = (index: number): void => {
setVisibleCounts((prevCounts) => ({
...prevCounts,
[index]: (prevCounts[index] || 4) + 4,
}));
};
const { mutate: getViewDetails, isLoading } = useMutation(getKafkaSpanEval, {
onSuccess: (data) => {
if (data.payload) {
setData(data.payload.result[0].list);
}
},
onError: handleOnError,
});
useEffect(() => {
if (data?.length > 0) {
setColumns(getColumns(data, visibleCounts, handleShowMore));
setTableData(getTableData(data));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data, visibleCounts]);
useEffect(() => {
if (evaluationTime) {
getViewDetails(tableApiPayload);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [minTime, maxTime, evaluationTime]);
return (
<div className={cx('mq-overview-container', 'droprate-view')}>
<div className="mq-overview-title">
{MessagingQueuesViewType.dropRate.label}
<EvaluationTimeSelector setInterval={setInterval} />
</div>
<Table
className={cx('mq-table', 'pagination-left')}
pagination={paginationConfig}
size="middle"
columns={columns}
dataSource={tableData}
bordered={false}
loading={isLoading}
/>
</div>
);
}
export default DropRateView;

View File

@@ -0,0 +1,111 @@
import './DropRateView.styles.scss';
import { Input, Select, Typography } from 'antd';
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
const { Option } = Select;
interface SelectDropdownRenderProps {
menu: React.ReactNode;
inputValue: string;
handleInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
handleKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => void;
handleAddCustomValue: () => void;
}
function SelectDropdownRender({
menu,
inputValue,
handleInputChange,
handleAddCustomValue,
handleKeyDown,
}: SelectDropdownRenderProps): JSX.Element {
return (
<>
{menu}
<Input
placeholder="Enter custom time (ms)"
value={inputValue}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
onBlur={handleAddCustomValue}
className="select-dropdown-render"
/>
</>
);
}
function EvaluationTimeSelector({
setInterval,
}: {
setInterval: Dispatch<SetStateAction<string>>;
}): JSX.Element {
const [inputValue, setInputValue] = useState<string>('');
const [selectedInterval, setSelectedInterval] = useState<string | null>('5ms');
const [dropdownOpen, setDropdownOpen] = useState<boolean>(false);
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
setInputValue(e.target.value);
};
const handleSelectChange = (value: string): void => {
setSelectedInterval(value);
setInputValue('');
setDropdownOpen(false);
};
const handleAddCustomValue = (): void => {
setSelectedInterval(inputValue);
setInputValue(inputValue);
setDropdownOpen(false);
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>): void => {
if (e.key === 'Enter') {
e.preventDefault();
e.stopPropagation();
handleAddCustomValue();
}
};
const renderDropdown = (menu: React.ReactNode): JSX.Element => (
<SelectDropdownRender
menu={menu}
inputValue={inputValue}
handleInputChange={handleInputChange}
handleAddCustomValue={handleAddCustomValue}
handleKeyDown={handleKeyDown}
/>
);
useEffect(() => {
if (selectedInterval) {
setInterval(() => selectedInterval);
}
}, [selectedInterval, setInterval]);
return (
<div className="evaluation-time-selector">
<Typography.Text className="eval-title">
Evaluation Interval:
</Typography.Text>
<Select
style={{ width: 220 }}
placeholder="Select time interval (ms)"
value={selectedInterval}
onChange={handleSelectChange}
open={dropdownOpen}
onDropdownVisibleChange={setDropdownOpen}
dropdownRender={renderDropdown}
>
<Option value="1ms">1ms</Option>
<Option value="2ms">2ms</Option>
<Option value="5ms">5ms</Option>
<Option value="10ms">10ms</Option>
<Option value="15ms">15ms</Option>
</Select>
</div>
);
}
export default EvaluationTimeSelector;

View File

@@ -0,0 +1,46 @@
export function convertToMilliseconds(timeInput: string): number {
if (!timeInput.trim()) {
return 0;
}
const match = timeInput.match(/^(\d+)(ms|s|ns)?$/); // Match number and optional unit
if (!match) {
throw new Error(`Invalid time format: ${timeInput}`);
}
const value = parseInt(match[1], 10);
const unit = match[2] || 'ms'; // Default to 'ms' if no unit is provided
switch (unit) {
case 's':
return value * 1e3;
case 'ms':
return value;
case 'ns':
return value / 1e6;
default:
throw new Error('Invalid time format');
}
}
export interface DropRateResponse {
timestamp: string;
data: {
breach_percentage: number;
breached_spans: number;
consumer_service: string;
producer_service: string;
top_traceIDs: string[];
total_spans: number;
};
}
export interface DropRateAPIResponse {
status: string;
data: {
resultType: string;
result: {
queryName: string;
list: DropRateResponse[];
}[];
};
}

View File

@@ -4,3 +4,177 @@
flex-direction: column;
gap: 24px;
}
.mq-overview-container {
display: flex;
padding: 24px;
flex-direction: column;
align-items: start;
gap: 16px;
border-radius: 6px;
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-500);
.mq-overview-title {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
color: var(--bg-vanilla-200);
font-family: Inter;
font-size: 18px;
font-style: normal;
font-weight: 500;
line-height: 28px;
}
.mq-details-options {
letter-spacing: -0.06px;
cursor: pointer;
.ant-radio-button-wrapper {
border-color: var(--bg-slate-400);
color: var(--bg-vanilla-400);
}
.ant-radio-button-wrapper-checked {
background: var(--bg-slate-400);
color: var(--bg-vanilla-100);
}
.ant-radio-button-wrapper::before {
width: 0px;
}
}
}
.droprate-view {
.mq-table {
width: 100%;
.ant-table-content {
border-radius: 6px;
border: 1px solid var(--bg-slate-500);
box-shadow: 0px 4px 12px 0px rgba(0, 0, 0, 0.1);
}
.ant-table-tbody {
.ant-table-cell {
max-width: 250px;
border-bottom: none;
}
}
.ant-table-thead {
.ant-table-cell {
background-color: var(--bg-ink-500);
border-bottom: 1px solid var(--bg-slate-500);
}
}
}
.trace-id-list {
display: flex;
flex-direction: column;
gap: 4px;
width: max-content;
.traceid-style {
display: flex;
gap: 8px;
align-items: center;
.traceid-text {
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-slate-400);
padding: 2px;
cursor: pointer;
}
.remaing-count {
cursor: pointer;
color: var(--bg-vanilla-100);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: normal;
letter-spacing: -0.06px;
}
}
}
}
.pagination-left {
&.mq-table {
.ant-pagination {
justify-content: flex-start;
}
}
}
.lightMode {
.mq-overview-container {
background: var(--bg-vanilla-200);
border: 1px solid var(--bg-vanilla-300);
.mq-overview-title {
color: var(--bg-ink-400);
}
.mq-details-options {
.ant-radio-button-wrapper {
border-color: var(--bg-vanilla-300);
color: var(--bg-slate-200);
}
.ant-radio-button-wrapper-checked {
color: var(--bg-slate-200);
background: var(--bg-vanilla-300);
}
.ant-radio-button-wrapper-disabled {
background: var(--bg-vanilla-100);
color: var(--bg-vanilla-400);
}
}
}
.droprate-view {
.mq-table {
.ant-table-content {
border: 1px solid var(--bg-vanilla-300);
}
.ant-table-tbody {
.ant-table-cell {
background-color: var(--bg-vanilla-100);
}
}
.ant-table-thead {
.ant-table-cell {
background-color: var(--bg-vanilla-100);
border-bottom: 1px solid var(--bg-vanilla-300);
}
}
}
.no-data-style {
border: 1px solid var(--bg-vanilla-300);
}
}
.trace-id-list {
.traceid-style {
.traceid-text {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-300);
}
.remaing-count {
color: var(--bg-ink-400);
}
}
}
}

View File

@@ -1,65 +1,206 @@
import './MQDetails.style.scss';
import { Radio } from 'antd';
import { Dispatch, SetStateAction, useState } from 'react';
import { QueryParams } from 'constants/query';
import useUrlQuery from 'hooks/useUrlQuery';
import { isEmpty } from 'lodash-es';
import { Dispatch, SetStateAction, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import {
ConsumerLagDetailTitle,
ConsumerLagDetailType,
getMetaDataAndAPIPerView,
MessagingQueueServiceDetailType,
MessagingQueuesViewType,
MessagingQueuesViewTypeOptions,
ProducerLatencyOptions,
SelectedTimelineQuery,
} from '../MessagingQueuesUtils';
import { ComingSoon } from '../MQCommon/MQCommon';
import MessagingQueuesTable from './MQTables/MQTables';
const MQServiceDetailTypePerView = (
producerLatencyOption: ProducerLatencyOptions,
): Record<string, MessagingQueueServiceDetailType[]> => ({
[MessagingQueuesViewType.consumerLag.value]: [
MessagingQueueServiceDetailType.ConsumerDetails,
MessagingQueueServiceDetailType.ProducerDetails,
MessagingQueueServiceDetailType.NetworkLatency,
],
[MessagingQueuesViewType.partitionLatency.value]: [
MessagingQueueServiceDetailType.ConsumerDetails,
MessagingQueueServiceDetailType.ProducerDetails,
],
[MessagingQueuesViewType.producerLatency.value]: [
producerLatencyOption === ProducerLatencyOptions.Consumers
? MessagingQueueServiceDetailType.ConsumerDetails
: MessagingQueueServiceDetailType.ProducerDetails,
],
});
interface MessagingQueuesOptionsProps {
currentTab: MessagingQueueServiceDetailType;
setCurrentTab: Dispatch<SetStateAction<MessagingQueueServiceDetailType>>;
selectedView: MessagingQueuesViewTypeOptions;
producerLatencyOption: ProducerLatencyOptions;
}
function MessagingQueuesOptions({
currentTab,
setCurrentTab,
}: {
currentTab: ConsumerLagDetailType;
setCurrentTab: Dispatch<SetStateAction<ConsumerLagDetailType>>;
}): JSX.Element {
const [option, setOption] = useState<ConsumerLagDetailType>(currentTab);
selectedView,
producerLatencyOption,
}: MessagingQueuesOptionsProps): JSX.Element {
const handleChange = (value: MessagingQueueServiceDetailType): void => {
setCurrentTab(value);
};
const renderRadioButtons = (): JSX.Element[] => {
const detailTypes =
MQServiceDetailTypePerView(producerLatencyOption)[selectedView] || [];
return detailTypes.map((detailType) => (
<Radio.Button key={detailType} value={detailType}>
{ConsumerLagDetailTitle[detailType]}
</Radio.Button>
));
};
return (
<Radio.Group
onChange={(value): void => {
setOption(value.target.value);
setCurrentTab(value.target.value);
}}
value={option}
onChange={(e): void => handleChange(e.target.value)}
value={currentTab}
className="mq-details-options"
>
<Radio.Button value={ConsumerLagDetailType.ConsumerDetails} checked>
{ConsumerLagDetailTitle[ConsumerLagDetailType.ConsumerDetails]}
</Radio.Button>
<Radio.Button value={ConsumerLagDetailType.ProducerDetails}>
{ConsumerLagDetailTitle[ConsumerLagDetailType.ProducerDetails]}
</Radio.Button>
<Radio.Button value={ConsumerLagDetailType.NetworkLatency}>
{ConsumerLagDetailTitle[ConsumerLagDetailType.NetworkLatency]}
</Radio.Button>
<Radio.Button
value={ConsumerLagDetailType.PartitionHostMetrics}
disabled
className="disabled-option"
>
{ConsumerLagDetailTitle[ConsumerLagDetailType.PartitionHostMetrics]}
<ComingSoon />
</Radio.Button>
{renderRadioButtons()}
</Radio.Group>
);
}
function MessagingQueuesDetails(): JSX.Element {
const [currentTab, setCurrentTab] = useState<ConsumerLagDetailType>(
ConsumerLagDetailType.ConsumerDetails,
const checkValidityOfDetailConfigs = (
selectedTimelineQuery: SelectedTimelineQuery,
selectedView: MessagingQueuesViewTypeOptions,
currentTab: MessagingQueueServiceDetailType,
configDetails?: {
[key: string]: string;
},
// eslint-disable-next-line sonarjs/cognitive-complexity
): boolean => {
if (selectedView === MessagingQueuesViewType.consumerLag.value) {
return !(
isEmpty(selectedTimelineQuery) ||
(!selectedTimelineQuery?.group &&
!selectedTimelineQuery?.topic &&
!selectedTimelineQuery?.partition)
);
}
if (selectedView === MessagingQueuesViewType.partitionLatency.value) {
if (isEmpty(configDetails)) {
return false;
}
return Boolean(configDetails?.topic && configDetails?.partition);
}
if (selectedView === MessagingQueuesViewType.producerLatency.value) {
if (isEmpty(configDetails)) {
return false;
}
if (currentTab === MessagingQueueServiceDetailType.ProducerDetails) {
return Boolean(
configDetails?.topic &&
configDetails?.partition &&
configDetails?.service_name,
);
}
return Boolean(configDetails?.topic && configDetails?.service_name);
}
return selectedView === MessagingQueuesViewType.dropRate.value;
};
function MessagingQueuesDetails({
selectedView,
producerLatencyOption,
}: {
selectedView: MessagingQueuesViewTypeOptions;
producerLatencyOption: ProducerLatencyOptions;
}): JSX.Element {
const [currentTab, setCurrentTab] = useState<MessagingQueueServiceDetailType>(
MessagingQueueServiceDetailType.ConsumerDetails,
);
useEffect(() => {
if (
producerLatencyOption &&
selectedView === MessagingQueuesViewType.producerLatency.value
) {
setCurrentTab(
producerLatencyOption === ProducerLatencyOptions.Consumers
? MessagingQueueServiceDetailType.ConsumerDetails
: MessagingQueueServiceDetailType.ProducerDetails,
);
}
}, [selectedView, producerLatencyOption]);
const urlQuery = useUrlQuery();
const timelineQuery = decodeURIComponent(
urlQuery.get(QueryParams.selectedTimelineQuery) || '',
);
const timelineQueryData: SelectedTimelineQuery = useMemo(
() => (timelineQuery ? JSON.parse(timelineQuery) : {}),
[timelineQuery],
);
const configDetails = decodeURIComponent(
urlQuery.get(QueryParams.configDetail) || '',
);
const configDetailQueryData: {
[key: string]: string;
} = useMemo(() => (configDetails ? JSON.parse(configDetails) : {}), [
configDetails,
]);
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const serviceConfigDetails = useMemo(
() =>
getMetaDataAndAPIPerView({
detailType: currentTab,
minTime,
maxTime,
selectedTimelineQuery: timelineQueryData,
configDetails: configDetailQueryData,
}),
[configDetailQueryData, currentTab, maxTime, minTime, timelineQueryData],
);
return (
<div className="mq-details">
<MessagingQueuesOptions
currentTab={currentTab}
setCurrentTab={setCurrentTab}
selectedView={selectedView}
producerLatencyOption={producerLatencyOption}
/>
<MessagingQueuesTable
currentTab={currentTab}
selectedView={selectedView}
tableApi={serviceConfigDetails[selectedView]?.tableApi}
validConfigPresent={checkValidityOfDetailConfigs(
timelineQueryData,
selectedView,
currentTab,
configDetailQueryData,
)}
tableApiPayload={serviceConfigDetails[selectedView]?.tableApiPayload}
/>
<MessagingQueuesTable currentTab={currentTab} />
</div>
);
}

View File

@@ -1,4 +1,7 @@
.mq-tables-container {
width: 100%;
height: 100%;
.mq-table-title {
display: flex;
align-items: center;
@@ -31,9 +34,6 @@
.ant-table-tbody {
.ant-table-cell {
max-width: 250px;
background-color: var(--bg-ink-400);
border-bottom: none;
}
}
@@ -63,6 +63,21 @@
}
}
.mq-table {
&.mq-overview-row-clickable {
.ant-table-row {
background-color: var(--bg-ink-400);
&:hover {
cursor: pointer;
background-color: var(--bg-slate-400) !important;
color: var(--bg-vanilla-400);
transition: background-color 0.3s ease, color 0.3s ease;
}
}
}
}
.lightMode {
.mq-tables-container {
.mq-table-title {

View File

@@ -1,9 +1,11 @@
/* eslint-disable no-nested-ternary */
/* eslint-disable react/require-default-props */
import './MQTables.styles.scss';
import { Skeleton, Table, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import axios from 'axios';
import { isNumber } from 'chart.js/helpers';
import cx from 'classnames';
import { ColumnTypeRender } from 'components/Logs/TableView/types';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { QueryParams } from 'constants/query';
@@ -13,27 +15,31 @@ import useUrlQuery from 'hooks/useUrlQuery';
import { isEmpty } from 'lodash-es';
import {
ConsumerLagDetailTitle,
ConsumerLagDetailType,
convertToTitleCase,
MessagingQueueServiceDetailType,
MessagingQueuesViewType,
MessagingQueuesViewTypeOptions,
RowData,
SelectedTimelineQuery,
setConfigDetail,
} from 'pages/MessagingQueues/MessagingQueuesUtils';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { useMutation } from 'react-query';
import { useHistory } from 'react-router-dom';
import { useHistory, useLocation } from 'react-router-dom';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
ConsumerLagPayload,
getConsumerLagDetails,
MessagingQueueServicePayload,
MessagingQueuesPayloadProps,
} from './getConsumerLagDetails';
const INITIAL_PAGE_SIZE = 10;
// eslint-disable-next-line sonarjs/cognitive-complexity
export function getColumns(
data: MessagingQueuesPayloadProps['payload'],
history: History<unknown>,
): RowData[] {
console.log(data);
if (data?.result?.length === 0) {
return [];
}
@@ -105,10 +111,25 @@ const showPaginationItem = (total: number, range: number[]): JSX.Element => (
</>
);
// eslint-disable-next-line sonarjs/cognitive-complexity
function MessagingQueuesTable({
currentTab,
selectedView,
tableApiPayload,
tableApi,
validConfigPresent = false,
type = 'Detail',
}: {
currentTab: ConsumerLagDetailType;
currentTab?: MessagingQueueServiceDetailType;
selectedView: MessagingQueuesViewTypeOptions;
tableApiPayload?: MessagingQueueServicePayload;
tableApi: (
props: MessagingQueueServicePayload,
) => Promise<
SuccessResponse<MessagingQueuesPayloadProps['payload']> | ErrorResponse
>;
validConfigPresent?: boolean;
type?: 'Detail' | 'Overview';
}): JSX.Element {
const [columns, setColumns] = useState<any[]>([]);
const [tableData, setTableData] = useState<any[]>([]);
@@ -118,15 +139,26 @@ function MessagingQueuesTable({
const timelineQuery = decodeURIComponent(
urlQuery.get(QueryParams.selectedTimelineQuery) || '',
);
const timelineQueryData: SelectedTimelineQuery = useMemo(
() => (timelineQuery ? JSON.parse(timelineQuery) : {}),
[timelineQuery],
);
const configDetails = decodeURIComponent(
urlQuery.get(QueryParams.configDetail) || '',
);
const configDetailQueryData: {
[key: string]: string;
} = useMemo(() => (configDetails ? JSON.parse(configDetails) : {}), [
configDetails,
]);
const paginationConfig = useMemo(
() =>
tableData?.length > 20 && {
pageSize: 20,
tableData?.length > INITIAL_PAGE_SIZE && {
pageSize: INITIAL_PAGE_SIZE,
showTotal: showPaginationItem,
showSizeChanger: false,
hideOnSinglePage: true,
@@ -134,28 +166,14 @@ function MessagingQueuesTable({
[tableData],
);
const props: ConsumerLagPayload = useMemo(
() => ({
start: (timelineQueryData?.start || 0) * 1e9,
end: (timelineQueryData?.end || 0) * 1e9,
variables: {
partition: timelineQueryData?.partition,
topic: timelineQueryData?.topic,
consumer_group: timelineQueryData?.group,
},
detailType: currentTab,
}),
[currentTab, timelineQueryData],
);
const handleConsumerDetailsOnError = (error: Error): void => {
notifications.error({
message: axios.isAxiosError(error) ? error?.message : SOMETHING_WENT_WRONG,
});
};
const { mutate: getConsumerDetails, isLoading } = useMutation(
getConsumerLagDetails,
const { mutate: getViewDetails, isLoading, error, isError } = useMutation(
tableApi,
{
onSuccess: (data) => {
if (data.payload) {
@@ -167,57 +185,92 @@ function MessagingQueuesTable({
},
);
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => getConsumerDetails(props), [currentTab, props]);
useEffect(
() => {
if (validConfigPresent && tableApiPayload) {
getViewDetails(tableApiPayload);
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[currentTab, selectedView, tableApiPayload],
);
const isLogEventCalled = useRef<boolean>(false);
const [selectedRowKey, setSelectedRowKey] = useState<React.Key>();
const [, setSelectedRows] = useState<any>();
const location = useLocation();
const isEmptyDetails = (timelineQueryData: SelectedTimelineQuery): boolean => {
const isEmptyDetail =
isEmpty(timelineQueryData) ||
(!timelineQueryData?.group &&
!timelineQueryData?.topic &&
!timelineQueryData?.partition);
const onRowClick = (record: { [key: string]: string }): void => {
const selectedKey = record.key;
if (!isEmptyDetail && !isLogEventCalled.current) {
logEvent('Messaging Queues: More details viewed', {
'tab-option': ConsumerLagDetailTitle[currentTab],
variables: {
group: timelineQueryData?.group,
topic: timelineQueryData?.topic,
partition: timelineQueryData?.partition,
},
});
isLogEventCalled.current = true;
if (`${selectedKey}_${selectedView}` === selectedRowKey) {
setSelectedRowKey(undefined);
setSelectedRows({});
setConfigDetail(urlQuery, location, history, {});
} else {
setSelectedRowKey(`${selectedKey}_${selectedView}`);
setSelectedRows(record);
if (!isEmpty(record)) {
setConfigDetail(urlQuery, location, history, record);
}
}
return isEmptyDetail;
};
const subtitle =
selectedView === MessagingQueuesViewType.consumerLag.value
? `${timelineQueryData?.group || ''} ${timelineQueryData?.topic || ''} ${
timelineQueryData?.partition || ''
}`
: `${configDetailQueryData?.service_name || ''} ${
configDetailQueryData?.topic || ''
} ${configDetailQueryData?.partition || ''}`;
return (
<div className="mq-tables-container">
{isEmptyDetails(timelineQueryData) ? (
{!validConfigPresent ? (
<div className="no-data-style">
<Typography.Text>
Click on a co-ordinate above to see the details
{selectedView === MessagingQueuesViewType.consumerLag.value
? 'Click on a co-ordinate above to see the details'
: 'Click on a row above to see the details'}
</Typography.Text>
<Skeleton />
</div>
) : isError ? (
<div className="no-data-style">
<Typography.Text>{error?.message || SOMETHING_WENT_WRONG}</Typography.Text>
</div>
) : (
<>
<div className="mq-table-title">
{ConsumerLagDetailTitle[currentTab]}
<div className="mq-table-subtitle">{`${timelineQueryData?.group || ''} ${
timelineQueryData?.topic || ''
} ${timelineQueryData?.partition || ''}`}</div>
</div>
{currentTab && (
<div className="mq-table-title">
{ConsumerLagDetailTitle[currentTab]}
<div className="mq-table-subtitle">{subtitle}</div>
</div>
)}
<Table
className="mq-table"
className={cx(
'mq-table',
type !== 'Detail' ? 'mq-overview-row-clickable' : 'pagination-left',
)}
pagination={paginationConfig}
size="middle"
columns={columns}
dataSource={tableData}
bordered={false}
loading={isLoading}
onRow={(record): any =>
type !== 'Detail'
? {
onClick: (): void => onRowClick(record),
}
: {}
}
rowClassName={(record): any =>
`${record.key}_${selectedView}` === selectedRowKey
? 'ant-table-row-selected'
: ''
}
/>
</>
)}

View File

@@ -1,19 +1,18 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { ConsumerLagDetailType } from 'pages/MessagingQueues/MessagingQueuesUtils';
import { MessagingQueueServiceDetailType } from 'pages/MessagingQueues/MessagingQueuesUtils';
import { ErrorResponse, SuccessResponse } from 'types/api';
export interface ConsumerLagPayload {
export interface MessagingQueueServicePayload {
start?: number | string;
end?: number | string;
variables: {
variables?: {
partition?: string;
topic?: string;
consumer_group?: string;
service_name?: string;
};
detailType: ConsumerLagDetailType;
detailType?: MessagingQueueServiceDetailType | 'producer' | 'consumer';
evalTime?: number;
}
export interface MessagingQueuesPayloadProps {
@@ -36,26 +35,22 @@ export interface MessagingQueuesPayloadProps {
}
export const getConsumerLagDetails = async (
props: ConsumerLagPayload,
props: MessagingQueueServicePayload,
): Promise<
SuccessResponse<MessagingQueuesPayloadProps['payload']> | ErrorResponse
> => {
const { detailType, ...restProps } = props;
try {
const response = await axios.post(
`/messaging-queues/kafka/consumer-lag/${props.detailType}`,
{
...restProps,
},
);
const response = await axios.post(
`/messaging-queues/kafka/consumer-lag/${props.detailType}`,
{
...restProps,
},
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler((error as AxiosError) || SOMETHING_WENT_WRONG);
}
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};

View File

@@ -0,0 +1,23 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { DropRateAPIResponse } from '../DropRateView/dropRateViewUtils';
import { MessagingQueueServicePayload } from './getConsumerLagDetails';
export const getKafkaSpanEval = async (
props: Omit<MessagingQueueServicePayload, 'detailType' | 'variables'>,
): Promise<SuccessResponse<DropRateAPIResponse['data']> | ErrorResponse> => {
const { start, end, evalTime } = props;
const response = await axios.post(`messaging-queues/kafka/span/evaluation`, {
start,
end,
eval_time: evalTime,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};

View File

@@ -0,0 +1,33 @@
import axios from 'api';
import { MessagingQueueServiceDetailType } from 'pages/MessagingQueues/MessagingQueuesUtils';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
MessagingQueueServicePayload,
MessagingQueuesPayloadProps,
} from './getConsumerLagDetails';
export const getPartitionLatencyDetails = async (
props: MessagingQueueServicePayload,
): Promise<
SuccessResponse<MessagingQueuesPayloadProps['payload']> | ErrorResponse
> => {
const { detailType, ...rest } = props;
let endpoint = '';
if (detailType === MessagingQueueServiceDetailType.ConsumerDetails) {
endpoint = `/messaging-queues/kafka/partition-latency/consumer`;
} else {
endpoint = `/messaging-queues/kafka/consumer-lag/producer-details`;
}
const response = await axios.post(endpoint, {
...rest,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};

View File

@@ -0,0 +1,27 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
MessagingQueueServicePayload,
MessagingQueuesPayloadProps,
} from './getConsumerLagDetails';
export const getPartitionLatencyOverview = async (
props: Omit<MessagingQueueServicePayload, 'detailType' | 'variables'>,
): Promise<
SuccessResponse<MessagingQueuesPayloadProps['payload']> | ErrorResponse
> => {
const response = await axios.post(
`/messaging-queues/kafka/partition-latency/overview`,
{
...props,
},
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};

View File

@@ -0,0 +1,26 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
MessagingQueueServicePayload,
MessagingQueuesPayloadProps,
} from './getConsumerLagDetails';
export const getTopicThroughputDetails = async (
props: MessagingQueueServicePayload,
): Promise<
SuccessResponse<MessagingQueuesPayloadProps['payload']> | ErrorResponse
> => {
const { detailType, ...rest } = props;
const endpoint = `/messaging-queues/kafka/topic-throughput/${detailType}`;
const response = await axios.post(endpoint, {
...rest,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};

View File

@@ -0,0 +1,29 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
MessagingQueueServicePayload,
MessagingQueuesPayloadProps,
} from './getConsumerLagDetails';
export const getTopicThroughputOverview = async (
props: Omit<MessagingQueueServicePayload, 'variables'>,
): Promise<
SuccessResponse<MessagingQueuesPayloadProps['payload']> | ErrorResponse
> => {
const { detailType, start, end } = props;
const response = await axios.post(
`messaging-queues/kafka/topic-throughput/${detailType}`,
{
start,
end,
},
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};

View File

@@ -0,0 +1,110 @@
import './MQDetails.style.scss';
import { Radio } from 'antd';
import { Dispatch, SetStateAction } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import {
MessagingQueuesViewType,
MessagingQueuesViewTypeOptions,
ProducerLatencyOptions,
} from '../MessagingQueuesUtils';
import { MessagingQueueServicePayload } from './MQTables/getConsumerLagDetails';
import { getKafkaSpanEval } from './MQTables/getKafkaSpanEval';
import { getPartitionLatencyOverview } from './MQTables/getPartitionLatencyOverview';
import { getTopicThroughputOverview } from './MQTables/getTopicThroughputOverview';
import MessagingQueuesTable from './MQTables/MQTables';
type SelectedViewType = keyof typeof MessagingQueuesViewType;
function PartitionLatencyTabs({
option,
setOption,
}: {
option: ProducerLatencyOptions;
setOption: Dispatch<SetStateAction<ProducerLatencyOptions>>;
}): JSX.Element {
return (
<Radio.Group
onChange={(e): void => setOption(e.target.value)}
value={option}
className="mq-details-options"
>
<Radio.Button
value={ProducerLatencyOptions.Producers}
key={ProducerLatencyOptions.Producers}
>
{ProducerLatencyOptions.Producers}
</Radio.Button>
<Radio.Button
value={ProducerLatencyOptions.Consumers}
key={ProducerLatencyOptions.Consumers}
>
{ProducerLatencyOptions.Consumers}
</Radio.Button>
</Radio.Group>
);
}
const getTableApi = (selectedView: MessagingQueuesViewTypeOptions): any => {
if (selectedView === MessagingQueuesViewType.producerLatency.value) {
return getTopicThroughputOverview;
}
if (selectedView === MessagingQueuesViewType.dropRate.value) {
return getKafkaSpanEval;
}
return getPartitionLatencyOverview;
};
function MessagingQueueOverview({
selectedView,
option,
setOption,
}: {
selectedView: MessagingQueuesViewTypeOptions;
option: ProducerLatencyOptions;
setOption: Dispatch<SetStateAction<ProducerLatencyOptions>>;
}): JSX.Element {
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const tableApiPayload: MessagingQueueServicePayload = {
variables: {},
start: minTime,
end: maxTime,
detailType:
// eslint-disable-next-line no-nested-ternary
selectedView === MessagingQueuesViewType.producerLatency.value
? option === ProducerLatencyOptions.Producers
? 'producer'
: 'consumer'
: undefined,
evalTime:
selectedView === MessagingQueuesViewType.dropRate.value
? 2363404
: undefined,
};
return (
<div className="mq-overview-container">
{selectedView === MessagingQueuesViewType.producerLatency.value ? (
<PartitionLatencyTabs option={option} setOption={setOption} />
) : (
<div className="mq-overview-title">
{MessagingQueuesViewType[selectedView as SelectedViewType].label}
</div>
)}
<MessagingQueuesTable
selectedView={selectedView}
tableApiPayload={tableApiPayload}
tableApi={getTableApi(selectedView)}
validConfigPresent
type="Overview"
/>
</div>
);
}
export default MessagingQueueOverview;

View File

@@ -0,0 +1,115 @@
import { Typography } from 'antd';
import { CardContainer } from 'container/GridCardLayout/styles';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useTranslation } from 'react-i18next';
import { Widgets } from 'types/api/dashboard/getAll';
import MetricPageGridGraph from './MetricPageGraph';
import {
averageRequestLatencyWidgetData,
brokerCountWidgetData,
brokerNetworkThroughputWidgetData,
bytesConsumedWidgetData,
consumerFetchRateWidgetData,
consumerGroupMemberWidgetData,
consumerLagByGroupWidgetData,
consumerOffsetWidgetData,
ioWaitTimeWidgetData,
kafkaProducerByteRateWidgetData,
messagesConsumedWidgetData,
producerFetchRequestPurgatoryWidgetData,
requestResponseWidgetData,
requestTimesWidgetData,
} from './MetricPageUtil';
interface MetricSectionProps {
title: string;
description: string;
graphCount: Widgets[];
}
function MetricSection({
title,
description,
graphCount,
}: MetricSectionProps): JSX.Element {
const isDarkMode = useIsDarkMode();
return (
<div className="metric-column-graph">
<CardContainer className="row-card" isDarkMode={isDarkMode}>
<div className="row-panel">
<Typography.Text className="section-title">{title}</Typography.Text>
</div>
</CardContainer>
<Typography.Text className="graph-description">
{description}
</Typography.Text>
<div className="metric-page-grid">
{graphCount.map((widgetData) => (
<MetricPageGridGraph
key={`graph-${widgetData.id}`}
widgetData={widgetData}
/>
))}
</div>
</div>
);
}
function MetricColumnGraphs(): JSX.Element {
const { t } = useTranslation('messagingQueues');
const metricsData = [
{
title: t('metricGraphCategory.brokerMetrics.title'),
description: t('metricGraphCategory.brokerMetrics.description'),
graphCount: [
brokerCountWidgetData,
requestTimesWidgetData,
producerFetchRequestPurgatoryWidgetData,
brokerNetworkThroughputWidgetData,
],
id: 'broker-metrics',
},
{
title: t('metricGraphCategory.producerMetrics.title'),
description: t('metricGraphCategory.producerMetrics.description'),
graphCount: [
ioWaitTimeWidgetData,
requestResponseWidgetData,
averageRequestLatencyWidgetData,
kafkaProducerByteRateWidgetData,
bytesConsumedWidgetData,
],
id: 'producer-metrics',
},
{
title: t('metricGraphCategory.consumerMetrics.title'),
description: t('metricGraphCategory.consumerMetrics.description'),
graphCount: [
consumerOffsetWidgetData,
consumerGroupMemberWidgetData,
consumerLagByGroupWidgetData,
consumerFetchRateWidgetData,
messagesConsumedWidgetData,
],
id: 'consumer-metrics',
},
];
return (
<div className="metric-column-graph-container">
{metricsData.map((metric) => (
<MetricSection
key={metric.id}
title={metric.title}
description={metric.description}
graphCount={metric?.graphCount || []}
/>
))}
</div>
);
}
export default MetricColumnGraphs;

View File

@@ -0,0 +1,128 @@
.metric-page {
padding: 20px;
display: flex;
flex-direction: column;
gap: 32px;
.metric-page-container {
display: flex;
flex-direction: column;
.row-panel {
padding-left: 10px;
}
.metric-page-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
align-items: flex-start;
gap: 10px;
.metric-graph {
height: 320px;
padding: 10px;
width: 100%;
box-sizing: border-box;
}
}
@media (max-width: 768px) {
.metric-page-grid {
grid-template-columns: 1fr;
}
}
.graph-description {
padding: 16px 10px 16px 10px;
}
}
.row-panel {
border-radius: 4px;
background: rgba(18, 19, 23, 0.4);
padding: 8px;
display: flex;
gap: 6px;
align-items: center;
height: 48px !important;
.ant-typography {
font-size: 14px;
font-weight: 500;
}
.row-panel-section {
display: flex;
gap: 6px;
align-items: center;
.row-icon {
color: var(--bg-vanilla-400);
cursor: pointer;
}
.section-title {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
letter-spacing: -0.07px;
}
}
}
.metric-column-graph-container {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 10px;
.metric-column-graph {
display: flex;
flex-direction: column;
gap: 10px;
.row-panel {
justify-content: center;
}
.metric-page-grid {
display: flex;
flex-direction: column;
flex: 1;
min-width: 0;
gap: 10px;
.metric-graph {
height: 320px;
padding: 10px;
width: 100%;
box-sizing: border-box;
}
}
}
}
@media (max-width: 768px) {
.metric-column-graph-container {
grid-template-columns: 1fr;
}
}
}
.lightMode {
.metric-page {
.row-panel {
.row-panel-section {
.row-icon {
color: var(--bg-ink-300);
}
.section-title {
color: var(--bg-ink-300);
}
}
}
}
}

View File

@@ -0,0 +1,134 @@
import './MetricPage.styles.scss';
import { Typography } from 'antd';
import cx from 'classnames';
import { CardContainer } from 'container/GridCardLayout/styles';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { ChevronDown, ChevronUp } from 'lucide-react';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Widgets } from 'types/api/dashboard/getAll';
import MetricColumnGraphs from './MetricColumnGraphs';
import MetricPageGridGraph from './MetricPageGraph';
import {
cpuRecentUtilizationWidgetData,
currentOffsetPartitionWidgetData,
insyncReplicasWidgetData,
jvmGcCollectionsElapsedWidgetData,
jvmGCCountWidgetData,
jvmMemoryHeapWidgetData,
oldestOffsetWidgetData,
partitionCountPerTopicWidgetData,
} from './MetricPageUtil';
interface CollapsibleMetricSectionProps {
title: string;
description: string;
graphCount: Widgets[];
isCollapsed: boolean;
onToggle: () => void;
}
function CollapsibleMetricSection({
title,
description,
graphCount,
isCollapsed,
onToggle,
}: CollapsibleMetricSectionProps): JSX.Element {
const isDarkMode = useIsDarkMode();
return (
<div className="metric-page-container">
<CardContainer className="row-card" isDarkMode={isDarkMode}>
<div className={cx('row-panel')}>
<div className="row-panel-section">
<Typography.Text className="section-title">{title}</Typography.Text>
{isCollapsed ? (
<ChevronDown size={14} onClick={onToggle} className="row-icon" />
) : (
<ChevronUp size={14} onClick={onToggle} className="row-icon" />
)}
</div>
</div>
</CardContainer>
{!isCollapsed && (
<>
<Typography.Text className="graph-description">
{description}
</Typography.Text>
<div className="metric-page-grid">
{graphCount.map((widgetData) => (
<MetricPageGridGraph
key={`graph-${widgetData.id}`}
widgetData={widgetData}
/>
))}
</div>
</>
)}
</div>
);
}
function MetricPage(): JSX.Element {
const [collapsedSections, setCollapsedSections] = useState<{
[key: string]: boolean;
}>({
producerMetrics: false,
consumerMetrics: false,
});
const toggleCollapse = (key: string): void => {
setCollapsedSections((prev) => ({
...prev,
[key]: !prev[key],
}));
};
const { t } = useTranslation('messagingQueues');
const metricSections = [
{
key: 'bokerJVMMetrics',
title: t('metricGraphCategory.brokerJVMMetrics.title'),
description: t('metricGraphCategory.brokerJVMMetrics.description'),
graphCount: [
jvmGCCountWidgetData,
jvmGcCollectionsElapsedWidgetData,
cpuRecentUtilizationWidgetData,
jvmMemoryHeapWidgetData,
],
},
{
key: 'partitionMetrics',
title: t('metricGraphCategory.partitionMetrics.title'),
description: t('metricGraphCategory.partitionMetrics.description'),
graphCount: [
partitionCountPerTopicWidgetData,
currentOffsetPartitionWidgetData,
oldestOffsetWidgetData,
insyncReplicasWidgetData,
],
},
];
return (
<div className="metric-page">
<MetricColumnGraphs />
{metricSections.map(({ key, title, description, graphCount }) => (
<CollapsibleMetricSection
key={key}
title={title}
description={description}
graphCount={graphCount}
isCollapsed={collapsedSections[key]}
onToggle={(): void => toggleCollapse(key)}
/>
))}
</div>
);
}
export default MetricPage;

View File

@@ -0,0 +1,59 @@
import './MetricPage.styles.scss';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { ViewMenuAction } from 'container/GridCardLayout/config';
import GridCard from 'container/GridCardLayout/GridCard';
import { Card } from 'container/GridCardLayout/styles';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery';
import { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { useHistory, useLocation } from 'react-router-dom';
import { UpdateTimeInterval } from 'store/actions';
import { Widgets } from 'types/api/dashboard/getAll';
function MetricPageGridGraph({
widgetData,
}: {
widgetData: Widgets;
}): JSX.Element {
const history = useHistory();
const { pathname } = useLocation();
const dispatch = useDispatch();
const urlQuery = useUrlQuery();
const isDarkMode = useIsDarkMode();
const onDragSelect = useCallback(
(start: number, end: number) => {
const startTimestamp = Math.trunc(start);
const endTimestamp = Math.trunc(end);
urlQuery.set(QueryParams.startTime, startTimestamp.toString());
urlQuery.set(QueryParams.endTime, endTimestamp.toString());
const generatedUrl = `${pathname}?${urlQuery.toString()}`;
history.push(generatedUrl);
if (startTimestamp !== endTimestamp) {
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
}
},
[dispatch, history, pathname, urlQuery],
);
return (
<Card
isDarkMode={isDarkMode}
$panelType={PANEL_TYPES.TIME_SERIES}
className="metric-graph"
>
<GridCard
widget={widgetData}
headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect}
/>
</Card>
);
}
export default MetricPageGridGraph;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,270 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import './MessagingQueueHealthCheck.styles.scss';
import { CaretDownOutlined, LoadingOutlined } from '@ant-design/icons';
import {
Modal,
Select,
Spin,
Tooltip,
Tree,
TreeDataNode,
Typography,
} from 'antd';
import { OnboardingStatusResponse } from 'api/messagingQueues/onboarding/getOnboardingStatus';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { History } from 'history';
import { Bolt, Check, OctagonAlert, X } from 'lucide-react';
import { ReactNode, useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { isCloudUser } from 'utils/app';
import { v4 as uuid } from 'uuid';
import {
KAFKA_SETUP_DOC_LINK,
MessagingQueueHealthCheckService,
} from '../MessagingQueuesUtils';
interface AttributeCheckListProps {
visible: boolean;
onClose: () => void;
onboardingStatusResponses: {
title: string;
data: OnboardingStatusResponse['data'];
errorMsg?: string;
}[];
loading: boolean;
}
export enum AttributesFilters {
ALL = 'all',
SUCCESS = 'success',
ERROR = 'error',
}
function ErrorTitleAndKey({
title,
parentTitle,
history,
isCloudUserVal,
errorMsg,
isLeaf,
}: {
title: string;
parentTitle: string;
isCloudUserVal: boolean;
history: History<unknown>;
errorMsg?: string;
isLeaf?: boolean;
}): TreeDataNode {
const handleRedirection = (): void => {
let link = '';
switch (parentTitle) {
case 'Consumers':
link = `${ROUTES.GET_STARTED_APPLICATION_MONITORING}?${QueryParams.getStartedSource}=kafka&${QueryParams.getStartedSourceService}=${MessagingQueueHealthCheckService.Consumers}`;
break;
case 'Producers':
link = `${ROUTES.GET_STARTED_APPLICATION_MONITORING}?${QueryParams.getStartedSource}=kafka&${QueryParams.getStartedSourceService}=${MessagingQueueHealthCheckService.Producers}`;
break;
case 'Kafka':
link = `${ROUTES.GET_STARTED_INFRASTRUCTURE_MONITORING}?${QueryParams.getStartedSource}=kafka&${QueryParams.getStartedSourceService}=${MessagingQueueHealthCheckService.Kafka}`;
break;
default:
link = '';
}
if (isCloudUserVal && !!link) {
history.push(link);
} else {
window.open(KAFKA_SETUP_DOC_LINK, '_blank');
}
};
return {
key: `${title}-key-${uuid()}`,
title: (
<div className="attribute-error-title">
<Typography.Text className="tree-text" ellipsis={{ tooltip: title }}>
{title}
</Typography.Text>
<Tooltip title={errorMsg}>
<div
className="attribute-error-warning"
onClick={(e): void => {
e.preventDefault();
handleRedirection();
}}
>
<OctagonAlert size={14} />
Fix
</div>
</Tooltip>
</div>
),
isLeaf,
};
}
function AttributeLabels({ title }: { title: ReactNode }): JSX.Element {
return (
<div className="attribute-label">
<Bolt size={14} />
{title}
</div>
);
}
function treeTitleAndKey({
title,
isLeaf,
}: {
title: string;
isLeaf?: boolean;
}): TreeDataNode {
return {
key: `${title}-key-${uuid()}`,
title: (
<div className="attribute-success-title">
<Typography.Text className="tree-text" ellipsis={{ tooltip: title }}>
{title}
</Typography.Text>
{isLeaf && (
<div className="success-attribute-icon">
<Tooltip title="Success">
<Check size={14} />
</Tooltip>
</div>
)}
</div>
),
isLeaf,
};
}
function generateTreeDataNodes(
response: OnboardingStatusResponse['data'],
parentTitle: string,
isCloudUserVal: boolean,
history: History<unknown>,
): TreeDataNode[] {
return response
.map((item) => {
if (item.attribute) {
if (item.status === '1') {
return treeTitleAndKey({ title: item.attribute, isLeaf: true });
}
if (item.status === '0') {
return ErrorTitleAndKey({
title: item.attribute,
errorMsg: item.error_message || '',
parentTitle,
history,
isCloudUserVal,
});
}
}
return null;
})
.filter(Boolean) as TreeDataNode[];
}
function AttributeCheckList({
visible,
onClose,
onboardingStatusResponses,
loading,
}: AttributeCheckListProps): JSX.Element {
const [filter, setFilter] = useState<AttributesFilters>(AttributesFilters.ALL);
const [treeData, setTreeData] = useState<TreeDataNode[]>([]);
const handleFilterChange = (value: AttributesFilters): void => {
setFilter(value);
};
const isCloudUserVal = isCloudUser();
const history = useHistory();
useEffect(() => {
const filteredData = onboardingStatusResponses.map((response) => {
if (response.errorMsg) {
return ErrorTitleAndKey({
title: response.title,
errorMsg: response.errorMsg,
isLeaf: true,
parentTitle: response.title,
history,
isCloudUserVal,
});
}
let filteredData = response.data;
if (filter === AttributesFilters.SUCCESS) {
filteredData = response.data.filter((item) => item.status === '1');
} else if (filter === AttributesFilters.ERROR) {
filteredData = response.data.filter((item) => item.status === '0');
}
return {
...treeTitleAndKey({ title: response.title }),
children: generateTreeDataNodes(
filteredData,
response.title,
isCloudUserVal,
history,
),
};
});
setTreeData(filteredData);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filter, onboardingStatusResponses]);
return (
<Modal
title="Kafka Service Attributes"
open={visible}
onCancel={onClose}
footer={false}
className="mq-health-check-modal"
closeIcon={<X size={14} />}
>
{loading ? (
<div className="loader-container">
<Spin indicator={<LoadingOutlined spin />} size="large" />
</div>
) : (
<div className="modal-content">
<Select
defaultValue={AttributesFilters.ALL}
className="attribute-select"
onChange={handleFilterChange}
options={[
{
value: AttributesFilters.ALL,
label: AttributeLabels({ title: 'Attributes: All' }),
},
{
value: AttributesFilters.SUCCESS,
label: AttributeLabels({ title: 'Attributes: Success' }),
},
{
value: AttributesFilters.ERROR,
label: AttributeLabels({ title: 'Attributes: Error' }),
},
]}
/>
<Tree
showLine
switcherIcon={<CaretDownOutlined />}
treeData={treeData}
height={450}
className="attribute-tree"
/>
</div>
)}
</Modal>
);
}
export default AttributeCheckList;

View File

@@ -0,0 +1,242 @@
.mq-health-check-modal {
.ant-modal-content {
border-radius: 4px;
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
.ant-modal-close {
margin-top: 4px;
}
.ant-modal-header {
border-bottom: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
margin-bottom: 16px;
padding-bottom: 4px;
.ant-modal-title {
color: var(--bg-vanilla-100);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 600;
line-height: 22px;
letter-spacing: 0.52px;
}
}
.modal-content {
display: flex;
flex-direction: column;
gap: 8px;
padding: 8px;
background: var(--bg-ink-300);
.attribute-select {
align-items: center;
display: flex;
gap: 8px;
width: 170px;
.ant-select-selector {
display: flex;
height: 28px !important;
padding: 8px;
align-items: center;
gap: 4px;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
}
}
.attribute-tree {
padding: 8px;
}
.tree-text {
color: var(--bg-vanilla-400);
font-family: 'Geist Mono';
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
width: 328px;
}
.ant-tree {
.ant-tree-title {
cursor: default;
.attribute-error-title {
display: flex;
align-items: center;
color: var(--bg-amber-400);
height: 24px;
.tree-text {
color: var(--bg-amber-400);
}
.attribute-error-warning {
display: flex;
align-items: center;
gap: 6px;
font-family: 'Geist Mono';
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
cursor: pointer;
}
}
.attribute-success-title {
display: flex;
align-items: center;
height: 24px;
.success-attribute-icon {
width: 44px;
color: var(--bg-vanilla-400);
display: flex;
> svg {
margin-left: auto;
}
}
}
}
.ant-tree-treenode {
width: 100%;
.ant-tree-node-content-wrapper {
width: 100%;
max-width: 380px;
}
}
}
}
.loader-container {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
padding: 8px;
background: var(--bg-ink-300);
height: 156px;
}
}
}
.attribute-label {
display: flex;
align-items: center;
gap: 8px;
}
.config-btn {
display: flex;
align-items: center;
height: 28px;
border-radius: 2px;
border: none;
box-shadow: none;
background: var(--bg-slate-500);
&.missing-config-btn {
background: rgba(255, 205, 86, 0.1);
color: var(--bg-amber-400);
&:hover {
color: var(--bg-amber-300) !important;
}
}
.config-btn-content {
display: flex;
align-items: center;
margin-right: 8px;
border-right: 1px solid rgba(255, 215, 120, 0.1);
padding-right: 8px;
}
}
.lightMode {
.mq-health-check-modal {
.ant-modal-content {
border: 1px solid var(--bg-vanilla-400);
background: var(--bg-vanilla-200);
.ant-modal-header {
border-bottom: 1px solid var(--bg-vanilla-400);
background: var(--bg-vanilla-200);
.ant-modal-title {
color: var(--bg-ink-300);
}
}
.modal-content {
background: var(--bg-vanilla-100);
.attribute-select {
.ant-select-selector {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-200);
}
}
.tree-text {
color: var(--bg-ink-300);
}
.ant-tree {
.ant-tree-title {
.attribute-error-title {
color: var(--bg-amber-500);
.tree-text {
color: var(--bg-amber-500);
}
}
.attribute-success-title {
.success-attribute-icon {
color: var(--bg-ink-300);
}
}
}
}
}
.loader-container {
background: var(--bg-ink-300);
}
}
}
.config-btn {
background: var(--bg-vanilla-300);
&.missing-config-btn {
background: var(--bg-amber-100);
color: var(--bg-amber-500);
&:hover {
color: var(--bg-amber-600) !important;
}
}
.missing-config-btn {
.config-btn-content {
border-right: 1px solid var(--bg-amber-600);
}
}
}
}

View File

@@ -0,0 +1,133 @@
/* eslint-disable sonarjs/no-duplicate-string */
import './MessagingQueueHealthCheck.styles.scss';
import { Button } from 'antd';
import cx from 'classnames';
import { useOnboardingStatus } from 'hooks/messagingQueue / onboarding/useOnboardingStatus';
import { Bolt, FolderTree } from 'lucide-react';
import { useEffect, useMemo, useState } from 'react';
import { MessagingQueueHealthCheckService } from '../MessagingQueuesUtils';
import AttributeCheckList from './AttributeCheckList';
interface MessagingQueueHealthCheckProps {
serviceToInclude: string[];
}
function MessagingQueueHealthCheck({
serviceToInclude,
}: MessagingQueueHealthCheckProps): JSX.Element {
const [loading, setLoading] = useState(false);
const [checkListOpen, setCheckListOpen] = useState(false);
// Consumer Data
const {
data: consumerData,
error: consumerError,
isFetching: consumerLoading,
} = useOnboardingStatus(
{
enabled: !!serviceToInclude.filter(
(service) => service === MessagingQueueHealthCheckService.Consumers,
).length,
},
MessagingQueueHealthCheckService.Consumers,
);
// Producer Data
const {
data: producerData,
error: producerError,
isFetching: producerLoading,
} = useOnboardingStatus(
{
enabled: !!serviceToInclude.filter(
(service) => service === MessagingQueueHealthCheckService.Producers,
).length,
},
MessagingQueueHealthCheckService.Producers,
);
// Kafka Data
const {
data: kafkaData,
error: kafkaError,
isFetching: kafkaLoading,
} = useOnboardingStatus(
{
enabled: !!serviceToInclude.filter(
(service) => service === MessagingQueueHealthCheckService.Kafka,
).length,
},
MessagingQueueHealthCheckService.Kafka,
);
// combined loading and update state
useEffect(() => {
setLoading(consumerLoading || producerLoading || kafkaLoading);
}, [consumerLoading, producerLoading, kafkaLoading]);
const missingConfiguration = useMemo(() => {
const consumerMissing =
(serviceToInclude.includes(MessagingQueueHealthCheckService.Consumers) &&
consumerData?.payload?.data?.filter((item) => item.status === '0')
.length) ||
0;
const producerMissing =
(serviceToInclude.includes(MessagingQueueHealthCheckService.Producers) &&
producerData?.payload?.data?.filter((item) => item.status === '0')
.length) ||
0;
const kafkaMissing =
(serviceToInclude.includes(MessagingQueueHealthCheckService.Kafka) &&
kafkaData?.payload?.data?.filter((item) => item.status === '0').length) ||
0;
return consumerMissing + producerMissing + kafkaMissing;
}, [consumerData, producerData, kafkaData, serviceToInclude]);
return (
<div>
<Button
onClick={(): void => setCheckListOpen(true)}
loading={loading}
className={cx(
'config-btn',
missingConfiguration ? 'missing-config-btn' : '',
)}
icon={<Bolt size={12} />}
>
<div className="config-btn-content">
{missingConfiguration
? `Missing Configuration (${missingConfiguration})`
: 'Configuration'}
</div>
<FolderTree size={14} />
</Button>
<AttributeCheckList
visible={checkListOpen}
onClose={(): void => setCheckListOpen(false)}
onboardingStatusResponses={[
{
title: 'Consumers',
data: consumerData?.payload?.data || [],
errorMsg: (consumerError || consumerData?.error) as string,
},
{
title: 'Producers',
data: producerData?.payload?.data || [],
errorMsg: (producerError || producerData?.error) as string,
},
{
title: 'Kafka',
data: kafkaData?.payload?.data || [],
errorMsg: (kafkaError || kafkaData?.error) as string,
},
].filter((item) => serviceToInclude.includes(item.title.toLowerCase()))}
loading={loading}
/>
</div>
);
}
export default MessagingQueueHealthCheck;

View File

@@ -47,7 +47,7 @@
.header-config {
display: flex;
gap: 10px;
gap: 12px;
align-items: center;
.messaging-queue-options {
@@ -106,6 +106,8 @@
.mq-details-options {
letter-spacing: -0.06px;
cursor: pointer;
.ant-radio-button-wrapper {
border-color: var(--bg-slate-400);
color: var(--bg-vanilla-400);
@@ -219,6 +221,29 @@
}
}
}
:nth-child(2),
:nth-child(4) {
border-left: none !important;
border-right: none !important;
}
&.summary-section {
.overview-info-card {
min-height: 144px;
.card-title {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 500;
line-height: 22px;
letter-spacing: 0.52px;
text-transform: uppercase;
}
}
}
}
.summary-section {
@@ -312,6 +337,10 @@
.messaging-breadcrumb {
color: var(--bg-ink-400);
border-bottom: 1px solid var(--bg-vanilla-300);
.message-queue-text {
color: var(--bg-ink-400);
}
}
.messaging-header {
color: var(--bg-ink-400);

View File

@@ -1,47 +1,38 @@
/* eslint-disable sonarjs/no-duplicate-string */
import './MessagingQueues.styles.scss';
import { ExclamationCircleFilled } from '@ant-design/icons';
import { Color } from '@signozhq/design-tokens';
import { Button, Modal } from 'antd';
import { Button } from 'antd';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { Calendar, ListMinus } from 'lucide-react';
import { ListMinus } from 'lucide-react';
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom';
import { isCloudUser } from 'utils/app';
import MessagingQueueHealthCheck from './MessagingQueueHealthCheck/MessagingQueueHealthCheck';
import {
KAFKA_SETUP_DOC_LINK,
MessagingQueueHealthCheckService,
MessagingQueuesViewType,
} from './MessagingQueuesUtils';
import { ComingSoon } from './MQCommon/MQCommon';
function MessagingQueues(): JSX.Element {
const history = useHistory();
const { t } = useTranslation('messagingQueuesKafkaOverview');
const { confirm } = Modal;
const showConfirm = (): void => {
const redirectToDetailsPage = (callerView?: string): void => {
logEvent('Messaging Queues: View details clicked', {
page: 'Messaging Queues Overview',
source: 'Consumer Latency view',
source: callerView,
});
confirm({
icon: <ExclamationCircleFilled />,
content: t('confirmModal.content'),
className: 'overview-confirm-modal',
onOk() {
logEvent('Messaging Queues: Proceed button clicked', {
page: 'Messaging Queues Overview',
});
history.push(ROUTES.MESSAGING_QUEUES_DETAIL);
},
okText: t('confirmModal.okText'),
});
history.push(
`${ROUTES.MESSAGING_QUEUES_DETAIL}?${QueryParams.mqServiceView}=${callerView}`,
);
};
const isCloudUserVal = isCloudUser();
@@ -69,7 +60,16 @@ function MessagingQueues(): JSX.Element {
{t('breadcrumb')}
</div>
<div className="messaging-header">
<div className="header-config">{t('header')}</div>
<div className="header-config">
{t('header')} /
<MessagingQueueHealthCheck
serviceToInclude={[
MessagingQueueHealthCheckService.Consumers,
MessagingQueueHealthCheckService.Producers,
MessagingQueueHealthCheckService.Kafka,
]}
/>
</div>
<DateTimeSelectionV2 showAutoRefresh={false} hideShareModal />
</div>
<div className="messaging-overview">
@@ -86,7 +86,7 @@ function MessagingQueues(): JSX.Element {
type="default"
onClick={(): void =>
getStartedRedirect(
ROUTES.GET_STARTED_APPLICATION_MONITORING,
`${ROUTES.GET_STARTED_APPLICATION_MONITORING}?${QueryParams.getStartedSource}=kafka&${QueryParams.getStartedSourceService}=${MessagingQueueHealthCheckService.Consumers}`,
'Configure Consumer',
)
}
@@ -105,7 +105,7 @@ function MessagingQueues(): JSX.Element {
type="default"
onClick={(): void =>
getStartedRedirect(
ROUTES.GET_STARTED_APPLICATION_MONITORING,
`${ROUTES.GET_STARTED_APPLICATION_MONITORING}?${QueryParams.getStartedSource}=kafka&${QueryParams.getStartedSourceService}=${MessagingQueueHealthCheckService.Producers}`,
'Configure Producer',
)
}
@@ -124,7 +124,7 @@ function MessagingQueues(): JSX.Element {
type="default"
onClick={(): void =>
getStartedRedirect(
ROUTES.GET_STARTED_INFRASTRUCTURE_MONITORING,
`${ROUTES.GET_STARTED_INFRASTRUCTURE_MONITORING}?${QueryParams.getStartedSource}=kafka&${QueryParams.getStartedSourceService}=${MessagingQueueHealthCheckService.Kafka}`,
'Monitor kafka',
)
}
@@ -134,55 +134,98 @@ function MessagingQueues(): JSX.Element {
</div>
</div>
</div>
<div className="summary-section">
<div className="summary-card">
<div className="summary-title">
<p>{MessagingQueuesViewType.consumerLag.label}</p>
<div className="time-value">
<Calendar size={14} color={Color.BG_SLATE_200} />
<p className="time-value">1D</p>
</div>
<p className="overview-text">{t('overviewSummarySection.title')}</p>
<p className="overview-subtext">{t('overviewSummarySection.subtitle')}</p>
<div className={cx('overview-doc-area', 'summary-section')}>
<div className="overview-info-card">
<div>
<p className="card-title">{t('summarySection.consumer.title')}</p>
<p className="card-info-text">
{t('summarySection.consumer.description')}
</p>
</div>
<div className="view-detail-btn">
<Button type="primary" onClick={showConfirm}>
<div className="button-grp">
<Button
type="default"
onClick={(): void =>
redirectToDetailsPage(MessagingQueuesViewType.consumerLag.value)
}
>
{t('summarySection.viewDetailsButton')}
</Button>
</div>
</div>
<div className="summary-card coming-soon-card">
<div className="summary-title">
<p>{MessagingQueuesViewType.partitionLatency.label}</p>
<div className="time-value">
<Calendar size={14} color={Color.BG_SLATE_200} />
<p className="time-value">1D</p>
</div>
<div className="overview-info-card">
<div>
<p className="card-title">{t('summarySection.producer.title')}</p>
<p className="card-info-text">
{t('summarySection.producer.description')}
</p>
</div>
<div className="view-detail-btn">
<ComingSoon />
<div className="button-grp">
<Button
type="default"
onClick={(): void =>
redirectToDetailsPage(MessagingQueuesViewType.producerLatency.value)
}
>
{t('summarySection.viewDetailsButton')}
</Button>
</div>
</div>
<div className="summary-card coming-soon-card">
<div className="summary-title">
<p>{MessagingQueuesViewType.producerLatency.label}</p>
<div className="time-value">
<Calendar size={14} color={Color.BG_SLATE_200} />
<p className="time-value">1D</p>
</div>
<div className="overview-info-card">
<div>
<p className="card-title">{t('summarySection.partition.title')}</p>
<p className="card-info-text">
{t('summarySection.partition.description')}
</p>
</div>
<div className="view-detail-btn">
<ComingSoon />
<div className="button-grp">
<Button
type="default"
onClick={(): void =>
redirectToDetailsPage(MessagingQueuesViewType.partitionLatency.value)
}
>
{t('summarySection.viewDetailsButton')}
</Button>
</div>
</div>
<div className="summary-card coming-soon-card">
<div className="summary-title">
<p>{MessagingQueuesViewType.consumerLatency.label}</p>
<div className="time-value">
<Calendar size={14} color={Color.BG_SLATE_200} />
<p className="time-value">1D</p>
</div>
<div className="overview-info-card">
<div>
<p className="card-title">{t('summarySection.dropRate.title')}</p>
<p className="card-info-text">
{t('summarySection.dropRate.description')}
</p>
</div>
<div className="view-detail-btn">
<ComingSoon />
<div className="button-grp">
<Button
type="default"
onClick={(): void =>
redirectToDetailsPage(MessagingQueuesViewType.dropRate.value)
}
>
{t('summarySection.viewDetailsButton')}
</Button>
</div>
</div>
<div className="overview-info-card">
<div>
<p className="card-title">{t('summarySection.metricPage.title')}</p>
<p className="card-info-text">
{t('summarySection.metricPage.description')}
</p>
</div>
<div className="button-grp">
<Button
type="default"
onClick={(): void =>
redirectToDetailsPage(MessagingQueuesViewType.metricPage.value)
}
>
{t('summarySection.viewDetailsButton')}
</Button>
</div>
</div>
</div>

View File

@@ -1,14 +1,24 @@
import { OnboardingStatusResponse } from 'api/messagingQueues/onboarding/getOnboardingStatus';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetWidgetQueryBuilderProps } from 'container/MetricsApplication/types';
import { History, Location } from 'history';
import { isEmpty } from 'lodash-es';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import { v4 as uuid } from 'uuid';
import {
getConsumerLagDetails,
MessagingQueueServicePayload,
MessagingQueuesPayloadProps,
} from './MQDetails/MQTables/getConsumerLagDetails';
import { getPartitionLatencyDetails } from './MQDetails/MQTables/getPartitionLatencyDetails';
import { getTopicThroughputDetails } from './MQDetails/MQTables/getTopicThroughputDetails';
export const KAFKA_SETUP_DOC_LINK =
'https://signoz.io/docs/messaging-queues/kafka?utm_source=product&utm_medium=kafka-get-started';
@@ -24,14 +34,17 @@ export type RowData = {
[key: string]: string | number;
};
export enum ConsumerLagDetailType {
export enum MessagingQueueServiceDetailType {
ConsumerDetails = 'consumer-details',
ProducerDetails = 'producer-details',
NetworkLatency = 'network-latency',
PartitionHostMetrics = 'partition-host-metric',
}
export const ConsumerLagDetailTitle: Record<ConsumerLagDetailType, string> = {
export const ConsumerLagDetailTitle: Record<
MessagingQueueServiceDetailType,
string
> = {
'consumer-details': 'Consumer Groups Details',
'producer-details': 'Producer Details',
'network-latency': 'Network Latency',
@@ -205,21 +218,177 @@ export function setSelectedTimelineQuery(
history.replace(generatedUrl);
}
export enum MessagingQueuesViewTypeOptions {
ConsumerLag = 'consumerLag',
PartitionLatency = 'partitionLatency',
ProducerLatency = 'producerLatency',
DropRate = 'dropRate',
MetricPage = 'metricPage',
}
export const MessagingQueuesViewType = {
consumerLag: {
label: 'Consumer Lag view',
value: 'consumerLag',
value: MessagingQueuesViewTypeOptions.ConsumerLag,
},
partitionLatency: {
label: 'Partition Latency view',
value: 'partitionLatency',
value: MessagingQueuesViewTypeOptions.PartitionLatency,
},
producerLatency: {
label: 'Producer Latency view',
value: 'producerLatency',
value: MessagingQueuesViewTypeOptions.ProducerLatency,
},
consumerLatency: {
label: 'Consumer latency view',
value: 'consumerLatency',
dropRate: {
label: 'Drop Rate view',
value: MessagingQueuesViewTypeOptions.DropRate,
},
metricPage: {
label: 'Metric view',
value: MessagingQueuesViewTypeOptions.MetricPage,
},
};
export function setConfigDetail(
urlQuery: URLSearchParams,
location: Location<unknown>,
history: History<unknown>,
paramsToSet?: {
[key: string]: string;
},
): void {
// remove "key" and its value from the paramsToSet object
const { key, ...restParamsToSet } = paramsToSet || {};
if (!isEmpty(restParamsToSet)) {
const configDetail = {
...restParamsToSet,
};
urlQuery.set(
QueryParams.configDetail,
encodeURIComponent(JSON.stringify(configDetail)),
);
} else {
urlQuery.delete(QueryParams.configDetail);
}
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
history.replace(generatedUrl);
}
export enum ProducerLatencyOptions {
Producers = 'Producers',
Consumers = 'Consumers',
}
interface MetaDataAndAPI {
tableApiPayload: MessagingQueueServicePayload;
tableApi: (
props: MessagingQueueServicePayload,
) => Promise<
SuccessResponse<MessagingQueuesPayloadProps['payload']> | ErrorResponse
>;
}
interface MetaDataAndAPIPerView {
detailType: MessagingQueueServiceDetailType;
selectedTimelineQuery: SelectedTimelineQuery;
configDetails?: {
[key: string]: string;
};
minTime: number;
maxTime: number;
}
export const getMetaDataAndAPIPerView = (
metaDataProps: MetaDataAndAPIPerView,
): Record<string, MetaDataAndAPI> => {
const {
detailType,
minTime,
maxTime,
selectedTimelineQuery,
configDetails,
} = metaDataProps;
return {
[MessagingQueuesViewType.consumerLag.value]: {
tableApiPayload: {
start: (selectedTimelineQuery?.start || 0) * 1e9,
end: (selectedTimelineQuery?.end || 0) * 1e9,
variables: {
partition: selectedTimelineQuery?.partition,
topic: selectedTimelineQuery?.topic,
consumer_group: selectedTimelineQuery?.group,
},
detailType,
},
tableApi: getConsumerLagDetails,
},
[MessagingQueuesViewType.partitionLatency.value]: {
tableApiPayload: {
start: minTime,
end: maxTime,
variables: {
partition: configDetails?.partition,
topic: configDetails?.topic,
consumer_group: configDetails?.group,
},
detailType,
},
tableApi: getPartitionLatencyDetails,
},
[MessagingQueuesViewType.producerLatency.value]: {
tableApiPayload: {
start: minTime,
end: maxTime,
variables: {
partition: configDetails?.partition,
topic: configDetails?.topic,
service_name: configDetails?.service_name,
},
detailType,
},
tableApi: getTopicThroughputDetails,
},
};
};
interface OnboardingStatusAttributeData {
overallStatus: string;
allAvailableAttributes: string[];
attributeDataWithError: { attributeName: string; errorMsg: string }[];
}
export const getAttributeDataFromOnboardingStatus = (
onboardingStatus?: OnboardingStatusResponse | null,
): OnboardingStatusAttributeData => {
const allAvailableAttributes: string[] = [];
const attributeDataWithError: {
attributeName: string;
errorMsg: string;
}[] = [];
if (onboardingStatus?.data && !isEmpty(onboardingStatus?.data)) {
onboardingStatus.data.forEach((status) => {
if (status.attribute) {
allAvailableAttributes.push(status.attribute);
if (status.status === '0') {
attributeDataWithError.push({
attributeName: status.attribute,
errorMsg: status.error_message || '',
});
}
}
});
}
return {
overallStatus: attributeDataWithError.length ? 'error' : 'success',
allAvailableAttributes,
attributeDataWithError,
};
};
export enum MessagingQueueHealthCheckService {
Consumers = 'consumers',
Producers = 'producers',
Kafka = 'kafka',
}

View File

@@ -19,7 +19,7 @@ export const defaultSeasonality = 'hourly';
export interface AlertDef {
id?: number;
alertType?: string;
alert?: string;
alert: string;
ruleType?: string;
frequency?: string;
condition: RuleCondition;

View File

@@ -9,6 +9,7 @@ export interface ILog {
severityNumber: number;
body: string;
resources_string: Record<string, never>;
scope_string: Record<string, never>;
attributesString: Record<string, never>;
attributes_string: Record<string, never>;
attributesInt: Record<string, never>;
@@ -22,6 +23,7 @@ type OmitAttributesResources = Pick<
Exclude<
keyof ILog,
| 'resources_string'
| 'scope_string'
| 'attributesString'
| 'attributes_string'
| 'attributesInt'
@@ -32,4 +34,5 @@ type OmitAttributesResources = Pick<
export type ILogAggregateAttributesResources = OmitAttributesResources & {
attributes: Record<string, never>;
resources: Record<string, never>;
scope: Record<string, never>;
};

File diff suppressed because it is too large Load Diff

2
go.mod
View File

@@ -8,7 +8,7 @@ require (
github.com/ClickHouse/clickhouse-go/v2 v2.25.0
github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd
github.com/SigNoz/signoz-otel-collector v0.111.5
github.com/SigNoz/signoz-otel-collector v0.111.8
github.com/SigNoz/zap_otlp/zap_otlp_encoder v0.0.0-20230822164844-1b861a431974
github.com/SigNoz/zap_otlp/zap_otlp_sync v0.0.0-20230822164844-1b861a431974
github.com/antonmedv/expr v1.15.3

Some files were not shown because too many files have changed in this diff Show More