mirror of
https://github.com/SigNoz/signoz.git
synced 2026-05-07 02:50:31 +01:00
Compare commits
36 Commits
v0.57.0-cl
...
fixProduce
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf74ac7b5e | ||
|
|
55a4056aa5 | ||
|
|
e974e9d47f | ||
|
|
577a169508 | ||
|
|
939e2a3570 | ||
|
|
b64326070c | ||
|
|
63872983c6 | ||
|
|
831540eaf0 | ||
|
|
22c10f9479 | ||
|
|
e748fb0655 | ||
|
|
fdc54a62a9 | ||
|
|
abe0ab69b0 | ||
|
|
e623c92615 | ||
|
|
dc5917db01 | ||
|
|
d6a7f0b6f4 | ||
|
|
471803115e | ||
|
|
8403a3362d | ||
|
|
64d46bc855 | ||
|
|
c9fee27604 | ||
|
|
f1b6b2d3d8 | ||
|
|
468f056530 | ||
|
|
7086470ce2 | ||
|
|
352296c6cd | ||
|
|
975307a8b8 | ||
|
|
12377be809 | ||
|
|
9d90b8d19c | ||
|
|
5005923ef4 | ||
|
|
db4338be42 | ||
|
|
c7d0598ec0 | ||
|
|
4978fb9599 | ||
|
|
7b18c3ba06 | ||
|
|
92cdb36879 | ||
|
|
580f0b816e | ||
|
|
b770fc2457 | ||
|
|
c177230cce | ||
|
|
2112047a02 |
83
.github/workflows/docs.yml
vendored
Normal file
83
.github/workflows/docs.yml
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
name: "Update PR labels and Block PR until related docs are shipped for the feature"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
types: [opened, edited, labeled, unlabeled]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
docs_label_check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check PR Title and Manage Labels
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const prTitle = context.payload.pull_request.title;
|
||||
const prNumber = context.payload.pull_request.number;
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
|
||||
// Fetch the current PR details to get labels
|
||||
const pr = await github.rest.pulls.get({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: prNumber
|
||||
});
|
||||
|
||||
const labels = pr.data.labels.map(label => label.name);
|
||||
|
||||
if (prTitle.startsWith('feat:')) {
|
||||
const hasDocsRequired = labels.includes('docs required');
|
||||
const hasDocsShipped = labels.includes('docs shipped');
|
||||
const hasDocsNotRequired = labels.includes('docs not required');
|
||||
|
||||
// If "docs not required" is present, skip the checks
|
||||
if (hasDocsNotRequired && !hasDocsRequired) {
|
||||
console.log("Skipping checks due to 'docs not required' label.");
|
||||
return; // Exit the script early
|
||||
}
|
||||
|
||||
// If "docs shipped" is present, remove "docs required" if it exists
|
||||
if (hasDocsShipped && hasDocsRequired) {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
name: 'docs required'
|
||||
});
|
||||
console.log("Removed 'docs required' label.");
|
||||
}
|
||||
|
||||
// Add "docs required" label if neither "docs shipped" nor "docs required" are present
|
||||
if (!hasDocsRequired && !hasDocsShipped) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
labels: ['docs required']
|
||||
});
|
||||
console.log("Added 'docs required' label.");
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch the updated labels after any changes
|
||||
const updatedPr = await github.rest.pulls.get({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: prNumber
|
||||
});
|
||||
|
||||
const updatedLabels = updatedPr.data.labels.map(label => label.name);
|
||||
const updatedHasDocsRequired = updatedLabels.includes('docs required');
|
||||
const updatedHasDocsShipped = updatedLabels.includes('docs shipped');
|
||||
|
||||
// Block PR if "docs required" is still present and "docs shipped" is missing
|
||||
if (updatedHasDocsRequired && !updatedHasDocsShipped) {
|
||||
core.setFailed("This PR requires documentation. Please remove the 'docs required' label and add the 'docs shipped' label to proceed.");
|
||||
}
|
||||
2
.github/workflows/staging-deployment.yaml
vendored
2
.github/workflows/staging-deployment.yaml
vendored
@@ -31,7 +31,6 @@ jobs:
|
||||
GCP_ZONE: ${{ secrets.GCP_ZONE }}
|
||||
GCP_INSTANCE: ${{ secrets.GCP_INSTANCE }}
|
||||
CLOUDSDK_CORE_DISABLE_PROMPTS: 1
|
||||
KAFKA_SPAN_EVAL: true
|
||||
run: |
|
||||
read -r -d '' COMMAND <<EOF || true
|
||||
echo "GITHUB_BRANCH: ${GITHUB_BRANCH}"
|
||||
@@ -39,6 +38,7 @@ jobs:
|
||||
export DOCKER_TAG="${GITHUB_SHA:0:7}" # needed for child process to access it
|
||||
export OTELCOL_TAG="main"
|
||||
export PATH="/usr/local/go/bin/:$PATH" # needed for Golang to work
|
||||
export KAFKA_SPAN_EVAL="true"
|
||||
docker system prune --force
|
||||
docker pull signoz/signoz-otel-collector:main
|
||||
docker pull signoz/signoz-schema-migrator:main
|
||||
|
||||
@@ -191,6 +191,7 @@ services:
|
||||
- GODEBUG=netdns=go
|
||||
- TELEMETRY_ENABLED=true
|
||||
- DEPLOYMENT_TYPE=docker-standalone-amd
|
||||
- KAFKA_SPAN_EVAL=${KAFKA_SPAN_EVAL:-false}
|
||||
restart: on-failure
|
||||
healthcheck:
|
||||
test:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -3,6 +3,7 @@ package license
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
@@ -48,6 +49,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 +108,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 +272,53 @@ 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) error {
|
||||
|
||||
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 fmt.Errorf("insert license failed: license marshal error")
|
||||
}
|
||||
|
||||
_, err = r.db.ExecContext(ctx,
|
||||
query,
|
||||
l.ID,
|
||||
l.Key,
|
||||
string(licenseData),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
zap.L().Error("error in inserting license data: ", zap.Error(err))
|
||||
return fmt.Errorf("failed to insert license in db: %v", 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
|
||||
}
|
||||
|
||||
@@ -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, model.InternalError(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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
170
ee/query-service/model/license_test.go
Normal file
170
ee/query-service/model/license_test.go
Normal 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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"go.signoz.io/signoz/pkg/query-service/constants"
|
||||
basemodel "go.signoz.io/signoz/pkg/query-service/model"
|
||||
)
|
||||
|
||||
@@ -8,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"
|
||||
@@ -134,6 +146,13 @@ var BasicPlan = basemodel.FeatureSet{
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.HostsInfraMonitoring,
|
||||
Active: constants.EnableHostsInfraMonitoring(),
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
}
|
||||
|
||||
var ProPlan = basemodel.FeatureSet{
|
||||
@@ -249,6 +268,13 @@ var ProPlan = basemodel.FeatureSet{
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.HostsInfraMonitoring,
|
||||
Active: constants.EnableHostsInfraMonitoring(),
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
}
|
||||
|
||||
var EnterprisePlan = basemodel.FeatureSet{
|
||||
@@ -378,4 +404,11 @@ var EnterprisePlan = basemodel.FeatureSet{
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.HostsInfraMonitoring,
|
||||
Active: constants.EnableHostsInfraMonitoring(),
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
@@ -126,7 +127,7 @@
|
||||
"uplot": "1.6.31",
|
||||
"uuid": "^8.3.2",
|
||||
"web-vitals": "^0.2.4",
|
||||
"webpack": "5.88.2",
|
||||
"webpack": "5.94.0",
|
||||
"webpack-dev-server": "^4.15.1",
|
||||
"webpack-retry-chunk-load-plugin": "3.1.1",
|
||||
"xstate": "^4.31.0"
|
||||
|
||||
24
frontend/public/locales/en-GB/messagingQueues.json
Normal file
24
frontend/public/locales/en-GB/messagingQueues.json
Normal 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."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
24
frontend/public/locales/en/messagingQueues.json
Normal file
24
frontend/public/locales/en/messagingQueues.json
Normal 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."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,10 @@
|
||||
"dropRate": {
|
||||
"title": "Drop Rate view",
|
||||
"description": "Connect and Monitor Your Data Streams"
|
||||
},
|
||||
"metricPage": {
|
||||
"title": "Metric View",
|
||||
"description": "Connect and Monitor Your Data Streams"
|
||||
}
|
||||
},
|
||||
"confirmModal": {
|
||||
|
||||
@@ -9,7 +9,7 @@ import ROUTES from 'constants/routes';
|
||||
import useLicense from 'hooks/useLicense';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import history from 'lib/history';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { isEmpty, isNull } from 'lodash-es';
|
||||
import { ReactChild, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery } from 'react-query';
|
||||
@@ -35,13 +35,18 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
const location = useLocation();
|
||||
const { pathname } = location;
|
||||
|
||||
const { org, orgPreferences } = useSelector<AppState, AppReducer>(
|
||||
(state) => state.app,
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
|
||||
const [isOnboardingComplete, setIsOnboardingComplete] = useState<
|
||||
boolean | null
|
||||
>(null);
|
||||
const {
|
||||
org,
|
||||
orgPreferences,
|
||||
user,
|
||||
role,
|
||||
isUserFetching,
|
||||
isUserFetchingError,
|
||||
isLoggedIn: isLoggedInState,
|
||||
isFetchingOrgPreferences,
|
||||
} = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
|
||||
const mapRoutes = useMemo(
|
||||
() =>
|
||||
@@ -56,30 +61,19 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
[pathname],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (orgPreferences && !isEmpty(orgPreferences)) {
|
||||
const onboardingPreference = orgPreferences?.find(
|
||||
const isOnboardingComplete = useMemo(
|
||||
() =>
|
||||
orgPreferences?.find(
|
||||
(preference: Record<string, any>) => preference.key === 'ORG_ONBOARDING',
|
||||
);
|
||||
|
||||
if (onboardingPreference) {
|
||||
setIsOnboardingComplete(onboardingPreference.value);
|
||||
}
|
||||
}
|
||||
}, [orgPreferences]);
|
||||
)?.value,
|
||||
[orgPreferences],
|
||||
);
|
||||
|
||||
const {
|
||||
data: licensesData,
|
||||
isFetching: isFetchingLicensesData,
|
||||
} = useLicense();
|
||||
|
||||
const {
|
||||
isUserFetching,
|
||||
isUserFetchingError,
|
||||
isLoggedIn: isLoggedInState,
|
||||
isFetchingOrgPreferences,
|
||||
} = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
|
||||
const { t } = useTranslation(['common']);
|
||||
|
||||
const localStorageUserAuthToken = getInitialUserTokenRefreshToken();
|
||||
@@ -135,7 +129,8 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
// Check if the onboarding should be shown based on the org users and onboarding completion status, wait for org users and preferences to load
|
||||
const shouldShowOnboarding = (): boolean => {
|
||||
// Only run this effect if the org users and preferences are loaded
|
||||
if (!isLoadingOrgUsers) {
|
||||
|
||||
if (!isLoadingOrgUsers && !isFetchingOrgPreferences) {
|
||||
const isFirstUser = checkFirstTimeUser();
|
||||
|
||||
// Redirect to get started if it's not the first user or if the onboarding is complete
|
||||
@@ -145,6 +140,26 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
return false;
|
||||
};
|
||||
|
||||
const handleRedirectForOrgOnboarding = (key: string): void => {
|
||||
if (
|
||||
isLoggedInState &&
|
||||
!isFetchingOrgPreferences &&
|
||||
!isLoadingOrgUsers &&
|
||||
!isEmpty(orgUsers?.payload) &&
|
||||
!isNull(orgPreferences)
|
||||
) {
|
||||
if (key === 'ONBOARDING' && isOnboardingComplete) {
|
||||
history.push(ROUTES.APPLICATION);
|
||||
}
|
||||
|
||||
const isFirstTimeUser = checkFirstTimeUser();
|
||||
|
||||
if (isFirstTimeUser && !isOnboardingComplete) {
|
||||
history.push(ROUTES.ONBOARDING);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleUserLoginIfTokenPresent = async (
|
||||
key: keyof typeof ROUTES,
|
||||
): Promise<void> => {
|
||||
@@ -166,15 +181,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
response.payload.refreshJwt,
|
||||
);
|
||||
|
||||
const showOnboarding = shouldShowOnboarding();
|
||||
|
||||
if (
|
||||
userResponse &&
|
||||
showOnboarding &&
|
||||
userResponse.payload.role === 'ADMIN'
|
||||
) {
|
||||
history.push(ROUTES.ONBOARDING);
|
||||
}
|
||||
handleRedirectForOrgOnboarding(key);
|
||||
|
||||
if (
|
||||
userResponse &&
|
||||
@@ -203,7 +210,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
) {
|
||||
handleUserLoginIfTokenPresent(key);
|
||||
} else {
|
||||
// user does have localstorage values
|
||||
handleRedirectForOrgOnboarding(key);
|
||||
|
||||
navigateToLoginIfNotLoggedIn(isLocalStorageLoggedIn);
|
||||
}
|
||||
@@ -241,9 +248,9 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
}, [org]);
|
||||
|
||||
const handleRouting = (): void => {
|
||||
const showOnboarding = shouldShowOnboarding();
|
||||
const showOrgOnboarding = shouldShowOnboarding();
|
||||
|
||||
if (showOnboarding) {
|
||||
if (showOrgOnboarding && !isOnboardingComplete) {
|
||||
history.push(ROUTES.ONBOARDING);
|
||||
} else {
|
||||
history.push(ROUTES.APPLICATION);
|
||||
@@ -251,17 +258,27 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Only run this effect if the org users and preferences are loaded
|
||||
if (!isLoadingOrgUsers && isOnboardingComplete !== null) {
|
||||
const isFirstUser = checkFirstTimeUser();
|
||||
const { isPrivate } = currentRoute || {
|
||||
isPrivate: false,
|
||||
};
|
||||
|
||||
// Redirect to get started if it's not the first user or if the onboarding is complete
|
||||
if (isFirstUser && !isOnboardingComplete) {
|
||||
history.push(ROUTES.ONBOARDING);
|
||||
}
|
||||
if (isLoggedInState && role && role !== 'ADMIN') {
|
||||
setIsLoading(false);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isLoadingOrgUsers, isOnboardingComplete, orgUsers]);
|
||||
|
||||
if (!isPrivate) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
if (
|
||||
!isEmpty(user) &&
|
||||
!isFetchingOrgPreferences &&
|
||||
!isEmpty(orgUsers?.payload) &&
|
||||
!isNull(orgPreferences)
|
||||
) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [currentRoute, user, role, orgUsers, orgPreferences]);
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
useEffect(() => {
|
||||
@@ -284,7 +301,6 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
handlePrivateRoutes(key);
|
||||
} else {
|
||||
// no need to fetch the user and make user fetching false
|
||||
|
||||
if (getLocalStorageApi(LOCALSTORAGE.IS_LOGGED_IN) === 'true') {
|
||||
handleRouting();
|
||||
}
|
||||
@@ -311,13 +327,20 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
history.push(ROUTES.SOMETHING_WENT_WRONG);
|
||||
}
|
||||
})();
|
||||
}, [dispatch, isLoggedInState, currentRoute, licensesData]);
|
||||
}, [
|
||||
dispatch,
|
||||
isLoggedInState,
|
||||
currentRoute,
|
||||
licensesData,
|
||||
orgUsers,
|
||||
orgPreferences,
|
||||
]);
|
||||
|
||||
if (isUserFetchingError) {
|
||||
return <Redirect to={ROUTES.SOMETHING_WENT_WRONG} />;
|
||||
}
|
||||
|
||||
if (isUserFetching || (isLoggedInState && isFetchingOrgPreferences)) {
|
||||
if (isUserFetching || isLoading) {
|
||||
return <Spinner tip="Loading..." />;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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: {},
|
||||
|
||||
@@ -37,8 +37,8 @@ export enum QueryParams {
|
||||
partition = 'partition',
|
||||
selectedTimelineQuery = 'selectedTimelineQuery',
|
||||
ruleType = 'ruleType',
|
||||
configDetail = 'configDetail',
|
||||
getStartedSource = 'getStartedSource',
|
||||
getStartedSourceService = 'getStartedSourceService',
|
||||
configDetail = 'configDetail',
|
||||
mqServiceView = 'mqServiceView',
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
@@ -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> = {
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -386,32 +386,31 @@ function RuleOptions({
|
||||
renderThresholdRuleOpts()}
|
||||
|
||||
<Space direction="vertical" size="large">
|
||||
{queryCategory !== EQueryType.PROM &&
|
||||
ruleType !== AlertDetectionTypes.ANOMALY_DETECTION_ALERT && (
|
||||
<Space direction="horizontal" align="center">
|
||||
<Form.Item noStyle name={['condition', 'target']}>
|
||||
<InputNumber
|
||||
addonBefore={t('field_threshold')}
|
||||
value={alertDef?.condition?.target}
|
||||
onChange={onChange}
|
||||
type="number"
|
||||
onWheel={(e): void => e.currentTarget.blur()}
|
||||
/>
|
||||
</Form.Item>
|
||||
{ruleType !== AlertDetectionTypes.ANOMALY_DETECTION_ALERT && (
|
||||
<Space direction="horizontal" align="center">
|
||||
<Form.Item noStyle name={['condition', 'target']}>
|
||||
<InputNumber
|
||||
addonBefore={t('field_threshold')}
|
||||
value={alertDef?.condition?.target}
|
||||
onChange={onChange}
|
||||
type="number"
|
||||
onWheel={(e): void => e.currentTarget.blur()}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item noStyle>
|
||||
<Select
|
||||
getPopupContainer={popupContainer}
|
||||
allowClear
|
||||
showSearch
|
||||
options={categorySelectOptions}
|
||||
placeholder={t('field_unit')}
|
||||
value={alertDef.condition.targetUnit}
|
||||
onChange={onChangeAlertUnit}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Space>
|
||||
)}
|
||||
<Form.Item noStyle>
|
||||
<Select
|
||||
getPopupContainer={popupContainer}
|
||||
allowClear
|
||||
showSearch
|
||||
options={categorySelectOptions}
|
||||
placeholder={t('field_unit')}
|
||||
value={alertDef.condition.targetUnit}
|
||||
onChange={onChangeAlertUnit}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Space>
|
||||
)}
|
||||
|
||||
<Collapse>
|
||||
<Collapse.Panel header={t('More options')} key="1">
|
||||
|
||||
@@ -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';
|
||||
@@ -73,6 +74,19 @@ export enum AlertDetectionTypes {
|
||||
ANOMALY_DETECTION_ALERT = 'anomaly_rule',
|
||||
}
|
||||
|
||||
const ALERT_SETUP_GUIDE_URLS: Record<AlertTypes, string> = {
|
||||
[AlertTypes.METRICS_BASED_ALERT]:
|
||||
'https://signoz.io/docs/alerts-management/metrics-based-alerts/?utm_source=product&utm_medium=alert-creation-page',
|
||||
[AlertTypes.LOGS_BASED_ALERT]:
|
||||
'https://signoz.io/docs/alerts-management/log-based-alerts/?utm_source=product&utm_medium=alert-creation-page',
|
||||
[AlertTypes.TRACES_BASED_ALERT]:
|
||||
'https://signoz.io/docs/alerts-management/trace-based-alerts/?utm_source=product&utm_medium=alert-creation-page',
|
||||
[AlertTypes.EXCEPTIONS_BASED_ALERT]:
|
||||
'https://signoz.io/docs/alerts-management/exceptions-based-alerts/?utm_source=product&utm_medium=alert-creation-page',
|
||||
[AlertTypes.ANOMALY_BASED_ALERT]:
|
||||
'https://signoz.io/docs/alerts-management/anomaly-based-alerts/?utm_source=product&utm_medium=alert-creation-page',
|
||||
};
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
function FormAlertRules({
|
||||
alertType,
|
||||
@@ -92,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;
|
||||
|
||||
@@ -101,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();
|
||||
@@ -702,6 +720,29 @@ function FormAlertRules({
|
||||
|
||||
const isRuleCreated = !ruleId || ruleId === 0;
|
||||
|
||||
function handleRedirection(option: AlertTypes): void {
|
||||
let url;
|
||||
if (
|
||||
option === AlertTypes.METRICS_BASED_ALERT &&
|
||||
alertTypeFromURL === AlertDetectionTypes.ANOMALY_DETECTION_ALERT
|
||||
) {
|
||||
url = ALERT_SETUP_GUIDE_URLS[AlertTypes.ANOMALY_BASED_ALERT];
|
||||
} else {
|
||||
url = ALERT_SETUP_GUIDE_URLS[option];
|
||||
}
|
||||
|
||||
if (url) {
|
||||
logEvent('Alert: Check example alert clicked', {
|
||||
dataSource: ALERTS_DATA_SOURCE_MAP[alertDef?.alertType as AlertTypes],
|
||||
isNewRule: !ruleId || ruleId === 0,
|
||||
ruleId,
|
||||
queryType: currentQuery.queryType,
|
||||
link: url,
|
||||
});
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!isRuleCreated) {
|
||||
logEvent('Alert: Edit page visited', {
|
||||
@@ -752,7 +793,11 @@ function FormAlertRules({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button className="periscope-btn" icon={<ExternalLink size={14} />}>
|
||||
<Button
|
||||
className="periscope-btn"
|
||||
onClick={(): void => handleRedirection(alertDef.alertType as AlertTypes)}
|
||||
icon={<ExternalLink size={14} />}
|
||||
>
|
||||
Alert Setup Guide
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -53,6 +53,7 @@ export enum KeyOperationTableHeader {
|
||||
export enum MetricsType {
|
||||
Tag = 'tag',
|
||||
Resource = 'resource',
|
||||
Scope = 'scope',
|
||||
}
|
||||
|
||||
export enum WidgetKeys {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
|
||||
Once you are done intrumenting your Java application, you can run it using the below commands
|
||||
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.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
|
||||
Once you are done intrumenting your Java application, you can run it using the below commands
|
||||
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.
|
||||
|
||||
@@ -312,7 +312,7 @@ export default function Onboarding(): JSX.Element {
|
||||
<div
|
||||
onClick={(): void => {
|
||||
logEvent('Onboarding V2: Skip Button Clicked', {});
|
||||
history.push('/');
|
||||
history.push(ROUTES.APPLICATION);
|
||||
}}
|
||||
className="skip-to-console"
|
||||
>
|
||||
|
||||
@@ -93,6 +93,7 @@ export default function ConnectionStatus(): JSX.Element {
|
||||
] = 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);
|
||||
|
||||
@@ -82,7 +82,7 @@ export function AboutSigNozQuestions({
|
||||
otherInterestInSignoz,
|
||||
});
|
||||
|
||||
logEvent('User Onboarding: About SigNoz Questions Answered', {
|
||||
logEvent('Org Onboarding: Answered', {
|
||||
hearAboutSignoz,
|
||||
otherAboutSignoz,
|
||||
interestInSignoz,
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
CheckCircle,
|
||||
Loader2,
|
||||
Plus,
|
||||
TriangleAlert,
|
||||
X,
|
||||
@@ -33,6 +34,7 @@ interface TeamMember {
|
||||
}
|
||||
|
||||
interface InviteTeamMembersProps {
|
||||
isLoading: boolean;
|
||||
teamMembers: TeamMember[] | null;
|
||||
setTeamMembers: (teamMembers: TeamMember[]) => void;
|
||||
onNext: () => void;
|
||||
@@ -40,6 +42,7 @@ interface InviteTeamMembersProps {
|
||||
}
|
||||
|
||||
function InviteTeamMembers({
|
||||
isLoading,
|
||||
teamMembers,
|
||||
setTeamMembers,
|
||||
onNext,
|
||||
@@ -67,8 +70,6 @@ function InviteTeamMembers({
|
||||
|
||||
const [disableNextButton, setDisableNextButton] = useState<boolean>(false);
|
||||
|
||||
const [allInvitesSent, setAllInvitesSent] = useState<boolean>(false);
|
||||
|
||||
const defaultTeamMember: TeamMember = {
|
||||
email: '',
|
||||
role: 'EDITOR',
|
||||
@@ -157,10 +158,16 @@ function InviteTeamMembers({
|
||||
setError(null);
|
||||
setHasErrors(false);
|
||||
setInviteUsersErrorResponse(null);
|
||||
setAllInvitesSent(true);
|
||||
|
||||
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();
|
||||
@@ -358,33 +365,36 @@ function InviteTeamMembers({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{inviteUsersSuccessResponse && (
|
||||
<div className="success-message-container invite-users-success-message-container">
|
||||
{inviteUsersSuccessResponse?.map((success, index) => (
|
||||
<Typography.Text
|
||||
className="success-message"
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={`${success}-${index}`}
|
||||
>
|
||||
<CheckCircle size={14} /> {success}
|
||||
</Typography.Text>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasErrors && (
|
||||
<div className="error-message-container invite-users-error-message-container">
|
||||
{inviteUsersErrorResponse?.map((error, index) => (
|
||||
<Typography.Text
|
||||
className="error-message"
|
||||
type="danger"
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={`${error}-${index}`}
|
||||
>
|
||||
<TriangleAlert size={14} /> {error}
|
||||
</Typography.Text>
|
||||
))}
|
||||
</div>
|
||||
<>
|
||||
{/* show only when invites are sent successfully & partial error is present */}
|
||||
{inviteUsersSuccessResponse && inviteUsersErrorResponse && (
|
||||
<div className="success-message-container invite-users-success-message-container">
|
||||
{inviteUsersSuccessResponse?.map((success, index) => (
|
||||
<Typography.Text
|
||||
className="success-message"
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={`${success}-${index}`}
|
||||
>
|
||||
<CheckCircle size={14} /> {success}
|
||||
</Typography.Text>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="error-message-container invite-users-error-message-container">
|
||||
{inviteUsersErrorResponse?.map((error, index) => (
|
||||
<Typography.Text
|
||||
className="error-message"
|
||||
type="danger"
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={`${error}-${index}`}
|
||||
>
|
||||
<TriangleAlert size={14} /> {error}
|
||||
</Typography.Text>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -413,17 +423,23 @@ function InviteTeamMembers({
|
||||
type="primary"
|
||||
className="next-button"
|
||||
onClick={handleNext}
|
||||
loading={isSendingInvites || disableNextButton}
|
||||
loading={isSendingInvites || isLoading || disableNextButton}
|
||||
>
|
||||
{allInvitesSent ? 'Invites Sent' : 'Send Invites'}
|
||||
|
||||
{allInvitesSent ? <CheckCircle size={14} /> : <ArrowRight size={14} />}
|
||||
Send Invites
|
||||
<ArrowRight size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="do-later-container">
|
||||
<Button type="link" onClick={handleDoLater}>
|
||||
I'll do this later
|
||||
<Button
|
||||
type="link"
|
||||
className="do-later-button"
|
||||
onClick={handleDoLater}
|
||||
disabled={isSendingInvites || disableNextButton}
|
||||
>
|
||||
{isLoading && <Loader2 className="animate-spin" size={16} />}
|
||||
|
||||
<span>I'll do this later</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -189,6 +189,15 @@
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 24px;
|
||||
|
||||
.do-later-button {
|
||||
font-size: 12px;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.question {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -314,7 +312,7 @@ function OptimiseSignozNeeds({
|
||||
|
||||
<div className="do-later-container">
|
||||
<Button type="link" onClick={handleWillDoLater}>
|
||||
Skip for now
|
||||
I'll do this later
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,31 +1,25 @@
|
||||
import './OnboardingQuestionaire.styles.scss';
|
||||
|
||||
import { Skeleton } from 'antd';
|
||||
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 getOrgPreference from 'api/preferences/getOrgPreference';
|
||||
import updateOrgPreferenceAPI from 'api/preferences/updateOrgPreference';
|
||||
import getOrgUser from 'api/user/getOrgUser';
|
||||
import { AxiosError } from 'axios';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { InviteTeamMembersProps } from 'container/OrganizationSettings/PendingInvitesContainer';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import history from 'lib/history';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { Dispatch, useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import {
|
||||
UPDATE_IS_FETCHING_ORG_PREFERENCES,
|
||||
UPDATE_ORG_PREFERENCES,
|
||||
} from 'types/actions/app';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
|
||||
import {
|
||||
AboutSigNozQuestions,
|
||||
@@ -68,17 +62,20 @@ 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, role, isLoggedIn: isLoggedInState } = useSelector<
|
||||
AppState,
|
||||
AppReducer
|
||||
>((state) => state.app);
|
||||
const { org } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
const dispatch = useDispatch();
|
||||
const [currentStep, setCurrentStep] = useState<number>(1);
|
||||
const [orgDetails, setOrgDetails] = useState<OrgDetails>(INITIAL_ORG_DETAILS);
|
||||
const [signozDetails, setSignozDetails] = useState<SignozDetails>(
|
||||
INITIAL_SIGNOZ_DETAILS,
|
||||
);
|
||||
|
||||
const [
|
||||
optimiseSignozDetails,
|
||||
setOptimiseSignozDetails,
|
||||
@@ -87,113 +84,12 @@ function OnboardingQuestionaire(): JSX.Element {
|
||||
InviteTeamMembersProps[] | null
|
||||
>(null);
|
||||
|
||||
const { data: orgUsers, isLoading: isLoadingOrgUsers } = useQuery({
|
||||
queryFn: () =>
|
||||
getOrgUser({
|
||||
orgId: (org || [])[0].id,
|
||||
}),
|
||||
queryKey: ['getOrgUser', org?.[0].id],
|
||||
});
|
||||
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
const [currentOrgData, setCurrentOrgData] = useState<OrgData | null>(null);
|
||||
const [isOnboardingComplete, setIsOnboardingComplete] = useState<boolean>(
|
||||
false,
|
||||
);
|
||||
|
||||
const {
|
||||
data: onboardingPreferenceData,
|
||||
isLoading: isLoadingOnboardingPreference,
|
||||
} = useQuery({
|
||||
queryFn: () => getOrgPreference({ preferenceID: 'ORG_ONBOARDING' }),
|
||||
queryKey: ['getOrgPreferences', 'ORG_ONBOARDING'],
|
||||
enabled: role === USER_ROLES.ADMIN,
|
||||
});
|
||||
|
||||
const { data: orgPreferences, isLoading: isLoadingOrgPreferences } = useQuery({
|
||||
queryFn: () => getAllOrgPreferences(),
|
||||
queryKey: ['getOrgPreferences'],
|
||||
enabled: isOnboardingComplete && role === USER_ROLES.ADMIN,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (orgPreferences && !isLoadingOrgPreferences) {
|
||||
dispatch({
|
||||
type: UPDATE_IS_FETCHING_ORG_PREFERENCES,
|
||||
payload: {
|
||||
isFetchingOrgPreferences: false,
|
||||
},
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: UPDATE_ORG_PREFERENCES,
|
||||
payload: {
|
||||
orgPreferences: orgPreferences.payload?.data || null,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [orgPreferences, dispatch, isLoadingOrgPreferences]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoggedInState && role !== USER_ROLES.ADMIN) {
|
||||
dispatch({
|
||||
type: UPDATE_IS_FETCHING_ORG_PREFERENCES,
|
||||
payload: {
|
||||
isFetchingOrgPreferences: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [isLoggedInState, role, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!isLoadingOnboardingPreference &&
|
||||
!isEmpty(onboardingPreferenceData?.payload?.data)
|
||||
) {
|
||||
const preferenceId = onboardingPreferenceData?.payload?.data?.preference_id;
|
||||
const preferenceValue =
|
||||
onboardingPreferenceData?.payload?.data?.preference_value;
|
||||
|
||||
if (preferenceId === 'ORG_ONBOARDING') {
|
||||
setIsOnboardingComplete(preferenceValue as boolean);
|
||||
}
|
||||
}
|
||||
}, [onboardingPreferenceData, isLoadingOnboardingPreference]);
|
||||
|
||||
const checkFirstTimeUser = (): boolean => {
|
||||
const users = orgUsers?.payload || [];
|
||||
|
||||
const remainingUsers = users.filter(
|
||||
(user) => user.email !== 'admin@signoz.cloud',
|
||||
);
|
||||
|
||||
return remainingUsers.length === 1;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Only run this effect if the org users and preferences are loaded
|
||||
if (!isLoadingOrgUsers && !isLoadingOnboardingPreference) {
|
||||
const isFirstUser = checkFirstTimeUser();
|
||||
|
||||
// Redirect to get started if it's not the first user or if the onboarding is complete
|
||||
if (!isFirstUser || isOnboardingComplete) {
|
||||
history.push(ROUTES.GET_STARTED);
|
||||
|
||||
logEvent('User Onboarding: Redirected to Get Started', {
|
||||
isFirstUser,
|
||||
isOnboardingComplete,
|
||||
});
|
||||
} else {
|
||||
logEvent('User Onboarding: Started', {});
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
isLoadingOrgUsers,
|
||||
isLoadingOnboardingPreference,
|
||||
isOnboardingComplete,
|
||||
orgUsers,
|
||||
]);
|
||||
const [
|
||||
updatingOrgOnboardingStatus,
|
||||
setUpdatingOrgOnboardingStatus,
|
||||
] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (org) {
|
||||
@@ -207,6 +103,44 @@ 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'],
|
||||
enabled: false,
|
||||
refetchOnWindowFocus: false,
|
||||
onSuccess: (response) => {
|
||||
dispatch({
|
||||
type: UPDATE_IS_FETCHING_ORG_PREFERENCES,
|
||||
payload: {
|
||||
isFetchingOrgPreferences: false,
|
||||
},
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: UPDATE_ORG_PREFERENCES,
|
||||
payload: {
|
||||
orgPreferences: response.payload?.data || null,
|
||||
},
|
||||
});
|
||||
|
||||
setUpdatingOrgOnboardingStatus(false);
|
||||
|
||||
logEvent('Org Onboarding: Redirecting to Get Started', {});
|
||||
|
||||
history.push(ROUTES.GET_STARTED);
|
||||
},
|
||||
onError: () => {
|
||||
setUpdatingOrgOnboardingStatus(false);
|
||||
},
|
||||
});
|
||||
|
||||
const isNextDisabled =
|
||||
optimiseSignozDetails.logsPerDay === 0 &&
|
||||
optimiseSignozDetails.hostsPerDay === 0 &&
|
||||
@@ -226,14 +160,21 @@ function OnboardingQuestionaire(): JSX.Element {
|
||||
|
||||
const { mutate: updateOrgPreference } = useMutation(updateOrgPreferenceAPI, {
|
||||
onSuccess: () => {
|
||||
setIsOnboardingComplete(true);
|
||||
refetchOrgPreferences();
|
||||
},
|
||||
onError: (error) => {
|
||||
showErrorNotification(notifications, error as AxiosError);
|
||||
|
||||
setUpdatingOrgOnboardingStatus(false);
|
||||
},
|
||||
});
|
||||
|
||||
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,
|
||||
@@ -258,6 +199,11 @@ function OnboardingQuestionaire(): JSX.Element {
|
||||
};
|
||||
|
||||
const handleOnboardingComplete = (): void => {
|
||||
logEvent(ONBOARDING_COMPLETE_EVENT_NAME, {
|
||||
currentPageID: 4,
|
||||
});
|
||||
|
||||
setUpdatingOrgOnboardingStatus(true);
|
||||
updateOrgPreference({
|
||||
preferenceID: 'ORG_ONBOARDING',
|
||||
value: true,
|
||||
@@ -271,55 +217,75 @@ function OnboardingQuestionaire(): JSX.Element {
|
||||
</div>
|
||||
|
||||
<div className="onboarding-questionaire-content">
|
||||
{(isLoadingOnboardingPreference || isLoadingOrgUsers) && (
|
||||
<div className="onboarding-questionaire-loading-container">
|
||||
<Skeleton />
|
||||
</div>
|
||||
{currentStep === 1 && (
|
||||
<OrgQuestions
|
||||
currentOrgData={currentOrgData}
|
||||
orgDetails={orgDetails}
|
||||
onNext={(orgDetails: OrgDetails): void => {
|
||||
logEvent(NEXT_BUTTON_EVENT_NAME, {
|
||||
currentPageID: 1,
|
||||
nextPageID: 2,
|
||||
});
|
||||
|
||||
setOrgDetails(orgDetails);
|
||||
setCurrentStep(2);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isLoadingOnboardingPreference && !isLoadingOrgUsers && (
|
||||
<>
|
||||
{currentStep === 1 && (
|
||||
<OrgQuestions
|
||||
currentOrgData={currentOrgData}
|
||||
orgDetails={orgDetails}
|
||||
onNext={(orgDetails: OrgDetails): void => {
|
||||
setOrgDetails(orgDetails);
|
||||
setCurrentStep(2);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{currentStep === 2 && (
|
||||
<AboutSigNozQuestions
|
||||
signozDetails={signozDetails}
|
||||
setSignozDetails={setSignozDetails}
|
||||
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);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentStep === 2 && (
|
||||
<AboutSigNozQuestions
|
||||
signozDetails={signozDetails}
|
||||
setSignozDetails={setSignozDetails}
|
||||
onBack={(): void => setCurrentStep(1)}
|
||||
onNext={(): void => setCurrentStep(3)}
|
||||
/>
|
||||
)}
|
||||
{currentStep === 3 && (
|
||||
<OptimiseSignozNeeds
|
||||
isNextDisabled={isNextDisabled}
|
||||
isUpdatingProfile={isUpdatingProfile}
|
||||
optimiseSignozDetails={optimiseSignozDetails}
|
||||
setOptimiseSignozDetails={setOptimiseSignozDetails}
|
||||
onBack={(): void => {
|
||||
logEvent(BACK_BUTTON_EVENT_NAME, {
|
||||
currentPageID: 3,
|
||||
prevPageID: 2,
|
||||
});
|
||||
setCurrentStep(2);
|
||||
}}
|
||||
onNext={handleUpdateProfile}
|
||||
onWillDoLater={(): void => setCurrentStep(4)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentStep === 3 && (
|
||||
<OptimiseSignozNeeds
|
||||
isNextDisabled={isNextDisabled}
|
||||
isUpdatingProfile={isUpdatingProfile}
|
||||
optimiseSignozDetails={optimiseSignozDetails}
|
||||
setOptimiseSignozDetails={setOptimiseSignozDetails}
|
||||
onBack={(): void => 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
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentStep === 4 && (
|
||||
<InviteTeamMembers
|
||||
teamMembers={teamMembers}
|
||||
setTeamMembers={setTeamMembers}
|
||||
onBack={(): void => setCurrentStep(3)}
|
||||
onNext={handleOnboardingComplete}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
{currentStep === 4 && (
|
||||
<InviteTeamMembers
|
||||
isLoading={updatingOrgOnboardingStatus}
|
||||
teamMembers={teamMembers}
|
||||
setTeamMembers={setTeamMembers}
|
||||
onBack={(): void => {
|
||||
logEvent(BACK_BUTTON_EVENT_NAME, {
|
||||
currentPageID: 4,
|
||||
prevPageID: 3,
|
||||
});
|
||||
setCurrentStep(3);
|
||||
}}
|
||||
onNext={handleOnboardingComplete}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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}`,
|
||||
}),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -410,6 +410,7 @@ function QueryBuilderSearch({
|
||||
label={option.label}
|
||||
value={option.value}
|
||||
dataType={option.dataType || ''}
|
||||
type={option.type || ''}
|
||||
/>
|
||||
{option.selected && <StyledCheckOutlined />}
|
||||
</Select.Option>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -16,4 +16,5 @@ export type Option = {
|
||||
selected?: boolean;
|
||||
dataType?: string;
|
||||
isIndexed?: boolean;
|
||||
type?: string;
|
||||
};
|
||||
|
||||
@@ -46,6 +46,7 @@ export const useOptions = (
|
||||
value: item.key,
|
||||
dataType: item.dataType,
|
||||
isIndexed: item?.isIndexed,
|
||||
type: item?.type || '',
|
||||
})),
|
||||
[getLabel],
|
||||
);
|
||||
|
||||
@@ -163,7 +163,8 @@ export const getUPlotChartOptions = ({
|
||||
|
||||
const stackBarChart = stackChart && isUndefined(hiddenGraph);
|
||||
|
||||
const isAnomalyRule = apiResponse?.data?.newResult?.data?.result[0].isAnomaly;
|
||||
const isAnomalyRule =
|
||||
apiResponse?.data?.newResult?.data?.result[0]?.isAnomaly || false;
|
||||
|
||||
const series = getStackedSeries(apiResponse?.data?.result || []);
|
||||
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import '../MessagingQueues.styles.scss';
|
||||
|
||||
import { Select, Typography } from 'antd';
|
||||
@@ -12,16 +13,22 @@ import { useHistory } from 'react-router-dom';
|
||||
|
||||
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<string>(
|
||||
const [
|
||||
selectedView,
|
||||
setSelectedView,
|
||||
] = useState<MessagingQueuesViewTypeOptions>(
|
||||
MessagingQueuesViewType.consumerLag.value,
|
||||
);
|
||||
|
||||
@@ -30,7 +37,9 @@ function MQDetailPage(): JSX.Element {
|
||||
setproducerLatencyOption,
|
||||
] = useState<ProducerLatencyOptions>(ProducerLatencyOptions.Producers);
|
||||
|
||||
const mqServiceView = useUrlQuery().get(QueryParams.mqServiceView);
|
||||
const mqServiceView = useUrlQuery().get(
|
||||
QueryParams.mqServiceView,
|
||||
) as MessagingQueuesViewTypeOptions;
|
||||
|
||||
useEffect(() => {
|
||||
logEvent('Messaging Queues: Detail page visited', {});
|
||||
@@ -52,6 +61,10 @@ function MQDetailPage(): JSX.Element {
|
||||
});
|
||||
};
|
||||
|
||||
const showMessagingQueueDetails =
|
||||
selectedView !== MessagingQueuesViewType.dropRate.value &&
|
||||
selectedView !== MessagingQueuesViewType.metricPage.value;
|
||||
|
||||
return (
|
||||
<div className="messaging-queue-container">
|
||||
<div className="messaging-breadcrumb">
|
||||
@@ -74,7 +87,7 @@ function MQDetailPage(): JSX.Element {
|
||||
setSelectedView(value);
|
||||
updateUrlQuery({ [QueryParams.mqServiceView]: value });
|
||||
}}
|
||||
value={mqServiceView}
|
||||
value={selectedView}
|
||||
options={[
|
||||
{
|
||||
label: MessagingQueuesViewType.consumerLag.label,
|
||||
@@ -92,6 +105,10 @@ function MQDetailPage(): JSX.Element {
|
||||
label: MessagingQueuesViewType.dropRate.label,
|
||||
value: MessagingQueuesViewType.dropRate.value,
|
||||
},
|
||||
{
|
||||
label: MessagingQueuesViewType.metricPage.label,
|
||||
value: MessagingQueuesViewType.metricPage.value,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
@@ -102,6 +119,10 @@ function MQDetailPage(): JSX.Element {
|
||||
<MessagingQueuesConfigOptions />
|
||||
<MessagingQueuesGraph />
|
||||
</div>
|
||||
) : selectedView === MessagingQueuesViewType.dropRate.value ? (
|
||||
<DropRateView />
|
||||
) : selectedView === MessagingQueuesViewType.metricPage.value ? (
|
||||
<MetricPage />
|
||||
) : (
|
||||
<MessagingQueueOverview
|
||||
selectedView={selectedView}
|
||||
@@ -109,7 +130,7 @@ function MQDetailPage(): JSX.Element {
|
||||
setOption={setproducerLatencyOption}
|
||||
/>
|
||||
)}
|
||||
{selectedView !== MessagingQueuesViewType.dropRate.value && (
|
||||
{showMessagingQueueDetails && (
|
||||
<div className="messaging-queue-details">
|
||||
<MessagingQueuesDetails
|
||||
selectedView={selectedView}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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]} — {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;
|
||||
@@ -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;
|
||||
@@ -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[];
|
||||
}[];
|
||||
};
|
||||
}
|
||||
@@ -17,6 +17,11 @@
|
||||
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;
|
||||
@@ -43,3 +48,133 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,36 +7,26 @@ import { isEmpty } from 'lodash-es';
|
||||
import { Dispatch, SetStateAction, useEffect, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import {
|
||||
ConsumerLagDetailTitle,
|
||||
getMetaDataAndAPIPerView,
|
||||
MessagingQueueServiceDetailType,
|
||||
MessagingQueuesViewType,
|
||||
MessagingQueuesViewTypeOptions,
|
||||
ProducerLatencyOptions,
|
||||
SelectedTimelineQuery,
|
||||
} from '../MessagingQueuesUtils';
|
||||
import { ComingSoon } from '../MQCommon/MQCommon';
|
||||
import {
|
||||
getConsumerLagDetails,
|
||||
MessagingQueueServicePayload,
|
||||
MessagingQueuesPayloadProps,
|
||||
} from './MQTables/getConsumerLagDetails';
|
||||
import { getPartitionLatencyDetails } from './MQTables/getPartitionLatencyDetails';
|
||||
import { getTopicThroughputDetails } from './MQTables/getTopicThroughputDetails';
|
||||
import MessagingQueuesTable from './MQTables/MQTables';
|
||||
|
||||
const MQServiceDetailTypePerView = (
|
||||
producerLatencyOption: ProducerLatencyOptions,
|
||||
): {
|
||||
[x: string]: MessagingQueueServiceDetailType[];
|
||||
} => ({
|
||||
): Record<string, MessagingQueueServiceDetailType[]> => ({
|
||||
[MessagingQueuesViewType.consumerLag.value]: [
|
||||
MessagingQueueServiceDetailType.ConsumerDetails,
|
||||
MessagingQueueServiceDetailType.ProducerDetails,
|
||||
MessagingQueueServiceDetailType.NetworkLatency,
|
||||
MessagingQueueServiceDetailType.PartitionHostMetrics,
|
||||
],
|
||||
[MessagingQueuesViewType.partitionLatency.value]: [
|
||||
MessagingQueueServiceDetailType.ConsumerDetails,
|
||||
@@ -52,7 +42,7 @@ const MQServiceDetailTypePerView = (
|
||||
interface MessagingQueuesOptionsProps {
|
||||
currentTab: MessagingQueueServiceDetailType;
|
||||
setCurrentTab: Dispatch<SetStateAction<MessagingQueueServiceDetailType>>;
|
||||
selectedView: string;
|
||||
selectedView: MessagingQueuesViewTypeOptions;
|
||||
producerLatencyOption: ProducerLatencyOptions;
|
||||
}
|
||||
|
||||
@@ -62,16 +52,7 @@ function MessagingQueuesOptions({
|
||||
selectedView,
|
||||
producerLatencyOption,
|
||||
}: MessagingQueuesOptionsProps): JSX.Element {
|
||||
const [option, setOption] = useState<MessagingQueueServiceDetailType>(
|
||||
currentTab,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setOption(currentTab);
|
||||
}, [currentTab]);
|
||||
|
||||
const handleChange = (value: MessagingQueueServiceDetailType): void => {
|
||||
setOption(value);
|
||||
setCurrentTab(value);
|
||||
};
|
||||
|
||||
@@ -79,22 +60,8 @@ function MessagingQueuesOptions({
|
||||
const detailTypes =
|
||||
MQServiceDetailTypePerView(producerLatencyOption)[selectedView] || [];
|
||||
return detailTypes.map((detailType) => (
|
||||
<Radio.Button
|
||||
key={detailType}
|
||||
value={detailType}
|
||||
disabled={
|
||||
detailType === MessagingQueueServiceDetailType.PartitionHostMetrics
|
||||
}
|
||||
className={
|
||||
detailType === MessagingQueueServiceDetailType.PartitionHostMetrics
|
||||
? 'disabled-option'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
<Radio.Button key={detailType} value={detailType}>
|
||||
{ConsumerLagDetailTitle[detailType]}
|
||||
{detailType === MessagingQueueServiceDetailType.PartitionHostMetrics && (
|
||||
<ComingSoon />
|
||||
)}
|
||||
</Radio.Button>
|
||||
));
|
||||
};
|
||||
@@ -102,7 +69,7 @@ function MessagingQueuesOptions({
|
||||
return (
|
||||
<Radio.Group
|
||||
onChange={(e): void => handleChange(e.target.value)}
|
||||
value={option}
|
||||
value={currentTab}
|
||||
className="mq-details-options"
|
||||
>
|
||||
{renderRadioButtons()}
|
||||
@@ -110,80 +77,9 @@ function MessagingQueuesOptions({
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
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,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const checkValidityOfDetailConfigs = (
|
||||
selectedTimelineQuery: SelectedTimelineQuery,
|
||||
selectedView: string,
|
||||
selectedView: MessagingQueuesViewTypeOptions,
|
||||
currentTab: MessagingQueueServiceDetailType,
|
||||
configDetails?: {
|
||||
[key: string]: string;
|
||||
@@ -229,7 +125,7 @@ function MessagingQueuesDetails({
|
||||
selectedView,
|
||||
producerLatencyOption,
|
||||
}: {
|
||||
selectedView: string;
|
||||
selectedView: MessagingQueuesViewTypeOptions;
|
||||
producerLatencyOption: ProducerLatencyOptions;
|
||||
}): JSX.Element {
|
||||
const [currentTab, setCurrentTab] = useState<MessagingQueueServiceDetailType>(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
/* eslint-disable react/require-default-props */
|
||||
import './MQTables.styles.scss';
|
||||
|
||||
@@ -17,6 +18,7 @@ import {
|
||||
convertToTitleCase,
|
||||
MessagingQueueServiceDetailType,
|
||||
MessagingQueuesViewType,
|
||||
MessagingQueuesViewTypeOptions,
|
||||
RowData,
|
||||
SelectedTimelineQuery,
|
||||
setConfigDetail,
|
||||
@@ -31,6 +33,8 @@ import {
|
||||
MessagingQueuesPayloadProps,
|
||||
} from './getConsumerLagDetails';
|
||||
|
||||
const INITIAL_PAGE_SIZE = 10;
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
export function getColumns(
|
||||
data: MessagingQueuesPayloadProps['payload'],
|
||||
@@ -117,7 +121,7 @@ function MessagingQueuesTable({
|
||||
type = 'Detail',
|
||||
}: {
|
||||
currentTab?: MessagingQueueServiceDetailType;
|
||||
selectedView: string;
|
||||
selectedView: MessagingQueuesViewTypeOptions;
|
||||
tableApiPayload?: MessagingQueueServicePayload;
|
||||
tableApi: (
|
||||
props: MessagingQueueServicePayload,
|
||||
@@ -153,8 +157,8 @@ function MessagingQueuesTable({
|
||||
|
||||
const paginationConfig = useMemo(
|
||||
() =>
|
||||
tableData?.length > 10 && {
|
||||
pageSize: 10,
|
||||
tableData?.length > INITIAL_PAGE_SIZE && {
|
||||
pageSize: INITIAL_PAGE_SIZE,
|
||||
showTotal: showPaginationItem,
|
||||
showSizeChanger: false,
|
||||
hideOnSinglePage: true,
|
||||
@@ -168,15 +172,18 @@ function MessagingQueuesTable({
|
||||
});
|
||||
};
|
||||
|
||||
const { mutate: getViewDetails, isLoading } = useMutation(tableApi, {
|
||||
onSuccess: (data) => {
|
||||
if (data.payload) {
|
||||
setColumns(getColumns(data?.payload, history));
|
||||
setTableData(getTableData(data?.payload));
|
||||
}
|
||||
const { mutate: getViewDetails, isLoading, error, isError } = useMutation(
|
||||
tableApi,
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
if (data.payload) {
|
||||
setColumns(getColumns(data?.payload, history));
|
||||
setTableData(getTableData(data?.payload));
|
||||
}
|
||||
},
|
||||
onError: handleConsumerDetailsOnError,
|
||||
},
|
||||
onError: handleConsumerDetailsOnError,
|
||||
});
|
||||
);
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
@@ -229,6 +236,10 @@ function MessagingQueuesTable({
|
||||
</Typography.Text>
|
||||
<Skeleton />
|
||||
</div>
|
||||
) : isError ? (
|
||||
<div className="no-data-style">
|
||||
<Typography.Text>{error?.message || SOMETHING_WENT_WRONG}</Typography.Text>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{currentTab && (
|
||||
@@ -240,7 +251,7 @@ function MessagingQueuesTable({
|
||||
<Table
|
||||
className={cx(
|
||||
'mq-table',
|
||||
type !== 'Detail' ? 'mq-overview-row-clickable' : '',
|
||||
type !== 'Detail' ? 'mq-overview-row-clickable' : 'pagination-left',
|
||||
)}
|
||||
pagination={paginationConfig}
|
||||
size="middle"
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { MessagingQueueServiceDetailType } from 'pages/MessagingQueues/MessagingQueuesUtils';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
|
||||
@@ -43,21 +40,17 @@ export const getConsumerLagDetails = async (
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,34 +1,23 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
|
||||
import {
|
||||
MessagingQueueServicePayload,
|
||||
MessagingQueuesPayloadProps,
|
||||
} from './getConsumerLagDetails';
|
||||
import { DropRateAPIResponse } from '../DropRateView/dropRateViewUtils';
|
||||
import { MessagingQueueServicePayload } from './getConsumerLagDetails';
|
||||
|
||||
export const getKafkaSpanEval = async (
|
||||
props: Omit<MessagingQueueServicePayload, 'detailType' | 'variables'>,
|
||||
): Promise<
|
||||
SuccessResponse<MessagingQueuesPayloadProps['payload']> | ErrorResponse
|
||||
> => {
|
||||
): Promise<SuccessResponse<DropRateAPIResponse['data']> | ErrorResponse> => {
|
||||
const { start, end, evalTime } = props;
|
||||
try {
|
||||
const response = await axios.post(`messaging-queues/kafka/span/evaluation`, {
|
||||
start,
|
||||
end,
|
||||
eval_time: evalTime,
|
||||
});
|
||||
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,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler((error as AxiosError) || SOMETHING_WENT_WRONG);
|
||||
}
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { MessagingQueueServiceDetailType } from 'pages/MessagingQueues/MessagingQueuesUtils';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
|
||||
@@ -22,18 +19,15 @@ export const getPartitionLatencyDetails = async (
|
||||
} else {
|
||||
endpoint = `/messaging-queues/kafka/consumer-lag/producer-details`;
|
||||
}
|
||||
try {
|
||||
const response = await axios.post(endpoint, {
|
||||
...rest,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler((error as AxiosError) || SOMETHING_WENT_WRONG);
|
||||
}
|
||||
const response = await axios.post(endpoint, {
|
||||
...rest,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
|
||||
import {
|
||||
@@ -14,21 +11,17 @@ export const getPartitionLatencyOverview = async (
|
||||
): Promise<
|
||||
SuccessResponse<MessagingQueuesPayloadProps['payload']> | ErrorResponse
|
||||
> => {
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`/messaging-queues/kafka/partition-latency/overview`,
|
||||
{
|
||||
...props,
|
||||
},
|
||||
);
|
||||
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,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler((error as AxiosError) || SOMETHING_WENT_WRONG);
|
||||
}
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
|
||||
import {
|
||||
@@ -16,18 +13,14 @@ export const getTopicThroughputDetails = async (
|
||||
> => {
|
||||
const { detailType, ...rest } = props;
|
||||
const endpoint = `/messaging-queues/kafka/topic-throughput/${detailType}`;
|
||||
try {
|
||||
const response = await axios.post(endpoint, {
|
||||
...rest,
|
||||
});
|
||||
const response = await axios.post(endpoint, {
|
||||
...rest,
|
||||
});
|
||||
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
|
||||
import {
|
||||
@@ -15,23 +12,18 @@ export const getTopicThroughputOverview = async (
|
||||
SuccessResponse<MessagingQueuesPayloadProps['payload']> | ErrorResponse
|
||||
> => {
|
||||
const { detailType, start, end } = props;
|
||||
console.log(detailType);
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`messaging-queues/kafka/topic-throughput/${detailType}`,
|
||||
{
|
||||
start,
|
||||
end,
|
||||
},
|
||||
);
|
||||
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,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler((error as AxiosError) || SOMETHING_WENT_WRONG);
|
||||
}
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import {
|
||||
MessagingQueuesViewType,
|
||||
MessagingQueuesViewTypeOptions,
|
||||
ProducerLatencyOptions,
|
||||
} from '../MessagingQueuesUtils';
|
||||
import { MessagingQueueServicePayload } from './MQTables/getConsumerLagDetails';
|
||||
@@ -47,7 +48,7 @@ function PartitionLatencyTabs({
|
||||
);
|
||||
}
|
||||
|
||||
const getTableApi = (selectedView: string): any => {
|
||||
const getTableApi = (selectedView: MessagingQueuesViewTypeOptions): any => {
|
||||
if (selectedView === MessagingQueuesViewType.producerLatency.value) {
|
||||
return getTopicThroughputOverview;
|
||||
}
|
||||
@@ -62,7 +63,7 @@ function MessagingQueueOverview({
|
||||
option,
|
||||
setOption,
|
||||
}: {
|
||||
selectedView: string;
|
||||
selectedView: MessagingQueuesViewTypeOptions;
|
||||
option: ProducerLatencyOptions;
|
||||
setOption: Dispatch<SetStateAction<ProducerLatencyOptions>>;
|
||||
}): JSX.Element {
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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
@@ -60,8 +60,6 @@ function ErrorTitleAndKey({
|
||||
isLeaf?: boolean;
|
||||
}): TreeDataNode {
|
||||
const handleRedirection = (): void => {
|
||||
console.log('Redirect to the error page', parentTitle);
|
||||
|
||||
let link = '';
|
||||
|
||||
switch (parentTitle) {
|
||||
|
||||
@@ -166,3 +166,77 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,6 +222,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
:nth-child(2),
|
||||
:nth-child(4) {
|
||||
border-left: none !important;
|
||||
border-right: none !important;
|
||||
}
|
||||
|
||||
&.summary-section {
|
||||
.overview-info-card {
|
||||
min-height: 144px;
|
||||
@@ -331,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);
|
||||
|
||||
@@ -156,7 +156,7 @@ function MessagingQueues(): JSX.Element {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="overview-info-card middle-card">
|
||||
<div className="overview-info-card">
|
||||
<div>
|
||||
<p className="card-title">{t('summarySection.producer.title')}</p>
|
||||
<p className="card-info-text">
|
||||
@@ -174,7 +174,7 @@ function MessagingQueues(): JSX.Element {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="overview-info-card middle-card">
|
||||
<div className="overview-info-card">
|
||||
<div>
|
||||
<p className="card-title">{t('summarySection.partition.title')}</p>
|
||||
<p className="card-info-text">
|
||||
@@ -210,6 +210,24 @@ function MessagingQueues(): JSX.Element {
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,12 +4,21 @@ 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';
|
||||
|
||||
@@ -209,23 +218,137 @@ 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,
|
||||
},
|
||||
dropRate: {
|
||||
label: 'Drop Rate view',
|
||||
value: 'dropRate',
|
||||
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 {
|
||||
@@ -269,34 +392,3 @@ export enum MessagingQueueHealthCheckService {
|
||||
Producers = 'producers',
|
||||
Kafka = 'kafka',
|
||||
}
|
||||
|
||||
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',
|
||||
}
|
||||
|
||||
@@ -261,7 +261,7 @@ function SignUp({ version }: SignUpProps): JSX.Element {
|
||||
values,
|
||||
async (): Promise<void> => {
|
||||
if (isOnboardingEnabled && isCloudUser()) {
|
||||
history.push(ROUTES.ONBOARDING);
|
||||
history.push(ROUTES.GET_STARTED);
|
||||
} else {
|
||||
history.push(ROUTES.APPLICATION);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>;
|
||||
};
|
||||
|
||||
1062
frontend/yarn.lock
1062
frontend/yarn.lock
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
# use a minimal alpine image
|
||||
FROM alpine:3.18.5
|
||||
FROM alpine:3.20.3
|
||||
|
||||
# Add Maintainer Info
|
||||
LABEL maintainer="signoz"
|
||||
|
||||
@@ -111,6 +111,7 @@ type APIHandler struct {
|
||||
Upgrader *websocket.Upgrader
|
||||
|
||||
UseLogsNewSchema bool
|
||||
UseLicensesV3 bool
|
||||
|
||||
hostsRepo *inframetrics.HostsRepo
|
||||
processesRepo *inframetrics.ProcessesRepo
|
||||
@@ -156,6 +157,9 @@ type APIHandlerOpts struct {
|
||||
|
||||
// Use Logs New schema
|
||||
UseLogsNewSchema bool
|
||||
|
||||
// Use Licenses V3 structure
|
||||
UseLicensesV3 bool
|
||||
}
|
||||
|
||||
// NewAPIHandler returns an APIHandler
|
||||
@@ -211,6 +215,7 @@ func NewAPIHandler(opts APIHandlerOpts) (*APIHandler, error) {
|
||||
querier: querier,
|
||||
querierV2: querierv2,
|
||||
UseLogsNewSchema: opts.UseLogsNewSchema,
|
||||
UseLicensesV3: opts.UseLicensesV3,
|
||||
hostsRepo: hostsRepo,
|
||||
processesRepo: processesRepo,
|
||||
podsRepo: podsRepo,
|
||||
@@ -3217,16 +3222,16 @@ func (aH *APIHandler) getProducerThroughputOverview(
|
||||
}
|
||||
|
||||
for _, res := range result {
|
||||
for _, series := range res.Series {
|
||||
serviceName, serviceNameOk := series.Labels["service_name"]
|
||||
topicName, topicNameOk := series.Labels["topic"]
|
||||
params := []string{serviceName, topicName}
|
||||
for _, list := range res.List {
|
||||
serviceName, serviceNameOk := list.Data["service_name"].(*string)
|
||||
topicName, topicNameOk := list.Data["topic"].(*string)
|
||||
params := []string{*serviceName, *topicName}
|
||||
hashKey := uniqueIdentifier(params, "#")
|
||||
_, ok := attributeCache.Hash[hashKey]
|
||||
if topicNameOk && serviceNameOk && !ok {
|
||||
attributeCache.Hash[hashKey] = struct{}{}
|
||||
attributeCache.TopicName = append(attributeCache.TopicName, topicName)
|
||||
attributeCache.ServiceName = append(attributeCache.ServiceName, serviceName)
|
||||
attributeCache.TopicName = append(attributeCache.TopicName, *topicName)
|
||||
attributeCache.ServiceName = append(attributeCache.ServiceName, *serviceName)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3251,25 +3256,23 @@ func (aH *APIHandler) getProducerThroughputOverview(
|
||||
}
|
||||
|
||||
latencyColumn := &v3.Result{QueryName: "latency"}
|
||||
var latencySeries []*v3.Series
|
||||
var latencySeries []*v3.Row
|
||||
for _, res := range resultFetchLatency {
|
||||
for _, series := range res.Series {
|
||||
topic, topicOk := series.Labels["topic"]
|
||||
serviceName, serviceNameOk := series.Labels["service_name"]
|
||||
params := []string{topic, serviceName}
|
||||
for _, list := range res.List {
|
||||
topic, topicOk := list.Data["topic"].(*string)
|
||||
serviceName, serviceNameOk := list.Data["service_name"].(*string)
|
||||
params := []string{*serviceName, *topic}
|
||||
hashKey := uniqueIdentifier(params, "#")
|
||||
_, ok := attributeCache.Hash[hashKey]
|
||||
if topicOk && serviceNameOk && ok {
|
||||
latencySeries = append(latencySeries, series)
|
||||
latencySeries = append(latencySeries, list)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
latencyColumn.Series = latencySeries
|
||||
latencyColumn.List = latencySeries
|
||||
result = append(result, latencyColumn)
|
||||
|
||||
resultFetchLatency = postprocess.TransformToTableForBuilderQueries(result, queryRangeParams)
|
||||
|
||||
resp := v3.QueryRangeResponse{
|
||||
Result: resultFetchLatency,
|
||||
}
|
||||
|
||||
@@ -53,6 +53,10 @@ func getParamsForTopHosts(req model.HostListRequest) (int64, string, string) {
|
||||
return getParamsForTopItems(req.Start, req.End)
|
||||
}
|
||||
|
||||
func getParamsForTopProcesses(req model.ProcessListRequest) (int64, string, string) {
|
||||
return getParamsForTopItems(req.Start, req.End)
|
||||
}
|
||||
|
||||
func getParamsForTopPods(req model.PodListRequest) (int64, string, string) {
|
||||
return getParamsForTopItems(req.Start, req.End)
|
||||
}
|
||||
|
||||
@@ -2,10 +2,12 @@ package inframetrics
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.signoz.io/signoz/pkg/query-service/app/metrics/v4/helpers"
|
||||
"go.signoz.io/signoz/pkg/query-service/common"
|
||||
"go.signoz.io/signoz/pkg/query-service/interfaces"
|
||||
"go.signoz.io/signoz/pkg/query-service/model"
|
||||
@@ -54,9 +56,16 @@ var (
|
||||
// TODO(srikanthccv): remove hardcoded metric name and support keys from any system metric
|
||||
metricToUseForHostAttributes = "system_cpu_load_average_15m"
|
||||
hostNameAttrKey = "host_name"
|
||||
// TODO(srikanthccv): remove k8s hacky logic from hosts repo after charts users are migrated
|
||||
k8sNodeNameAttrKey = "k8s_node_name"
|
||||
agentNameToIgnore = "k8s-infra-otel-agent"
|
||||
agentNameToIgnore = "k8s-infra-otel-agent"
|
||||
hostAttrsToEnrich = []string{
|
||||
"os_type",
|
||||
}
|
||||
metricNamesForHosts = map[string]string{
|
||||
"cpu": "system_cpu_time",
|
||||
"memory": "system_memory_usage",
|
||||
"load15": "system_cpu_load_average_15m",
|
||||
"wait": "system_cpu_time",
|
||||
}
|
||||
)
|
||||
|
||||
func NewHostsRepo(reader interfaces.Reader, querierV2 interfaces.Querier) *HostsRepo {
|
||||
@@ -112,29 +121,10 @@ func (h *HostsRepo) GetHostAttributeValues(ctx context.Context, req v3.FilterAtt
|
||||
hostNames = append(hostNames, attributeValue)
|
||||
}
|
||||
|
||||
req.FilterAttributeKey = k8sNodeNameAttrKey
|
||||
req.DataSource = v3.DataSourceMetrics
|
||||
req.AggregateAttribute = metricToUseForHostAttributes
|
||||
if req.Limit == 0 {
|
||||
req.Limit = 50
|
||||
}
|
||||
|
||||
attributeValuesResponse, err = h.reader.GetMetricAttributeValues(ctx, &req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, attributeValue := range attributeValuesResponse.StringAttributeValues {
|
||||
if strings.Contains(attributeValue, agentNameToIgnore) {
|
||||
continue
|
||||
}
|
||||
hostNames = append(hostNames, attributeValue)
|
||||
}
|
||||
|
||||
return &v3.FilterAttributeValueResponse{StringAttributeValues: hostNames}, nil
|
||||
}
|
||||
|
||||
func (h *HostsRepo) getActiveHosts(ctx context.Context,
|
||||
req model.HostListRequest, hostNameAttrKey string) (map[string]bool, error) {
|
||||
func (h *HostsRepo) getActiveHosts(ctx context.Context, req model.HostListRequest) (map[string]bool, error) {
|
||||
activeStatus := map[string]bool{}
|
||||
step := common.MinAllowedStepInterval(req.Start, req.End)
|
||||
|
||||
@@ -192,12 +182,72 @@ func (h *HostsRepo) getActiveHosts(ctx context.Context,
|
||||
return activeStatus, nil
|
||||
}
|
||||
|
||||
// getTopHosts returns the top hosts for the given order by column name
|
||||
func (h *HostsRepo) getTopHosts(ctx context.Context, req model.HostListRequest, q *v3.QueryRangeParamsV3, hostNameAttrKey string) ([]string, []string, error) {
|
||||
func (h *HostsRepo) getMetadataAttributes(ctx context.Context, req model.HostListRequest) (map[string]map[string]string, error) {
|
||||
hostAttrs := map[string]map[string]string{}
|
||||
|
||||
for _, key := range hostAttrsToEnrich {
|
||||
hasKey := false
|
||||
for _, groupByKey := range req.GroupBy {
|
||||
if groupByKey.Key == key {
|
||||
hasKey = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasKey {
|
||||
req.GroupBy = append(req.GroupBy, v3.AttributeKey{Key: key})
|
||||
}
|
||||
}
|
||||
|
||||
mq := v3.BuilderQuery{
|
||||
DataSource: v3.DataSourceMetrics,
|
||||
AggregateAttribute: v3.AttributeKey{
|
||||
Key: metricToUseForHostAttributes,
|
||||
DataType: v3.AttributeKeyDataTypeFloat64,
|
||||
},
|
||||
Temporality: v3.Unspecified,
|
||||
GroupBy: req.GroupBy,
|
||||
}
|
||||
|
||||
query, err := helpers.PrepareTimeseriesFilterQuery(req.Start, req.End, &mq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query = localQueryToDistributedQuery(query)
|
||||
|
||||
attrsListResponse, err := h.reader.GetListResultV3(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, row := range attrsListResponse {
|
||||
stringData := map[string]string{}
|
||||
for key, value := range row.Data {
|
||||
if str, ok := value.(string); ok {
|
||||
stringData[key] = str
|
||||
} else if strPtr, ok := value.(*string); ok {
|
||||
stringData[key] = *strPtr
|
||||
}
|
||||
}
|
||||
|
||||
hostName := stringData[hostNameAttrKey]
|
||||
if _, ok := hostAttrs[hostName]; !ok {
|
||||
hostAttrs[hostName] = map[string]string{}
|
||||
}
|
||||
|
||||
for _, key := range req.GroupBy {
|
||||
hostAttrs[hostName][key.Key] = stringData[key.Key]
|
||||
}
|
||||
}
|
||||
|
||||
return hostAttrs, nil
|
||||
}
|
||||
|
||||
func (h *HostsRepo) getTopHostGroups(ctx context.Context, req model.HostListRequest, q *v3.QueryRangeParamsV3) ([]map[string]string, []map[string]string, error) {
|
||||
step, timeSeriesTableName, samplesTableName := getParamsForTopHosts(req)
|
||||
|
||||
queryNames := queryNamesForTopHosts[req.OrderBy.ColumnName]
|
||||
topHostsQueryRangeParams := &v3.QueryRangeParamsV3{
|
||||
topHostGroupsQueryRangeParams := &v3.QueryRangeParamsV3{
|
||||
Start: req.Start,
|
||||
End: req.End,
|
||||
Step: step,
|
||||
@@ -216,19 +266,16 @@ func (h *HostsRepo) getTopHosts(ctx context.Context, req model.HostListRequest,
|
||||
SamplesTableName: samplesTableName,
|
||||
}
|
||||
if req.Filters != nil && len(req.Filters.Items) > 0 {
|
||||
if query.Filters == nil {
|
||||
query.Filters = &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{}}
|
||||
}
|
||||
query.Filters.Items = append(query.Filters.Items, req.Filters.Items...)
|
||||
}
|
||||
topHostsQueryRangeParams.CompositeQuery.BuilderQueries[queryName] = query
|
||||
topHostGroupsQueryRangeParams.CompositeQuery.BuilderQueries[queryName] = query
|
||||
}
|
||||
|
||||
queryResponse, _, err := h.querierV2.QueryRange(ctx, topHostsQueryRangeParams)
|
||||
queryResponse, _, err := h.querierV2.QueryRange(ctx, topHostGroupsQueryRangeParams)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
formattedResponse, err := postprocess.PostProcessResult(queryResponse, topHostsQueryRangeParams)
|
||||
formattedResponse, err := postprocess.PostProcessResult(queryResponse, topHostGroupsQueryRangeParams)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -247,238 +294,150 @@ func (h *HostsRepo) getTopHosts(ctx context.Context, req model.HostListRequest,
|
||||
})
|
||||
}
|
||||
|
||||
paginatedTopHostsSeries := formattedResponse[0].Series[req.Offset : req.Offset+req.Limit]
|
||||
limit := math.Min(float64(req.Offset+req.Limit), float64(len(formattedResponse[0].Series)))
|
||||
|
||||
topHosts := []string{}
|
||||
for _, series := range paginatedTopHostsSeries {
|
||||
topHosts = append(topHosts, series.Labels[hostNameAttrKey])
|
||||
paginatedTopHostGroupsSeries := formattedResponse[0].Series[req.Offset:int(limit)]
|
||||
|
||||
topHostGroups := []map[string]string{}
|
||||
for _, series := range paginatedTopHostGroupsSeries {
|
||||
topHostGroups = append(topHostGroups, series.Labels)
|
||||
}
|
||||
allHosts := []string{}
|
||||
allHostGroups := []map[string]string{}
|
||||
for _, series := range formattedResponse[0].Series {
|
||||
allHosts = append(allHosts, series.Labels[hostNameAttrKey])
|
||||
allHostGroups = append(allHostGroups, series.Labels)
|
||||
}
|
||||
|
||||
return topHosts, allHosts, nil
|
||||
return topHostGroups, allHostGroups, nil
|
||||
}
|
||||
|
||||
func (h *HostsRepo) getHostsForQuery(ctx context.Context,
|
||||
req model.HostListRequest, q *v3.QueryRangeParamsV3, hostNameAttrKey string) ([]model.HostListRecord, []string, error) {
|
||||
func (h *HostsRepo) GetHostList(ctx context.Context, req model.HostListRequest) (model.HostListResponse, error) {
|
||||
resp := model.HostListResponse{}
|
||||
|
||||
step := common.MinAllowedStepInterval(req.Start, req.End)
|
||||
if req.Limit == 0 {
|
||||
req.Limit = 10
|
||||
}
|
||||
|
||||
query := q.Clone()
|
||||
// default to cpu order by
|
||||
if req.OrderBy == nil {
|
||||
req.OrderBy = &v3.OrderBy{ColumnName: "cpu", Order: v3.DirectionDesc}
|
||||
}
|
||||
|
||||
// default to host name group by
|
||||
if len(req.GroupBy) == 0 {
|
||||
req.GroupBy = []v3.AttributeKey{{Key: hostNameAttrKey}}
|
||||
resp.Type = model.ResponseTypeList
|
||||
} else {
|
||||
resp.Type = model.ResponseTypeGroupedList
|
||||
}
|
||||
|
||||
step := int64(math.Max(float64(common.MinAllowedStepInterval(req.Start, req.End)), 60))
|
||||
|
||||
query := HostsTableListQuery.Clone()
|
||||
|
||||
query.Start = req.Start
|
||||
query.End = req.End
|
||||
query.Step = step
|
||||
|
||||
topHosts, allHosts, err := h.getTopHosts(ctx, req, q, hostNameAttrKey)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
for _, query := range query.CompositeQuery.BuilderQueries {
|
||||
query.StepInterval = step
|
||||
// check if the filter has host_name and is either IN or EQUAL operator
|
||||
// if so, we don't need to add the topHosts filter again
|
||||
hasHostNameInOrEqual := false
|
||||
|
||||
if req.Filters != nil && len(req.Filters.Items) > 0 {
|
||||
for _, item := range req.Filters.Items {
|
||||
if item.Key.Key == hostNameAttrKey && (item.Operator == v3.FilterOperatorIn || item.Operator == v3.FilterOperatorEqual) {
|
||||
hasHostNameInOrEqual = true
|
||||
}
|
||||
}
|
||||
if query.Filters == nil {
|
||||
query.Filters = &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{}}
|
||||
}
|
||||
query.Filters.Items = append(query.Filters.Items, req.Filters.Items...)
|
||||
// what is happening here?
|
||||
// if the filter has host_name and we are querying for k8s host metrics,
|
||||
// we need to replace the host_name with k8s_node_name
|
||||
if hostNameAttrKey == k8sNodeNameAttrKey {
|
||||
for idx, item := range query.Filters.Items {
|
||||
if item.Key.Key == hostNameAttrKey {
|
||||
query.Filters.Items[idx].Key.Key = k8sNodeNameAttrKey
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !hasHostNameInOrEqual {
|
||||
if query.Filters == nil {
|
||||
query.Filters = &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{}}
|
||||
}
|
||||
query.Filters.Items = append(query.Filters.Items, v3.FilterItem{
|
||||
Key: v3.AttributeKey{
|
||||
Key: hostNameAttrKey,
|
||||
},
|
||||
Value: topHosts,
|
||||
Operator: v3.FilterOperatorIn,
|
||||
})
|
||||
query.GroupBy = req.GroupBy
|
||||
}
|
||||
|
||||
hostAttrs, err := h.getMetadataAttributes(ctx, req)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
activeHosts, err := h.getActiveHosts(ctx, req)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
topHostGroups, allHostGroups, err := h.getTopHostGroups(ctx, req, query)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
groupFilters := map[string][]string{}
|
||||
for _, topHostGroup := range topHostGroups {
|
||||
for k, v := range topHostGroup {
|
||||
groupFilters[k] = append(groupFilters[k], v)
|
||||
}
|
||||
}
|
||||
|
||||
activeHosts, err := h.getActiveHosts(ctx, req, hostNameAttrKey)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
for groupKey, groupValues := range groupFilters {
|
||||
hasGroupFilter := false
|
||||
if req.Filters != nil && len(req.Filters.Items) > 0 {
|
||||
for _, filter := range req.Filters.Items {
|
||||
if filter.Key.Key == groupKey {
|
||||
hasGroupFilter = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !hasGroupFilter {
|
||||
for _, query := range query.CompositeQuery.BuilderQueries {
|
||||
query.Filters.Items = append(query.Filters.Items, v3.FilterItem{
|
||||
Key: v3.AttributeKey{Key: groupKey},
|
||||
Value: groupValues,
|
||||
Operator: v3.FilterOperatorIn,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
queryResponse, _, err := h.querierV2.QueryRange(ctx, query)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return resp, err
|
||||
}
|
||||
|
||||
type hostTSInfo struct {
|
||||
cpuTimeSeries *v3.Series
|
||||
memoryTimeSeries *v3.Series
|
||||
waitTimeSeries *v3.Series
|
||||
load15TimeSeries *v3.Series
|
||||
}
|
||||
hostTSInfoMap := map[string]*hostTSInfo{}
|
||||
|
||||
for _, result := range queryResponse {
|
||||
for _, series := range result.Series {
|
||||
hostName := series.Labels[hostNameAttrKey]
|
||||
if _, ok := hostTSInfoMap[hostName]; !ok {
|
||||
hostTSInfoMap[hostName] = &hostTSInfo{}
|
||||
}
|
||||
if result.QueryName == "G" {
|
||||
loadSeries := *series
|
||||
hostTSInfoMap[hostName].load15TimeSeries = &loadSeries
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query.FormatForWeb = false
|
||||
query.CompositeQuery.PanelType = v3.PanelTypeGraph
|
||||
|
||||
formulaResult, err := postprocess.PostProcessResult(queryResponse, query)
|
||||
formattedResponse, err := postprocess.PostProcessResult(queryResponse, query)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return resp, err
|
||||
}
|
||||
|
||||
for _, result := range formulaResult {
|
||||
for _, series := range result.Series {
|
||||
hostName := series.Labels[hostNameAttrKey]
|
||||
if _, ok := hostTSInfoMap[hostName]; !ok {
|
||||
hostTSInfoMap[hostName] = &hostTSInfo{}
|
||||
}
|
||||
if result.QueryName == "F1" {
|
||||
hostTSInfoMap[hostName].cpuTimeSeries = series
|
||||
} else if result.QueryName == "F2" {
|
||||
hostTSInfoMap[hostName].memoryTimeSeries = series
|
||||
} else if result.QueryName == "F3" {
|
||||
hostTSInfoMap[hostName].waitTimeSeries = series
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query.FormatForWeb = true
|
||||
query.CompositeQuery.PanelType = v3.PanelTypeTable
|
||||
formattedResponse, _ := postprocess.PostProcessResult(queryResponse, query)
|
||||
|
||||
records := []model.HostListRecord{}
|
||||
|
||||
// there should be only one result in the response
|
||||
hostsInfo := formattedResponse[0]
|
||||
// each row represents a host
|
||||
for _, row := range hostsInfo.Table.Rows {
|
||||
record := model.HostListRecord{
|
||||
CPU: -1,
|
||||
Memory: -1,
|
||||
Wait: -1,
|
||||
Load15: -1,
|
||||
}
|
||||
for _, result := range formattedResponse {
|
||||
for _, row := range result.Table.Rows {
|
||||
record := model.HostListRecord{
|
||||
CPU: -1,
|
||||
Memory: -1,
|
||||
Wait: -1,
|
||||
Load15: -1,
|
||||
}
|
||||
|
||||
hostName, ok := row.Data[hostNameAttrKey].(string)
|
||||
if ok {
|
||||
record.HostName = hostName
|
||||
}
|
||||
if hostName, ok := row.Data[hostNameAttrKey].(string); ok {
|
||||
record.HostName = hostName
|
||||
}
|
||||
|
||||
osType, ok := row.Data["os_type"].(string)
|
||||
if ok {
|
||||
record.OS = osType
|
||||
}
|
||||
|
||||
cpu, ok := row.Data["F1"].(float64)
|
||||
if ok {
|
||||
record.CPU = cpu
|
||||
}
|
||||
memory, ok := row.Data["F2"].(float64)
|
||||
if ok {
|
||||
record.Memory = memory
|
||||
}
|
||||
wait, ok := row.Data["F3"].(float64)
|
||||
if ok {
|
||||
record.Wait = wait
|
||||
}
|
||||
load15, ok := row.Data["G"].(float64)
|
||||
if ok {
|
||||
record.Load15 = load15
|
||||
}
|
||||
record.Active = activeHosts[record.HostName]
|
||||
if hostTSInfoMap[record.HostName] != nil {
|
||||
record.CPUTimeSeries = hostTSInfoMap[record.HostName].cpuTimeSeries
|
||||
record.MemoryTimeSeries = hostTSInfoMap[record.HostName].memoryTimeSeries
|
||||
record.WaitTimeSeries = hostTSInfoMap[record.HostName].waitTimeSeries
|
||||
record.Load15TimeSeries = hostTSInfoMap[record.HostName].load15TimeSeries
|
||||
}
|
||||
records = append(records, record)
|
||||
}
|
||||
|
||||
return records, allHosts, nil
|
||||
}
|
||||
|
||||
func dedupRecords(records []model.HostListRecord) []model.HostListRecord {
|
||||
seen := map[string]bool{}
|
||||
deduped := []model.HostListRecord{}
|
||||
for _, record := range records {
|
||||
if !seen[record.HostName] {
|
||||
seen[record.HostName] = true
|
||||
deduped = append(deduped, record)
|
||||
if cpu, ok := row.Data["F1"].(float64); ok {
|
||||
record.CPU = cpu
|
||||
}
|
||||
if memory, ok := row.Data["F2"].(float64); ok {
|
||||
record.Memory = memory
|
||||
}
|
||||
if wait, ok := row.Data["F3"].(float64); ok {
|
||||
record.Wait = wait
|
||||
}
|
||||
if load15, ok := row.Data["G"].(float64); ok {
|
||||
record.Load15 = load15
|
||||
}
|
||||
record.Meta = map[string]string{}
|
||||
if _, ok := hostAttrs[record.HostName]; ok {
|
||||
record.Meta = hostAttrs[record.HostName]
|
||||
}
|
||||
if osType, ok := record.Meta["os_type"]; ok {
|
||||
record.OS = osType
|
||||
}
|
||||
record.Active = activeHosts[record.HostName]
|
||||
records = append(records, record)
|
||||
}
|
||||
}
|
||||
return deduped
|
||||
}
|
||||
|
||||
func (h *HostsRepo) GetHostList(ctx context.Context, req model.HostListRequest) (model.HostListResponse, error) {
|
||||
if req.Limit == 0 {
|
||||
req.Limit = 10
|
||||
}
|
||||
|
||||
if req.OrderBy == nil {
|
||||
req.OrderBy = &v3.OrderBy{ColumnName: "cpu", Order: v3.DirectionDesc}
|
||||
}
|
||||
|
||||
resp := model.HostListResponse{
|
||||
Type: "list",
|
||||
}
|
||||
|
||||
vmRecords, vmAllHosts, err := h.getHostsForQuery(ctx, req, &NonK8STableListQuery, hostNameAttrKey)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
k8sRecords, k8sAllHosts, err := h.getHostsForQuery(ctx, req, &K8STableListQuery, k8sNodeNameAttrKey)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
uniqueHosts := map[string]bool{}
|
||||
for _, host := range vmAllHosts {
|
||||
uniqueHosts[host] = true
|
||||
}
|
||||
for _, host := range k8sAllHosts {
|
||||
uniqueHosts[host] = true
|
||||
}
|
||||
|
||||
records := append(vmRecords, k8sRecords...)
|
||||
|
||||
// since we added the fix for incorrect host name, it is possible that both host_name and k8s_node_name
|
||||
// are present in the response. we need to dedup the results.
|
||||
records = dedupRecords(records)
|
||||
|
||||
resp.Total = len(uniqueHosts)
|
||||
|
||||
resp.Total = len(allHostGroups)
|
||||
resp.Records = records
|
||||
|
||||
return resp, nil
|
||||
|
||||
@@ -2,14 +2,14 @@ package inframetrics
|
||||
|
||||
import v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
||||
|
||||
var NonK8STableListQuery = v3.QueryRangeParamsV3{
|
||||
var HostsTableListQuery = v3.QueryRangeParamsV3{
|
||||
CompositeQuery: &v3.CompositeQuery{
|
||||
BuilderQueries: map[string]*v3.BuilderQuery{
|
||||
"A": {
|
||||
QueryName: "A",
|
||||
DataSource: v3.DataSourceMetrics,
|
||||
AggregateAttribute: v3.AttributeKey{
|
||||
Key: "system_cpu_time",
|
||||
Key: metricNamesForHosts["cpu"],
|
||||
DataType: v3.AttributeKeyDataTypeFloat64,
|
||||
},
|
||||
Temporality: v3.Cumulative,
|
||||
@@ -27,23 +27,18 @@ var NonK8STableListQuery = v3.QueryRangeParamsV3{
|
||||
},
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "host_name",
|
||||
Key: hostNameAttrKey,
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeResource,
|
||||
},
|
||||
Operator: v3.FilterOperatorNotContains,
|
||||
Value: "k8s-infra-otel-agent",
|
||||
Value: agentNameToIgnore,
|
||||
},
|
||||
},
|
||||
},
|
||||
GroupBy: []v3.AttributeKey{
|
||||
{
|
||||
Key: "host_name",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeResource,
|
||||
},
|
||||
{
|
||||
Key: "os_type",
|
||||
Key: hostNameAttrKey,
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeResource,
|
||||
},
|
||||
@@ -58,7 +53,7 @@ var NonK8STableListQuery = v3.QueryRangeParamsV3{
|
||||
QueryName: "B",
|
||||
DataSource: v3.DataSourceMetrics,
|
||||
AggregateAttribute: v3.AttributeKey{
|
||||
Key: "system_cpu_time",
|
||||
Key: metricNamesForHosts["cpu"],
|
||||
DataType: v3.AttributeKeyDataTypeFloat64,
|
||||
},
|
||||
Temporality: v3.Cumulative,
|
||||
@@ -67,23 +62,18 @@ var NonK8STableListQuery = v3.QueryRangeParamsV3{
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "host_name",
|
||||
Key: hostNameAttrKey,
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeResource,
|
||||
},
|
||||
Operator: v3.FilterOperatorNotContains,
|
||||
Value: "k8s-infra-otel-agent",
|
||||
Value: agentNameToIgnore,
|
||||
},
|
||||
},
|
||||
},
|
||||
GroupBy: []v3.AttributeKey{
|
||||
{
|
||||
Key: "host_name",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeResource,
|
||||
},
|
||||
{
|
||||
Key: "os_type",
|
||||
Key: hostNameAttrKey,
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeResource,
|
||||
},
|
||||
@@ -98,12 +88,16 @@ var NonK8STableListQuery = v3.QueryRangeParamsV3{
|
||||
QueryName: "F1",
|
||||
Expression: "A/B",
|
||||
Legend: "CPU Usage (%)",
|
||||
Filters: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{},
|
||||
},
|
||||
},
|
||||
"C": {
|
||||
QueryName: "C",
|
||||
DataSource: v3.DataSourceMetrics,
|
||||
AggregateAttribute: v3.AttributeKey{
|
||||
Key: "system_memory_usage",
|
||||
Key: metricNamesForHosts["memory"],
|
||||
DataType: v3.AttributeKeyDataTypeFloat64,
|
||||
},
|
||||
Temporality: v3.Cumulative,
|
||||
@@ -121,23 +115,18 @@ var NonK8STableListQuery = v3.QueryRangeParamsV3{
|
||||
},
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "host_name",
|
||||
Key: hostNameAttrKey,
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeResource,
|
||||
},
|
||||
Operator: v3.FilterOperatorNotContains,
|
||||
Value: "k8s-infra-otel-agent",
|
||||
Value: agentNameToIgnore,
|
||||
},
|
||||
},
|
||||
},
|
||||
GroupBy: []v3.AttributeKey{
|
||||
{
|
||||
Key: "host_name",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeResource,
|
||||
},
|
||||
{
|
||||
Key: "os_type",
|
||||
Key: hostNameAttrKey,
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeResource,
|
||||
},
|
||||
@@ -152,7 +141,7 @@ var NonK8STableListQuery = v3.QueryRangeParamsV3{
|
||||
QueryName: "D",
|
||||
DataSource: v3.DataSourceMetrics,
|
||||
AggregateAttribute: v3.AttributeKey{
|
||||
Key: "system_memory_usage",
|
||||
Key: metricNamesForHosts["memory"],
|
||||
DataType: v3.AttributeKeyDataTypeFloat64,
|
||||
},
|
||||
Temporality: v3.Cumulative,
|
||||
@@ -161,23 +150,18 @@ var NonK8STableListQuery = v3.QueryRangeParamsV3{
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "host_name",
|
||||
Key: hostNameAttrKey,
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeResource,
|
||||
},
|
||||
Operator: v3.FilterOperatorNotContains,
|
||||
Value: "k8s-infra-otel-agent",
|
||||
Value: agentNameToIgnore,
|
||||
},
|
||||
},
|
||||
},
|
||||
GroupBy: []v3.AttributeKey{
|
||||
{
|
||||
Key: "host_name",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeResource,
|
||||
},
|
||||
{
|
||||
Key: "os_type",
|
||||
Key: hostNameAttrKey,
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeResource,
|
||||
},
|
||||
@@ -192,12 +176,16 @@ var NonK8STableListQuery = v3.QueryRangeParamsV3{
|
||||
QueryName: "F2",
|
||||
Expression: "C/D",
|
||||
Legend: "Memory Usage (%)",
|
||||
Filters: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{},
|
||||
},
|
||||
},
|
||||
"E": {
|
||||
QueryName: "E",
|
||||
DataSource: v3.DataSourceMetrics,
|
||||
AggregateAttribute: v3.AttributeKey{
|
||||
Key: "system_cpu_time",
|
||||
Key: metricNamesForHosts["wait"],
|
||||
DataType: v3.AttributeKeyDataTypeFloat64,
|
||||
},
|
||||
Temporality: v3.Cumulative,
|
||||
@@ -215,23 +203,18 @@ var NonK8STableListQuery = v3.QueryRangeParamsV3{
|
||||
},
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "host_name",
|
||||
Key: hostNameAttrKey,
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeResource,
|
||||
},
|
||||
Operator: v3.FilterOperatorNotContains,
|
||||
Value: "k8s-infra-otel-agent",
|
||||
Value: agentNameToIgnore,
|
||||
},
|
||||
},
|
||||
},
|
||||
GroupBy: []v3.AttributeKey{
|
||||
{
|
||||
Key: "host_name",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeResource,
|
||||
},
|
||||
{
|
||||
Key: "os_type",
|
||||
Key: hostNameAttrKey,
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeResource,
|
||||
},
|
||||
@@ -246,7 +229,7 @@ var NonK8STableListQuery = v3.QueryRangeParamsV3{
|
||||
QueryName: "F",
|
||||
DataSource: v3.DataSourceMetrics,
|
||||
AggregateAttribute: v3.AttributeKey{
|
||||
Key: "system_cpu_time",
|
||||
Key: metricNamesForHosts["wait"],
|
||||
DataType: v3.AttributeKeyDataTypeFloat64,
|
||||
},
|
||||
Temporality: v3.Cumulative,
|
||||
@@ -255,23 +238,18 @@ var NonK8STableListQuery = v3.QueryRangeParamsV3{
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "host_name",
|
||||
Key: hostNameAttrKey,
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeResource,
|
||||
},
|
||||
Operator: v3.FilterOperatorNotContains,
|
||||
Value: "k8s-infra-otel-agent",
|
||||
Value: agentNameToIgnore,
|
||||
},
|
||||
},
|
||||
},
|
||||
GroupBy: []v3.AttributeKey{
|
||||
{
|
||||
Key: "host_name",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeResource,
|
||||
},
|
||||
{
|
||||
Key: "os_type",
|
||||
Key: hostNameAttrKey,
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeResource,
|
||||
},
|
||||
@@ -286,12 +264,16 @@ var NonK8STableListQuery = v3.QueryRangeParamsV3{
|
||||
QueryName: "F3",
|
||||
Expression: "E/F",
|
||||
Legend: "CPU Wait Time (%)",
|
||||
Filters: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{},
|
||||
},
|
||||
},
|
||||
"G": {
|
||||
QueryName: "G",
|
||||
DataSource: v3.DataSourceMetrics,
|
||||
AggregateAttribute: v3.AttributeKey{
|
||||
Key: "system_cpu_load_average_15m",
|
||||
Key: metricNamesForHosts["load15"],
|
||||
DataType: v3.AttributeKeyDataTypeFloat64,
|
||||
},
|
||||
Temporality: v3.Unspecified,
|
||||
@@ -300,23 +282,18 @@ var NonK8STableListQuery = v3.QueryRangeParamsV3{
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "host_name",
|
||||
Key: hostNameAttrKey,
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeResource,
|
||||
},
|
||||
Operator: v3.FilterOperatorNotContains,
|
||||
Value: "k8s-infra-otel-agent",
|
||||
Value: agentNameToIgnore,
|
||||
},
|
||||
},
|
||||
},
|
||||
GroupBy: []v3.AttributeKey{
|
||||
{
|
||||
Key: "host_name",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeResource,
|
||||
},
|
||||
{
|
||||
Key: "os_type",
|
||||
Key: hostNameAttrKey,
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeResource,
|
||||
},
|
||||
@@ -335,69 +312,3 @@ var NonK8STableListQuery = v3.QueryRangeParamsV3{
|
||||
Version: "v4",
|
||||
FormatForWeb: true,
|
||||
}
|
||||
|
||||
var ProcessesTableListQuery = v3.QueryRangeParamsV3{
|
||||
CompositeQuery: &v3.CompositeQuery{
|
||||
BuilderQueries: map[string]*v3.BuilderQuery{
|
||||
"A": {
|
||||
QueryName: "A",
|
||||
DataSource: v3.DataSourceMetrics,
|
||||
AggregateAttribute: v3.AttributeKey{
|
||||
Key: "process_cpu_time",
|
||||
DataType: v3.AttributeKeyDataTypeFloat64,
|
||||
},
|
||||
Temporality: v3.Cumulative,
|
||||
Filters: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{},
|
||||
},
|
||||
GroupBy: []v3.AttributeKey{
|
||||
{
|
||||
Key: "process_pid",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeResource,
|
||||
},
|
||||
},
|
||||
Expression: "A",
|
||||
ReduceTo: v3.ReduceToOperatorAvg,
|
||||
TimeAggregation: v3.TimeAggregationRate,
|
||||
SpaceAggregation: v3.SpaceAggregationSum,
|
||||
Disabled: true,
|
||||
},
|
||||
"F1": {
|
||||
QueryName: "F1",
|
||||
Expression: "A",
|
||||
Legend: "Process CPU Usage (%)",
|
||||
},
|
||||
"C": {
|
||||
QueryName: "C",
|
||||
DataSource: v3.DataSourceMetrics,
|
||||
AggregateAttribute: v3.AttributeKey{
|
||||
Key: "process_memory_usage",
|
||||
DataType: v3.AttributeKeyDataTypeFloat64,
|
||||
},
|
||||
Temporality: v3.Cumulative,
|
||||
Filters: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{},
|
||||
},
|
||||
GroupBy: []v3.AttributeKey{
|
||||
{
|
||||
Key: "process_pid",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeResource,
|
||||
},
|
||||
},
|
||||
Expression: "C",
|
||||
ReduceTo: v3.ReduceToOperatorAvg,
|
||||
TimeAggregation: v3.TimeAggregationAvg,
|
||||
SpaceAggregation: v3.SpaceAggregationSum,
|
||||
Disabled: false,
|
||||
},
|
||||
},
|
||||
PanelType: v3.PanelTypeTable,
|
||||
QueryType: v3.QueryTypeBuilder,
|
||||
},
|
||||
Version: "v4",
|
||||
FormatForWeb: true,
|
||||
}
|
||||
@@ -178,7 +178,9 @@ func (p *NamespacesRepo) getTopNamespaceGroups(ctx context.Context, req model.Na
|
||||
})
|
||||
}
|
||||
|
||||
paginatedTopNamespaceGroupsSeries := formattedResponse[0].Series[req.Offset : req.Offset+req.Limit]
|
||||
limit := math.Min(float64(req.Offset+req.Limit), float64(len(formattedResponse[0].Series)))
|
||||
|
||||
paginatedTopNamespaceGroupsSeries := formattedResponse[0].Series[req.Offset:int(limit)]
|
||||
|
||||
topNamespaceGroups := []map[string]string{}
|
||||
for _, series := range paginatedTopNamespaceGroupsSeries {
|
||||
|
||||
@@ -217,7 +217,9 @@ func (p *PodsRepo) getTopPodGroups(ctx context.Context, req model.PodListRequest
|
||||
})
|
||||
}
|
||||
|
||||
paginatedTopPodGroupsSeries := formattedResponse[0].Series[req.Offset : req.Offset+req.Limit]
|
||||
limit := math.Min(float64(req.Offset+req.Limit), float64(len(formattedResponse[0].Series)))
|
||||
|
||||
paginatedTopPodGroupsSeries := formattedResponse[0].Series[req.Offset:int(limit)]
|
||||
|
||||
topPodGroups := []map[string]string{}
|
||||
for _, series := range paginatedTopPodGroupsSeries {
|
||||
|
||||
73
pkg/query-service/app/inframetrics/process_query.go
Normal file
73
pkg/query-service/app/inframetrics/process_query.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package inframetrics
|
||||
|
||||
import v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
||||
|
||||
var ProcessesTableListQuery = v3.QueryRangeParamsV3{
|
||||
CompositeQuery: &v3.CompositeQuery{
|
||||
BuilderQueries: map[string]*v3.BuilderQuery{
|
||||
"A": {
|
||||
QueryName: "A",
|
||||
DataSource: v3.DataSourceMetrics,
|
||||
AggregateAttribute: v3.AttributeKey{
|
||||
Key: metricNamesForProcesses["cpu"],
|
||||
DataType: v3.AttributeKeyDataTypeFloat64,
|
||||
},
|
||||
Temporality: v3.Cumulative,
|
||||
Filters: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{},
|
||||
},
|
||||
GroupBy: []v3.AttributeKey{
|
||||
{
|
||||
Key: processPIDAttrKey,
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeResource,
|
||||
},
|
||||
},
|
||||
Expression: "A",
|
||||
ReduceTo: v3.ReduceToOperatorAvg,
|
||||
TimeAggregation: v3.TimeAggregationRate,
|
||||
SpaceAggregation: v3.SpaceAggregationSum,
|
||||
Disabled: true,
|
||||
},
|
||||
"F1": {
|
||||
QueryName: "F1",
|
||||
Expression: "A",
|
||||
Legend: "Process CPU Usage (%)",
|
||||
Filters: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{},
|
||||
},
|
||||
},
|
||||
"C": {
|
||||
QueryName: "C",
|
||||
DataSource: v3.DataSourceMetrics,
|
||||
AggregateAttribute: v3.AttributeKey{
|
||||
Key: metricNamesForProcesses["memory"],
|
||||
DataType: v3.AttributeKeyDataTypeFloat64,
|
||||
},
|
||||
Temporality: v3.Cumulative,
|
||||
Filters: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{},
|
||||
},
|
||||
GroupBy: []v3.AttributeKey{
|
||||
{
|
||||
Key: processPIDAttrKey,
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeResource,
|
||||
},
|
||||
},
|
||||
Expression: "C",
|
||||
ReduceTo: v3.ReduceToOperatorAvg,
|
||||
TimeAggregation: v3.TimeAggregationAvg,
|
||||
SpaceAggregation: v3.SpaceAggregationSum,
|
||||
Disabled: false,
|
||||
},
|
||||
},
|
||||
PanelType: v3.PanelTypeTable,
|
||||
QueryType: v3.QueryTypeBuilder,
|
||||
},
|
||||
Version: "v4",
|
||||
FormatForWeb: true,
|
||||
}
|
||||
@@ -2,9 +2,8 @@ package inframetrics
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
"sort"
|
||||
|
||||
"go.signoz.io/signoz/pkg/query-service/app/metrics/v4/helpers"
|
||||
"go.signoz.io/signoz/pkg/query-service/common"
|
||||
@@ -15,6 +14,23 @@ import (
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
var (
|
||||
queryNamesForTopProcesses = map[string][]string{
|
||||
"cpu": {"A"},
|
||||
"memory": {"C"},
|
||||
}
|
||||
|
||||
processPIDAttrKey = "process_pid"
|
||||
metricNamesForProcesses = map[string]string{
|
||||
"cpu": "process_cpu_time",
|
||||
"memory": "process_memory_usage",
|
||||
}
|
||||
metricToUseForProcessAttributes = "process_memory_usage"
|
||||
processNameAttrKey = "process_executable_name"
|
||||
processCMDAttrKey = "process_command"
|
||||
processCMDLineAttrKey = "process_command_line"
|
||||
)
|
||||
|
||||
type ProcessesRepo struct {
|
||||
reader interfaces.Reader
|
||||
querierV2 interfaces.Querier
|
||||
@@ -64,14 +80,6 @@ func (p *ProcessesRepo) GetProcessAttributeValues(ctx context.Context, req v3.Fi
|
||||
return attributeValuesResponse, nil
|
||||
}
|
||||
|
||||
func getGroupKeyForProcesses(record model.ProcessListRecord, groupBy []v3.AttributeKey) string {
|
||||
groupKey := ""
|
||||
for _, key := range groupBy {
|
||||
groupKey += fmt.Sprintf("%s=%s,", key.Key, record.Meta[key.Key])
|
||||
}
|
||||
return groupKey
|
||||
}
|
||||
|
||||
func (p *ProcessesRepo) getMetadataAttributes(ctx context.Context,
|
||||
req model.ProcessListRequest) (map[string]map[string]string, error) {
|
||||
processAttrs := map[string]map[string]string{}
|
||||
@@ -92,7 +100,7 @@ func (p *ProcessesRepo) getMetadataAttributes(ctx context.Context,
|
||||
|
||||
mq := v3.BuilderQuery{
|
||||
AggregateAttribute: v3.AttributeKey{
|
||||
Key: "process_memory_usage",
|
||||
Key: metricToUseForProcessAttributes,
|
||||
DataType: v3.AttributeKeyDataTypeFloat64,
|
||||
},
|
||||
Temporality: v3.Cumulative,
|
||||
@@ -104,14 +112,7 @@ func (p *ProcessesRepo) getMetadataAttributes(ctx context.Context,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO(srikanthccv): remove this
|
||||
// What is happening here?
|
||||
// The `PrepareTimeseriesFilterQuery` uses the local time series table for sub-query because each fingerprint
|
||||
// goes to same shard.
|
||||
// However, in this case, we are interested in the attributes values across all the shards.
|
||||
// So, we replace the local time series table with the distributed time series table.
|
||||
// See `PrepareTimeseriesFilterQuery` for more details.
|
||||
query = strings.Replace(query, ".time_series_v4", ".distributed_time_series_v4", 1)
|
||||
query = localQueryToDistributedQuery(query)
|
||||
|
||||
attrsListResponse, err := p.reader.GetListResultV3(ctx, query)
|
||||
if err != nil {
|
||||
@@ -128,36 +129,108 @@ func (p *ProcessesRepo) getMetadataAttributes(ctx context.Context,
|
||||
}
|
||||
}
|
||||
|
||||
pid := stringData["process_pid"]
|
||||
if _, ok := processAttrs[pid]; !ok {
|
||||
processAttrs[pid] = map[string]string{}
|
||||
processID := stringData[processPIDAttrKey]
|
||||
if _, ok := processAttrs[processID]; !ok {
|
||||
processAttrs[processID] = map[string]string{}
|
||||
}
|
||||
|
||||
for _, key := range req.GroupBy {
|
||||
processAttrs[pid][key.Key] = stringData[key.Key]
|
||||
processAttrs[processID][key.Key] = stringData[key.Key]
|
||||
}
|
||||
}
|
||||
|
||||
return processAttrs, nil
|
||||
}
|
||||
|
||||
func (p *ProcessesRepo) getTopProcessGroups(ctx context.Context, req model.ProcessListRequest, q *v3.QueryRangeParamsV3) ([]map[string]string, []map[string]string, error) {
|
||||
step, timeSeriesTableName, samplesTableName := getParamsForTopProcesses(req)
|
||||
|
||||
queryNames := queryNamesForTopProcesses[req.OrderBy.ColumnName]
|
||||
topProcessGroupsQueryRangeParams := &v3.QueryRangeParamsV3{
|
||||
Start: req.Start,
|
||||
End: req.End,
|
||||
Step: step,
|
||||
CompositeQuery: &v3.CompositeQuery{
|
||||
BuilderQueries: map[string]*v3.BuilderQuery{},
|
||||
QueryType: v3.QueryTypeBuilder,
|
||||
PanelType: v3.PanelTypeTable,
|
||||
},
|
||||
}
|
||||
|
||||
for _, queryName := range queryNames {
|
||||
query := q.CompositeQuery.BuilderQueries[queryName].Clone()
|
||||
query.StepInterval = step
|
||||
query.MetricTableHints = &v3.MetricTableHints{
|
||||
TimeSeriesTableName: timeSeriesTableName,
|
||||
SamplesTableName: samplesTableName,
|
||||
}
|
||||
if req.Filters != nil && len(req.Filters.Items) > 0 {
|
||||
query.Filters.Items = append(query.Filters.Items, req.Filters.Items...)
|
||||
}
|
||||
topProcessGroupsQueryRangeParams.CompositeQuery.BuilderQueries[queryName] = query
|
||||
}
|
||||
|
||||
queryResponse, _, err := p.querierV2.QueryRange(ctx, topProcessGroupsQueryRangeParams)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
formattedResponse, err := postprocess.PostProcessResult(queryResponse, topProcessGroupsQueryRangeParams)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if len(formattedResponse) == 0 || len(formattedResponse[0].Series) == 0 {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
if req.OrderBy.Order == v3.DirectionDesc {
|
||||
sort.Slice(formattedResponse[0].Series, func(i, j int) bool {
|
||||
return formattedResponse[0].Series[i].Points[0].Value > formattedResponse[0].Series[j].Points[0].Value
|
||||
})
|
||||
} else {
|
||||
sort.Slice(formattedResponse[0].Series, func(i, j int) bool {
|
||||
return formattedResponse[0].Series[i].Points[0].Value < formattedResponse[0].Series[j].Points[0].Value
|
||||
})
|
||||
}
|
||||
|
||||
limit := math.Min(float64(req.Offset+req.Limit), float64(len(formattedResponse[0].Series)))
|
||||
|
||||
paginatedTopProcessGroupsSeries := formattedResponse[0].Series[req.Offset:int(limit)]
|
||||
|
||||
topProcessGroups := []map[string]string{}
|
||||
for _, series := range paginatedTopProcessGroupsSeries {
|
||||
topProcessGroups = append(topProcessGroups, series.Labels)
|
||||
}
|
||||
allProcessGroups := []map[string]string{}
|
||||
for _, series := range formattedResponse[0].Series {
|
||||
allProcessGroups = append(allProcessGroups, series.Labels)
|
||||
}
|
||||
|
||||
return topProcessGroups, allProcessGroups, nil
|
||||
}
|
||||
|
||||
func (p *ProcessesRepo) GetProcessList(ctx context.Context, req model.ProcessListRequest) (model.ProcessListResponse, error) {
|
||||
resp := model.ProcessListResponse{}
|
||||
if req.Limit == 0 {
|
||||
req.Limit = 10
|
||||
}
|
||||
|
||||
resp := model.ProcessListResponse{
|
||||
Type: "list",
|
||||
// default to cpu order by
|
||||
if req.OrderBy == nil {
|
||||
req.OrderBy = &v3.OrderBy{ColumnName: "cpu", Order: v3.DirectionDesc}
|
||||
}
|
||||
|
||||
step := common.MinAllowedStepInterval(req.Start, req.End)
|
||||
// default to process pid group by
|
||||
if len(req.GroupBy) == 0 {
|
||||
req.GroupBy = []v3.AttributeKey{{Key: processPIDAttrKey}}
|
||||
resp.Type = model.ResponseTypeList
|
||||
} else {
|
||||
resp.Type = model.ResponseTypeGroupedList
|
||||
}
|
||||
|
||||
step := int64(math.Max(float64(common.MinAllowedStepInterval(req.Start, req.End)), 60))
|
||||
|
||||
query := ProcessesTableListQuery.Clone()
|
||||
if req.OrderBy != nil {
|
||||
for _, q := range query.CompositeQuery.BuilderQueries {
|
||||
q.OrderBy = []v3.OrderBy{*req.OrderBy}
|
||||
}
|
||||
}
|
||||
|
||||
query.Start = req.Start
|
||||
query.End = req.End
|
||||
@@ -166,11 +239,9 @@ func (p *ProcessesRepo) GetProcessList(ctx context.Context, req model.ProcessLis
|
||||
for _, query := range query.CompositeQuery.BuilderQueries {
|
||||
query.StepInterval = step
|
||||
if req.Filters != nil && len(req.Filters.Items) > 0 {
|
||||
if query.Filters == nil {
|
||||
query.Filters = &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{}}
|
||||
}
|
||||
query.Filters.Items = append(query.Filters.Items, req.Filters.Items...)
|
||||
}
|
||||
query.GroupBy = req.GroupBy
|
||||
}
|
||||
|
||||
processAttrs, err := p.getMetadataAttributes(ctx, req)
|
||||
@@ -178,157 +249,83 @@ func (p *ProcessesRepo) GetProcessList(ctx context.Context, req model.ProcessLis
|
||||
return resp, err
|
||||
}
|
||||
|
||||
topProcessGroups, allProcessGroups, err := p.getTopProcessGroups(ctx, req, query)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
groupFilters := map[string][]string{}
|
||||
for _, topProcessGroup := range topProcessGroups {
|
||||
for k, v := range topProcessGroup {
|
||||
groupFilters[k] = append(groupFilters[k], v)
|
||||
}
|
||||
}
|
||||
|
||||
for groupKey, groupValues := range groupFilters {
|
||||
hasGroupFilter := false
|
||||
if req.Filters != nil && len(req.Filters.Items) > 0 {
|
||||
for _, filter := range req.Filters.Items {
|
||||
if filter.Key.Key == groupKey {
|
||||
hasGroupFilter = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !hasGroupFilter {
|
||||
for _, query := range query.CompositeQuery.BuilderQueries {
|
||||
query.Filters.Items = append(query.Filters.Items, v3.FilterItem{
|
||||
Key: v3.AttributeKey{Key: groupKey},
|
||||
Value: groupValues,
|
||||
Operator: v3.FilterOperatorIn,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
queryResponse, _, err := p.querierV2.QueryRange(ctx, query)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
type processTSInfo struct {
|
||||
CpuTimeSeries *v3.Series `json:"cpu_time_series"`
|
||||
MemoryTimeSeries *v3.Series `json:"memory_time_series"`
|
||||
}
|
||||
processTSInfoMap := map[string]*processTSInfo{}
|
||||
|
||||
for _, result := range queryResponse {
|
||||
for _, series := range result.Series {
|
||||
pid := series.Labels["process_pid"]
|
||||
if _, ok := processTSInfoMap[pid]; !ok {
|
||||
processTSInfoMap[pid] = &processTSInfo{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query.FormatForWeb = false
|
||||
query.CompositeQuery.PanelType = v3.PanelTypeGraph
|
||||
|
||||
formulaResult, err := postprocess.PostProcessResult(queryResponse, query)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
for _, result := range formulaResult {
|
||||
for _, series := range result.Series {
|
||||
pid := series.Labels["process_pid"]
|
||||
if _, ok := processTSInfoMap[pid]; !ok {
|
||||
processTSInfoMap[pid] = &processTSInfo{}
|
||||
}
|
||||
loadSeries := *series
|
||||
if result.QueryName == "F1" {
|
||||
processTSInfoMap[pid].CpuTimeSeries = &loadSeries
|
||||
} else if result.QueryName == "C" {
|
||||
processTSInfoMap[pid].MemoryTimeSeries = &loadSeries
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query.FormatForWeb = true
|
||||
query.CompositeQuery.PanelType = v3.PanelTypeTable
|
||||
|
||||
formattedResponse, err := postprocess.PostProcessResult(queryResponse, query)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
if len(formattedResponse) == 0 {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
records := []model.ProcessListRecord{}
|
||||
|
||||
// there should be only one result in the response
|
||||
processInfo := formattedResponse[0]
|
||||
for _, result := range formattedResponse {
|
||||
for _, row := range result.Table.Rows {
|
||||
record := model.ProcessListRecord{
|
||||
ProcessCPU: -1,
|
||||
ProcessMemory: -1,
|
||||
}
|
||||
|
||||
for _, row := range processInfo.Table.Rows {
|
||||
record := model.ProcessListRecord{
|
||||
ProcessCPU: -1,
|
||||
ProcessMemory: -1,
|
||||
}
|
||||
pid, ok := row.Data[processPIDAttrKey].(string)
|
||||
if ok {
|
||||
record.ProcessID = pid
|
||||
}
|
||||
|
||||
pid, ok := row.Data["process_pid"].(string)
|
||||
if ok {
|
||||
record.ProcessID = pid
|
||||
}
|
||||
processCPU, ok := row.Data["F1"].(float64)
|
||||
if ok {
|
||||
record.ProcessCPU = processCPU
|
||||
}
|
||||
|
||||
processCPU, ok := row.Data["F1"].(float64)
|
||||
if ok {
|
||||
record.ProcessCPU = processCPU
|
||||
processMemory, ok := row.Data["C"].(float64)
|
||||
if ok {
|
||||
record.ProcessMemory = processMemory
|
||||
}
|
||||
record.Meta = processAttrs[record.ProcessID]
|
||||
record.ProcessName = record.Meta[processNameAttrKey]
|
||||
record.ProcessCMD = record.Meta[processCMDAttrKey]
|
||||
record.ProcessCMDLine = record.Meta[processCMDLineAttrKey]
|
||||
records = append(records, record)
|
||||
}
|
||||
|
||||
processMemory, ok := row.Data["C"].(float64)
|
||||
if ok {
|
||||
record.ProcessMemory = processMemory
|
||||
}
|
||||
record.Meta = processAttrs[record.ProcessID]
|
||||
if processTSInfoMap[record.ProcessID] != nil {
|
||||
record.ProcessCPUTimeSeries = processTSInfoMap[record.ProcessID].CpuTimeSeries
|
||||
record.ProcessMemoryTimeSeries = processTSInfoMap[record.ProcessID].MemoryTimeSeries
|
||||
}
|
||||
record.ProcessName = record.Meta["process_executable_name"]
|
||||
record.ProcessCMD = record.Meta["process_command"]
|
||||
record.ProcessCMDLine = record.Meta["process_command_line"]
|
||||
records = append(records, record)
|
||||
}
|
||||
|
||||
resp.Total = len(records)
|
||||
|
||||
if req.Offset > 0 {
|
||||
records = records[req.Offset:]
|
||||
}
|
||||
if req.Limit > 0 && len(records) > req.Limit {
|
||||
records = records[:req.Limit]
|
||||
}
|
||||
resp.Total = len(allProcessGroups)
|
||||
resp.Records = records
|
||||
|
||||
if len(req.GroupBy) > 0 {
|
||||
groups := []model.ProcessListGroup{}
|
||||
|
||||
groupMap := make(map[string][]model.ProcessListRecord)
|
||||
for _, record := range records {
|
||||
groupKey := getGroupKeyForProcesses(record, req.GroupBy)
|
||||
if _, ok := groupMap[groupKey]; !ok {
|
||||
groupMap[groupKey] = []model.ProcessListRecord{record}
|
||||
} else {
|
||||
groupMap[groupKey] = append(groupMap[groupKey], record)
|
||||
}
|
||||
}
|
||||
|
||||
for _, records := range groupMap {
|
||||
var avgCPU, avgMemory float64
|
||||
var validCPU, validMemory int
|
||||
for _, record := range records {
|
||||
if !math.IsNaN(record.ProcessCPU) {
|
||||
avgCPU += record.ProcessCPU
|
||||
validCPU++
|
||||
}
|
||||
if !math.IsNaN(record.ProcessMemory) {
|
||||
avgMemory += record.ProcessMemory
|
||||
validMemory++
|
||||
}
|
||||
}
|
||||
avgCPU /= float64(validCPU)
|
||||
avgMemory /= float64(validMemory)
|
||||
|
||||
// take any record and make it as the group meta
|
||||
firstRecord := records[0]
|
||||
var groupValues []string
|
||||
for _, key := range req.GroupBy {
|
||||
groupValues = append(groupValues, firstRecord.Meta[key.Key])
|
||||
}
|
||||
processNames := []string{}
|
||||
for _, record := range records {
|
||||
processNames = append(processNames, record.ProcessName)
|
||||
}
|
||||
|
||||
groups = append(groups, model.ProcessListGroup{
|
||||
GroupValues: groupValues,
|
||||
GroupCPUAvg: avgCPU,
|
||||
GroupMemoryAvg: avgMemory,
|
||||
ProcessNames: processNames,
|
||||
})
|
||||
}
|
||||
resp.Groups = groups
|
||||
resp.Type = "grouped_list"
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
@@ -1,269 +0,0 @@
|
||||
package inframetrics
|
||||
|
||||
import v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
||||
|
||||
var K8STableListQuery = v3.QueryRangeParamsV3{
|
||||
CompositeQuery: &v3.CompositeQuery{
|
||||
BuilderQueries: map[string]*v3.BuilderQuery{
|
||||
"A": {
|
||||
QueryName: "A",
|
||||
DataSource: v3.DataSourceMetrics,
|
||||
AggregateAttribute: v3.AttributeKey{
|
||||
Key: "system_cpu_time",
|
||||
DataType: v3.AttributeKeyDataTypeFloat64,
|
||||
},
|
||||
Temporality: v3.Cumulative,
|
||||
Filters: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "state",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: v3.FilterOperatorNotEqual,
|
||||
Value: "idle",
|
||||
},
|
||||
},
|
||||
},
|
||||
GroupBy: []v3.AttributeKey{
|
||||
{
|
||||
Key: "k8s_node_name",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeResource,
|
||||
},
|
||||
{
|
||||
Key: "os_type",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeResource,
|
||||
},
|
||||
},
|
||||
Expression: "A",
|
||||
ReduceTo: v3.ReduceToOperatorAvg,
|
||||
TimeAggregation: v3.TimeAggregationRate,
|
||||
SpaceAggregation: v3.SpaceAggregationSum,
|
||||
Disabled: true,
|
||||
},
|
||||
"B": {
|
||||
QueryName: "B",
|
||||
DataSource: v3.DataSourceMetrics,
|
||||
AggregateAttribute: v3.AttributeKey{
|
||||
Key: "system_cpu_time",
|
||||
DataType: v3.AttributeKeyDataTypeFloat64,
|
||||
},
|
||||
Temporality: v3.Cumulative,
|
||||
Filters: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{},
|
||||
},
|
||||
GroupBy: []v3.AttributeKey{
|
||||
{
|
||||
Key: "k8s_node_name",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeResource,
|
||||
},
|
||||
{
|
||||
Key: "os_type",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeResource,
|
||||
},
|
||||
},
|
||||
Expression: "B",
|
||||
ReduceTo: v3.ReduceToOperatorAvg,
|
||||
TimeAggregation: v3.TimeAggregationRate,
|
||||
SpaceAggregation: v3.SpaceAggregationSum,
|
||||
Disabled: true,
|
||||
},
|
||||
"F1": {
|
||||
QueryName: "F1",
|
||||
Expression: "A/B",
|
||||
Legend: "CPU Usage (%)",
|
||||
},
|
||||
"C": {
|
||||
QueryName: "C",
|
||||
DataSource: v3.DataSourceMetrics,
|
||||
AggregateAttribute: v3.AttributeKey{
|
||||
Key: "system_memory_usage",
|
||||
DataType: v3.AttributeKeyDataTypeFloat64,
|
||||
},
|
||||
Temporality: v3.Cumulative,
|
||||
Filters: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "state",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: v3.FilterOperatorIn,
|
||||
Value: []string{"used", "cached"},
|
||||
},
|
||||
},
|
||||
},
|
||||
GroupBy: []v3.AttributeKey{
|
||||
{
|
||||
Key: "k8s_node_name",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeResource,
|
||||
},
|
||||
{
|
||||
Key: "os_type",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeResource,
|
||||
},
|
||||
},
|
||||
Expression: "C",
|
||||
ReduceTo: v3.ReduceToOperatorAvg,
|
||||
TimeAggregation: v3.TimeAggregationAvg,
|
||||
SpaceAggregation: v3.SpaceAggregationSum,
|
||||
Disabled: true,
|
||||
},
|
||||
"D": {
|
||||
QueryName: "D",
|
||||
DataSource: v3.DataSourceMetrics,
|
||||
AggregateAttribute: v3.AttributeKey{
|
||||
Key: "system_memory_usage",
|
||||
DataType: v3.AttributeKeyDataTypeFloat64,
|
||||
},
|
||||
Temporality: v3.Cumulative,
|
||||
Filters: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{},
|
||||
},
|
||||
GroupBy: []v3.AttributeKey{
|
||||
{
|
||||
Key: "k8s_node_name",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeResource,
|
||||
},
|
||||
{
|
||||
Key: "os_type",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeResource,
|
||||
},
|
||||
},
|
||||
Expression: "D",
|
||||
ReduceTo: v3.ReduceToOperatorAvg,
|
||||
TimeAggregation: v3.TimeAggregationAvg,
|
||||
SpaceAggregation: v3.SpaceAggregationSum,
|
||||
Disabled: true,
|
||||
},
|
||||
"F2": {
|
||||
QueryName: "F2",
|
||||
Expression: "C/D",
|
||||
Legend: "Memory Usage (%)",
|
||||
},
|
||||
"E": {
|
||||
QueryName: "E",
|
||||
DataSource: v3.DataSourceMetrics,
|
||||
AggregateAttribute: v3.AttributeKey{
|
||||
Key: "system_cpu_time",
|
||||
DataType: v3.AttributeKeyDataTypeFloat64,
|
||||
},
|
||||
Temporality: v3.Cumulative,
|
||||
Filters: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "state",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: v3.FilterOperatorEqual,
|
||||
Value: "wait",
|
||||
},
|
||||
},
|
||||
},
|
||||
GroupBy: []v3.AttributeKey{
|
||||
{
|
||||
Key: "k8s_node_name",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeResource,
|
||||
},
|
||||
{
|
||||
Key: "os_type",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeResource,
|
||||
},
|
||||
},
|
||||
Expression: "E",
|
||||
ReduceTo: v3.ReduceToOperatorAvg,
|
||||
TimeAggregation: v3.TimeAggregationRate,
|
||||
SpaceAggregation: v3.SpaceAggregationSum,
|
||||
Disabled: true,
|
||||
},
|
||||
"F": {
|
||||
QueryName: "F",
|
||||
DataSource: v3.DataSourceMetrics,
|
||||
AggregateAttribute: v3.AttributeKey{
|
||||
Key: "system_cpu_time",
|
||||
DataType: v3.AttributeKeyDataTypeFloat64,
|
||||
},
|
||||
Temporality: v3.Cumulative,
|
||||
Filters: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{},
|
||||
},
|
||||
GroupBy: []v3.AttributeKey{
|
||||
{
|
||||
Key: "k8s_node_name",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeResource,
|
||||
},
|
||||
{
|
||||
Key: "os_type",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeResource,
|
||||
},
|
||||
},
|
||||
Expression: "F",
|
||||
ReduceTo: v3.ReduceToOperatorAvg,
|
||||
TimeAggregation: v3.TimeAggregationRate,
|
||||
SpaceAggregation: v3.SpaceAggregationSum,
|
||||
Disabled: true,
|
||||
},
|
||||
"F3": {
|
||||
QueryName: "F3",
|
||||
Expression: "E/F",
|
||||
Legend: "CPU Wait Time (%)",
|
||||
},
|
||||
"G": {
|
||||
QueryName: "G",
|
||||
DataSource: v3.DataSourceMetrics,
|
||||
AggregateAttribute: v3.AttributeKey{
|
||||
Key: "system_cpu_load_average_15m",
|
||||
DataType: v3.AttributeKeyDataTypeFloat64,
|
||||
},
|
||||
Temporality: v3.Unspecified,
|
||||
Filters: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{},
|
||||
},
|
||||
GroupBy: []v3.AttributeKey{
|
||||
{
|
||||
Key: "k8s_node_name",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeResource,
|
||||
},
|
||||
{
|
||||
Key: "os_type",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeResource,
|
||||
},
|
||||
},
|
||||
Expression: "G",
|
||||
ReduceTo: v3.ReduceToOperatorAvg,
|
||||
TimeAggregation: v3.TimeAggregationAvg,
|
||||
SpaceAggregation: v3.SpaceAggregationSum,
|
||||
Legend: "CPU Load Average (15m)",
|
||||
},
|
||||
},
|
||||
PanelType: v3.PanelTypeTable,
|
||||
QueryType: v3.QueryTypeBuilder,
|
||||
},
|
||||
Version: "v4",
|
||||
FormatForWeb: true,
|
||||
}
|
||||
@@ -284,7 +284,7 @@ func BuildQRParamsWithCache(messagingQueue *MessagingQueue, queryContext string,
|
||||
cq = &v3.CompositeQuery{
|
||||
QueryType: v3.QueryTypeBuilder,
|
||||
BuilderQueries: bhq,
|
||||
PanelType: v3.PanelTypeTable,
|
||||
PanelType: v3.PanelTypeList,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,7 +364,7 @@ func BuildClickHouseQuery(messagingQueue *MessagingQueue, queueType string, quer
|
||||
|
||||
func buildCompositeQuery(chq *v3.ClickHouseQuery, queryContext string) (*v3.CompositeQuery, error) {
|
||||
|
||||
if queryContext == "producer-consumer-eva" {
|
||||
if queryContext == "producer-consumer-eval" || queryContext == "producer-throughput-overview" {
|
||||
return &v3.CompositeQuery{
|
||||
QueryType: v3.QueryTypeClickHouseSQL,
|
||||
ClickHouseQueries: map[string]*v3.ClickHouseQuery{queryContext: chq},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user