mirror of
https://github.com/SigNoz/signoz.git
synced 2026-06-20 15:20:31 +01:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85d7075d3b | ||
|
|
ffd72cf406 | ||
|
|
6dfea14219 | ||
|
|
f2be856f63 | ||
|
|
f04589a0b2 | ||
|
|
1378590429 | ||
|
|
88084af4d4 | ||
|
|
d0eefa0cf2 | ||
|
|
cc9eb32c50 | ||
|
|
c648b72ace | ||
|
|
a449698dfe |
19
.github/workflows/pr_verify_linked_issue.yml
vendored
19
.github/workflows/pr_verify_linked_issue.yml
vendored
@@ -1,19 +0,0 @@
|
||||
# This workflow will inspect a pull request to ensure there is a linked issue or a
|
||||
# valid issue is mentioned in the body. If neither is present it fails the check and adds
|
||||
# a comment alerting users of this missing requirement.
|
||||
name: VerifyIssue
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [edited, opened]
|
||||
check_run:
|
||||
|
||||
jobs:
|
||||
verify_linked_issue:
|
||||
runs-on: ubuntu-latest
|
||||
name: Ensure Pull Request has a linked issue.
|
||||
steps:
|
||||
- name: Verify Linked Issue
|
||||
uses: srikanthccv/verify-linked-issue-action@v0.71
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -117,13 +117,6 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *baseapp.AuthMiddlew
|
||||
// note: add ee override methods first
|
||||
|
||||
// routes available only in ee version
|
||||
router.HandleFunc("/api/v1/licenses",
|
||||
am.AdminAccess(ah.listLicenses)).
|
||||
Methods(http.MethodGet)
|
||||
|
||||
router.HandleFunc("/api/v1/licenses",
|
||||
am.AdminAccess(ah.applyLicense)).
|
||||
Methods(http.MethodPost)
|
||||
|
||||
router.HandleFunc("/api/v1/featureFlags",
|
||||
am.OpenAccess(ah.getFeatureFlags)).
|
||||
@@ -178,11 +171,6 @@ 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)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -64,55 +63,8 @@ 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 {
|
||||
RespondError(w, apiError, nil)
|
||||
}
|
||||
ah.Respond(w, licenses)
|
||||
}
|
||||
|
||||
func (ah *APIHandler) applyLicense(w http.ResponseWriter, r *http.Request) {
|
||||
var l model.License
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&l); err != nil {
|
||||
RespondError(w, model.BadRequest(err), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if l.Key == "" {
|
||||
RespondError(w, model.BadRequest(fmt.Errorf("license key is required")), nil)
|
||||
return
|
||||
}
|
||||
license, apiError := ah.LM().ActivateV3(r.Context(), l.Key)
|
||||
if apiError != nil {
|
||||
RespondError(w, apiError, nil)
|
||||
return
|
||||
}
|
||||
|
||||
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))
|
||||
ah.listLicensesV2(w, r)
|
||||
}
|
||||
|
||||
func (ah *APIHandler) getActiveLicenseV3(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -121,6 +73,7 @@ func (ah *APIHandler) getActiveLicenseV3(w http.ResponseWriter, r *http.Request)
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// return 404 not found if there is no active license
|
||||
if activeLicense == nil {
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("no active license found")}, nil)
|
||||
|
||||
@@ -32,19 +32,6 @@ func (r *Repo) InitDB(inputDB *sqlx.DB) error {
|
||||
return sqlite.InitDB(inputDB)
|
||||
}
|
||||
|
||||
func (r *Repo) GetLicenses(ctx context.Context) ([]model.License, error) {
|
||||
licenses := []model.License{}
|
||||
|
||||
query := "SELECT key, activationId, planDetails, validationMessage FROM licenses"
|
||||
|
||||
err := r.db.Select(&licenses, query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get licenses from db: %v", err)
|
||||
}
|
||||
|
||||
return licenses, nil
|
||||
}
|
||||
|
||||
func (r *Repo) GetLicensesV3(ctx context.Context) ([]*model.LicenseV3, error) {
|
||||
licensesData := []model.LicenseDB{}
|
||||
licenseV3Data := []*model.LicenseV3{}
|
||||
@@ -73,35 +60,6 @@ func (r *Repo) GetLicensesV3(ctx context.Context) ([]*model.LicenseV3, error) {
|
||||
return licenseV3Data, nil
|
||||
}
|
||||
|
||||
func (r *Repo) GetActiveLicenseV2(ctx context.Context) (*model.License, *basemodel.ApiError) {
|
||||
var err error
|
||||
licenses := []model.License{}
|
||||
|
||||
query := "SELECT key, activationId, planDetails, validationMessage FROM licenses"
|
||||
|
||||
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.License
|
||||
for _, l := range licenses {
|
||||
l.ParsePlan()
|
||||
if active == nil &&
|
||||
(l.ValidFrom != 0) &&
|
||||
(l.ValidUntil == -1 || l.ValidUntil > time.Now().Unix()) {
|
||||
active = &l
|
||||
}
|
||||
if active != nil &&
|
||||
l.ValidFrom > active.ValidFrom &&
|
||||
(l.ValidUntil == -1 || l.ValidUntil > time.Now().Unix()) {
|
||||
active = &l
|
||||
}
|
||||
}
|
||||
|
||||
return active, 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) {
|
||||
@@ -156,50 +114,56 @@ func (r *Repo) GetActiveLicenseV3(ctx context.Context) (*model.LicenseV3, error)
|
||||
return active, nil
|
||||
}
|
||||
|
||||
// InsertLicense inserts a new license in db
|
||||
func (r *Repo) InsertLicense(ctx context.Context, l *model.License) error {
|
||||
// InsertLicenseV3 inserts a new license v3 in db
|
||||
func (r *Repo) InsertLicenseV3(ctx context.Context, l *model.LicenseV3) *model.ApiError {
|
||||
|
||||
if l.Key == "" {
|
||||
return fmt.Errorf("insert license failed: license key is required")
|
||||
query := `INSERT INTO licenses_v3 (id, key, data) VALUES ($1, $2, $3)`
|
||||
|
||||
// licsense is the entity of zeus so putting the entire license here without defining schema
|
||||
licenseData, err := json.Marshal(l.Data)
|
||||
if err != nil {
|
||||
return &model.ApiError{Typ: basemodel.ErrorBadData, Err: err}
|
||||
}
|
||||
|
||||
query := `INSERT INTO licenses
|
||||
(key, planDetails, activationId, validationmessage)
|
||||
VALUES ($1, $2, $3, $4)`
|
||||
|
||||
_, err := r.db.ExecContext(ctx,
|
||||
_, err = r.db.ExecContext(ctx,
|
||||
query,
|
||||
l.ID,
|
||||
l.Key,
|
||||
l.PlanDetails,
|
||||
l.ActivationId,
|
||||
l.ValidationMessage)
|
||||
string(licenseData),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
if sqliteErr, ok := err.(sqlite3.Error); ok {
|
||||
if sqliteErr.ExtendedCode == sqlite3.ErrConstraintUnique {
|
||||
zap.L().Error("error in inserting license data: ", zap.Error(sqliteErr))
|
||||
return &model.ApiError{Typ: model.ErrorConflict, Err: sqliteErr}
|
||||
}
|
||||
}
|
||||
zap.L().Error("error in inserting license data: ", zap.Error(err))
|
||||
return fmt.Errorf("failed to insert license in db: %v", err)
|
||||
return &model.ApiError{Typ: basemodel.ErrorExec, Err: err}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdatePlanDetails writes new plan details to the db
|
||||
func (r *Repo) UpdatePlanDetails(ctx context.Context,
|
||||
key,
|
||||
planDetails string) error {
|
||||
// UpdateLicenseV3 updates a new license v3 in db
|
||||
func (r *Repo) UpdateLicenseV3(ctx context.Context, l *model.LicenseV3) error {
|
||||
|
||||
if key == "" {
|
||||
return fmt.Errorf("update plan details failed: license key is required")
|
||||
// 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")
|
||||
}
|
||||
|
||||
query := `UPDATE licenses
|
||||
SET planDetails = $1,
|
||||
updatedAt = $2
|
||||
WHERE key = $3`
|
||||
|
||||
_, err := r.db.ExecContext(ctx, query, planDetails, time.Now(), key)
|
||||
_, err = r.db.ExecContext(ctx,
|
||||
query,
|
||||
license,
|
||||
l.ID,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
zap.L().Error("error in updating license: ", zap.Error(err))
|
||||
zap.L().Error("error in updating license data: ", zap.Error(err))
|
||||
return fmt.Errorf("failed to update license in db: %v", err)
|
||||
}
|
||||
|
||||
@@ -281,59 +245,3 @@ func (r *Repo) InitFeatures(req basemodel.FeatureSet) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// InsertLicenseV3 inserts a new license v3 in db
|
||||
func (r *Repo) InsertLicenseV3(ctx context.Context, l *model.LicenseV3) *model.ApiError {
|
||||
|
||||
query := `INSERT INTO licenses_v3 (id, key, data) VALUES ($1, $2, $3)`
|
||||
|
||||
// licsense is the entity of zeus so putting the entire license here without defining schema
|
||||
licenseData, err := json.Marshal(l.Data)
|
||||
if err != nil {
|
||||
return &model.ApiError{Typ: basemodel.ErrorBadData, Err: err}
|
||||
}
|
||||
|
||||
_, err = r.db.ExecContext(ctx,
|
||||
query,
|
||||
l.ID,
|
||||
l.Key,
|
||||
string(licenseData),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
if sqliteErr, ok := err.(sqlite3.Error); ok {
|
||||
if sqliteErr.ExtendedCode == sqlite3.ErrConstraintUnique {
|
||||
zap.L().Error("error in inserting license data: ", zap.Error(sqliteErr))
|
||||
return &model.ApiError{Typ: model.ErrorConflict, Err: sqliteErr}
|
||||
}
|
||||
}
|
||||
zap.L().Error("error in inserting license data: ", zap.Error(err))
|
||||
return &model.ApiError{Typ: basemodel.ErrorExec, Err: err}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateLicenseV3 updates a new license v3 in db
|
||||
func (r *Repo) UpdateLicenseV3(ctx context.Context, l *model.LicenseV3) error {
|
||||
|
||||
// the key and id for the license can't change so only update the data here!
|
||||
query := `UPDATE licenses_v3 SET data=$1 WHERE id=$2;`
|
||||
|
||||
license, err := json.Marshal(l.Data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("insert license failed: license marshal error")
|
||||
}
|
||||
_, err = r.db.ExecContext(ctx,
|
||||
query,
|
||||
license,
|
||||
l.ID,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
zap.L().Error("error in updating license data: ", zap.Error(err))
|
||||
return fmt.Errorf("failed to update license in db: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -27,26 +27,19 @@ var LM *Manager
|
||||
var validationFrequency = 24 * 60 * time.Minute
|
||||
|
||||
type Manager struct {
|
||||
repo *Repo
|
||||
mutex sync.Mutex
|
||||
|
||||
repo *Repo
|
||||
mutex sync.Mutex
|
||||
validatorRunning bool
|
||||
|
||||
// end the license validation, this is important to gracefully
|
||||
// stopping validation and protect in-consistent updates
|
||||
done chan struct{}
|
||||
|
||||
// terminated waits for the validate go routine to end
|
||||
terminated chan struct{}
|
||||
|
||||
// last time the license was validated
|
||||
lastValidated int64
|
||||
|
||||
// keep track of validation failure attempts
|
||||
failedAttempts uint64
|
||||
|
||||
// keep track of active license and features
|
||||
activeLicense *model.License
|
||||
activeLicenseV3 *model.LicenseV3
|
||||
activeFeatures basemodel.FeatureSet
|
||||
}
|
||||
@@ -58,7 +51,6 @@ func StartManager(db *sqlx.DB, features ...basemodel.Feature) (*Manager, error)
|
||||
|
||||
repo := NewLicenseRepo(db)
|
||||
err := repo.InitDB(db)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initiate license repo: %v", err)
|
||||
}
|
||||
@@ -66,10 +58,10 @@ func StartManager(db *sqlx.DB, features ...basemodel.Feature) (*Manager, error)
|
||||
m := &Manager{
|
||||
repo: &repo,
|
||||
}
|
||||
|
||||
if err := m.start(features...); err != nil {
|
||||
return m, err
|
||||
}
|
||||
|
||||
LM = m
|
||||
return m, nil
|
||||
}
|
||||
@@ -119,6 +111,7 @@ func (lm *Manager) LoadActiveLicenseV3(features ...basemodel.Feature) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if active != nil {
|
||||
lm.SetActiveV3(active, features...)
|
||||
} else {
|
||||
@@ -136,32 +129,6 @@ func (lm *Manager) LoadActiveLicenseV3(features ...basemodel.Feature) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (lm *Manager) GetLicenses(ctx context.Context) (response []model.License, apiError *model.ApiError) {
|
||||
|
||||
licenses, err := lm.repo.GetLicenses(ctx)
|
||||
if err != nil {
|
||||
return nil, model.InternalError(err)
|
||||
}
|
||||
|
||||
for _, l := range licenses {
|
||||
l.ParsePlan()
|
||||
|
||||
if lm.activeLicense != nil && l.Key == lm.activeLicense.Key {
|
||||
l.IsCurrent = true
|
||||
}
|
||||
|
||||
if l.ValidUntil == -1 {
|
||||
// for subscriptions, there is no end-date as such
|
||||
// but for showing user some validity we default one year timespan
|
||||
l.ValidUntil = l.ValidFrom + 31556926
|
||||
}
|
||||
|
||||
response = append(response, l)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (lm *Manager) GetLicensesV3(ctx context.Context) (response []*model.LicenseV3, apiError *model.ApiError) {
|
||||
|
||||
licenses, err := lm.repo.GetLicensesV3(ctx)
|
||||
@@ -188,11 +155,11 @@ func (lm *Manager) GetLicensesV3(ctx context.Context) (response []*model.License
|
||||
func (lm *Manager) ValidatorV3(ctx context.Context) {
|
||||
zap.L().Info("ValidatorV3 started!")
|
||||
defer close(lm.terminated)
|
||||
|
||||
tick := time.NewTicker(validationFrequency)
|
||||
defer tick.Stop()
|
||||
|
||||
lm.ValidateV3(ctx)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-lm.done:
|
||||
@@ -238,10 +205,27 @@ func (lm *Manager) ValidateV3(ctx context.Context) (reterr error) {
|
||||
lm.lastValidated = time.Now().Unix()
|
||||
if reterr != nil {
|
||||
zap.L().Error("License validation completed with error", zap.Error(reterr))
|
||||
|
||||
atomic.AddUint64(&lm.failedAttempts, 1)
|
||||
// default to basic plan if validation fails for three consecutive times
|
||||
if atomic.LoadUint64(&lm.failedAttempts) > 3 {
|
||||
zap.L().Error("License validation completed with error for three consecutive times, defaulting to basic plan", zap.String("license_id", lm.activeLicenseV3.ID), zap.Bool("license_validation", false))
|
||||
lm.activeLicenseV3 = nil
|
||||
lm.activeFeatures = model.BasicPlan
|
||||
setDefaultFeatures(lm)
|
||||
err := lm.InitFeatures(lm.activeFeatures)
|
||||
if err != nil {
|
||||
zap.L().Error("Couldn't initialize features", zap.Error(err))
|
||||
}
|
||||
lm.done <- struct{}{}
|
||||
lm.validatorRunning = false
|
||||
}
|
||||
|
||||
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_LICENSE_CHECK_FAILED,
|
||||
map[string]interface{}{"err": reterr.Error()}, "", true, false)
|
||||
} else {
|
||||
// reset the failed attempts counter
|
||||
atomic.StoreUint64(&lm.failedAttempts, 0)
|
||||
zap.L().Info("License validation completed with no errors")
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
@@ -61,37 +60,6 @@ type LicensePlan struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
func (l *License) ParsePlan() error {
|
||||
l.LicensePlan = LicensePlan{}
|
||||
|
||||
planData, err := base64.StdEncoding.DecodeString(l.PlanDetails)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
plan := LicensePlan{}
|
||||
err = json.Unmarshal([]byte(planData), &plan)
|
||||
if err != nil {
|
||||
l.ValidationMessage = "failed to parse plan from license"
|
||||
return errors.Wrap(err, "failed to parse plan from license")
|
||||
}
|
||||
|
||||
l.LicensePlan = plan
|
||||
l.ParseFeatures()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *License) ParseFeatures() {
|
||||
switch l.PlanKey {
|
||||
case Pro:
|
||||
l.FeatureSet = ProPlan
|
||||
case Enterprise:
|
||||
l.FeatureSet = EnterprisePlan
|
||||
default:
|
||||
l.FeatureSet = BasicPlan
|
||||
}
|
||||
}
|
||||
|
||||
type Licenses struct {
|
||||
TrialStart int64 `json:"trialStart"`
|
||||
TrialEnd int64 `json:"trialEnd"`
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import axios from 'api';
|
||||
import { ApiV3Instance as axios } from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiV2Instance as axios } from 'api';
|
||||
import { ApiV3Instance as axios } from 'api';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps } from 'types/api/licenses/getAll';
|
||||
|
||||
|
||||
@@ -259,7 +259,7 @@ function AnomalyAlertEvaluationView({
|
||||
grid: {
|
||||
show: true,
|
||||
},
|
||||
axes: getAxes(isDarkMode, yAxisUnit),
|
||||
axes: getAxes({ isDarkMode, yAxisUnit }),
|
||||
tzDate: (timestamp: number): Date =>
|
||||
uPlot.tzDate(new Date(timestamp * 1e3), timezone.value),
|
||||
};
|
||||
|
||||
@@ -122,7 +122,7 @@ export function BillingUsageGraph(props: BillingUsageGraphProps): JSX.Element {
|
||||
[graphCompatibleData.data.result],
|
||||
);
|
||||
|
||||
const axesOptions = getAxes(isDarkMode, '');
|
||||
const axesOptions = getAxes({ isDarkMode, yAxisUnit: '' });
|
||||
|
||||
const optionsForChart: uPlot.Options = useMemo(
|
||||
() => ({
|
||||
|
||||
@@ -1,34 +1,26 @@
|
||||
import './InfraMonitoring.styles.scss';
|
||||
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Skeleton,
|
||||
Spin,
|
||||
Table,
|
||||
TablePaginationConfig,
|
||||
TableProps,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { SorterResult } from 'antd/es/table/interface';
|
||||
import { VerticalAlignTopOutlined } from '@ant-design/icons';
|
||||
import { Button, Tooltip, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { HostListPayload } from 'api/infraMonitoring/getHostLists';
|
||||
import HostMetricDetail from 'components/HostMetricsDetail';
|
||||
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
import { QuickFiltersSource } from 'components/QuickFilters/types';
|
||||
import { usePageSize } from 'container/InfraMonitoringK8s/utils';
|
||||
import { useGetHostList } from 'hooks/infraMonitoring/useGetHostList';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import { Filter } from 'lucide-react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import HostsEmptyOrIncorrectMetrics from './HostsEmptyOrIncorrectMetrics';
|
||||
import HostsListControls from './HostsListControls';
|
||||
import {
|
||||
formatDataForTable,
|
||||
getHostListsQuery,
|
||||
getHostsListColumns,
|
||||
HostRowData,
|
||||
} from './utils';
|
||||
import HostsListTable from './HostsListTable';
|
||||
import { getHostListsQuery, HostsQuickFiltersConfig } from './utils';
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
function HostsList(): JSX.Element {
|
||||
@@ -41,6 +33,7 @@ function HostsList(): JSX.Element {
|
||||
items: [],
|
||||
op: 'and',
|
||||
});
|
||||
const [showFilters, setShowFilters] = useState<boolean>(true);
|
||||
|
||||
const [orderBy, setOrderBy] = useState<{
|
||||
columnName: string;
|
||||
@@ -72,55 +65,24 @@ function HostsList(): JSX.Element {
|
||||
},
|
||||
);
|
||||
|
||||
const sentAnyHostMetricsData = useMemo(
|
||||
() => data?.payload?.data?.sentAnyHostMetricsData || false,
|
||||
[data],
|
||||
);
|
||||
|
||||
const isSendingIncorrectK8SAgentMetrics = useMemo(
|
||||
() => data?.payload?.data?.isSendingK8SAgentMetrics || false,
|
||||
[data],
|
||||
);
|
||||
|
||||
const hostMetricsData = useMemo(() => data?.payload?.data?.records || [], [
|
||||
data,
|
||||
]);
|
||||
const totalCount = data?.payload?.data?.total || 0;
|
||||
|
||||
const formattedHostMetricsData = useMemo(
|
||||
() => formatDataForTable(hostMetricsData),
|
||||
[hostMetricsData],
|
||||
);
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
const columns = useMemo(() => getHostsListColumns(), []);
|
||||
|
||||
const handleTableChange: TableProps<HostRowData>['onChange'] = useCallback(
|
||||
(
|
||||
pagination: TablePaginationConfig,
|
||||
_filters: Record<string, (string | number | boolean)[] | null>,
|
||||
sorter: SorterResult<HostRowData> | SorterResult<HostRowData>[],
|
||||
): void => {
|
||||
if (pagination.current) {
|
||||
setCurrentPage(pagination.current);
|
||||
}
|
||||
|
||||
if ('field' in sorter && sorter.order) {
|
||||
setOrderBy({
|
||||
columnName: sorter.field as string,
|
||||
order: sorter.order === 'ascend' ? 'asc' : 'desc',
|
||||
});
|
||||
} else {
|
||||
setOrderBy(null);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
const { handleChangeQueryData } = useQueryOperations({
|
||||
index: 0,
|
||||
query: currentQuery.builder.queryData[0],
|
||||
entityVersion: '',
|
||||
});
|
||||
|
||||
const handleFiltersChange = useCallback(
|
||||
(value: IBuilderQuery['filters']): void => {
|
||||
const isNewFilterAdded = value.items.length !== filters.items.length;
|
||||
setFilters(value);
|
||||
handleChangeQueryData('filters', value);
|
||||
if (isNewFilterAdded) {
|
||||
setFilters(value);
|
||||
setCurrentPage(1);
|
||||
|
||||
logEvent('Infra Monitoring: Hosts list filters applied', {
|
||||
@@ -128,6 +90,7 @@ function HostsList(): JSX.Element {
|
||||
});
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[filters],
|
||||
);
|
||||
|
||||
@@ -142,118 +105,73 @@ function HostsList(): JSX.Element {
|
||||
);
|
||||
}, [selectedHostName, hostMetricsData]);
|
||||
|
||||
const handleRowClick = (record: HostRowData): void => {
|
||||
setSelectedHostName(record.hostName);
|
||||
|
||||
logEvent('Infra Monitoring: Hosts list item clicked', {
|
||||
host: record.hostName,
|
||||
});
|
||||
};
|
||||
|
||||
const handleCloseHostDetail = (): void => {
|
||||
setSelectedHostName(null);
|
||||
};
|
||||
|
||||
const showHostsTable =
|
||||
!isError &&
|
||||
sentAnyHostMetricsData &&
|
||||
!isSendingIncorrectK8SAgentMetrics &&
|
||||
!(formattedHostMetricsData.length === 0 && filters.items.length > 0);
|
||||
const handleFilterVisibilityChange = (): void => {
|
||||
setShowFilters(!showFilters);
|
||||
};
|
||||
|
||||
const showNoFilteredHostsMessage =
|
||||
!isFetching &&
|
||||
!isLoading &&
|
||||
formattedHostMetricsData.length === 0 &&
|
||||
filters.items.length > 0;
|
||||
|
||||
const showHostsEmptyState =
|
||||
!isFetching &&
|
||||
!isLoading &&
|
||||
(!sentAnyHostMetricsData || isSendingIncorrectK8SAgentMetrics) &&
|
||||
!filters.items.length;
|
||||
const handleQuickFiltersChange = (query: Query): void => {
|
||||
handleChangeQueryData('filters', query.builder.queryData[0].filters);
|
||||
handleFiltersChange(query.builder.queryData[0].filters);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="hosts-list">
|
||||
<HostsListControls handleFiltersChange={handleFiltersChange} />
|
||||
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
|
||||
|
||||
{showHostsEmptyState && (
|
||||
<HostsEmptyOrIncorrectMetrics
|
||||
noData={!sentAnyHostMetricsData}
|
||||
incorrectData={isSendingIncorrectK8SAgentMetrics}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showNoFilteredHostsMessage && (
|
||||
<div className="no-filtered-hosts-message-container">
|
||||
<div className="no-filtered-hosts-message-content">
|
||||
<img
|
||||
src="/Icons/emptyState.svg"
|
||||
alt="thinking-emoji"
|
||||
className="empty-state-svg"
|
||||
<div className="hosts-list-content">
|
||||
{showFilters && (
|
||||
<div className="hosts-quick-filters-container">
|
||||
<div className="hosts-quick-filters-container-header">
|
||||
<Typography.Text>Filters</Typography.Text>
|
||||
<Tooltip title="Collapse Filters">
|
||||
<VerticalAlignTopOutlined
|
||||
rotate={270}
|
||||
onClick={handleFilterVisibilityChange}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<QuickFilters
|
||||
source={QuickFiltersSource.INFRA_MONITORING}
|
||||
config={HostsQuickFiltersConfig}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
onFilterChange={handleQuickFiltersChange}
|
||||
/>
|
||||
|
||||
<Typography.Text className="no-filtered-hosts-message">
|
||||
This query had no results. Edit your query and try again!
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(isFetching || isLoading) && (
|
||||
<div className="hosts-list-loading-state">
|
||||
<Skeleton.Input
|
||||
className="hosts-list-loading-state-item"
|
||||
size="large"
|
||||
block
|
||||
active
|
||||
/>
|
||||
<Skeleton.Input
|
||||
className="hosts-list-loading-state-item"
|
||||
size="large"
|
||||
block
|
||||
active
|
||||
/>
|
||||
<Skeleton.Input
|
||||
className="hosts-list-loading-state-item"
|
||||
size="large"
|
||||
block
|
||||
active
|
||||
)}
|
||||
<div className="hosts-list-table-container">
|
||||
<div className="hosts-list-table-header">
|
||||
{!showFilters && (
|
||||
<div className="quick-filters-toggle-container">
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
type="text"
|
||||
size="small"
|
||||
onClick={handleFilterVisibilityChange}
|
||||
>
|
||||
<Filter size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<HostsListControls handleFiltersChange={handleFiltersChange} />
|
||||
</div>
|
||||
<HostsListTable
|
||||
isLoading={isLoading}
|
||||
isFetching={isFetching}
|
||||
isError={isError}
|
||||
tableData={data}
|
||||
hostMetricsData={hostMetricsData}
|
||||
filters={filters}
|
||||
currentPage={currentPage}
|
||||
setCurrentPage={setCurrentPage}
|
||||
setSelectedHostName={setSelectedHostName}
|
||||
pageSize={pageSize}
|
||||
setPageSize={setPageSize}
|
||||
setOrderBy={setOrderBy}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showHostsTable && (
|
||||
<Table
|
||||
className="hosts-list-table"
|
||||
dataSource={isFetching || isLoading ? [] : formattedHostMetricsData}
|
||||
columns={columns}
|
||||
pagination={{
|
||||
current: currentPage,
|
||||
pageSize,
|
||||
total: totalCount,
|
||||
showSizeChanger: true,
|
||||
hideOnSinglePage: false,
|
||||
onChange: (page, pageSize): void => {
|
||||
setCurrentPage(page);
|
||||
setPageSize(pageSize);
|
||||
},
|
||||
}}
|
||||
scroll={{ x: true }}
|
||||
loading={{
|
||||
spinning: isFetching || isLoading,
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
tableLayout="fixed"
|
||||
rowKey={(record): string => record.hostName}
|
||||
onChange={handleTableChange}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
|
||||
</div>
|
||||
<HostMetricDetail
|
||||
host={selectedHostData}
|
||||
isModalTimeSelection
|
||||
|
||||
183
frontend/src/container/InfraMonitoringHosts/HostsListTable.tsx
Normal file
183
frontend/src/container/InfraMonitoringHosts/HostsListTable.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Skeleton,
|
||||
Spin,
|
||||
Table,
|
||||
TablePaginationConfig,
|
||||
TableProps,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { SorterResult } from 'antd/es/table/interface';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import HostsEmptyOrIncorrectMetrics from './HostsEmptyOrIncorrectMetrics';
|
||||
import {
|
||||
formatDataForTable,
|
||||
getHostsListColumns,
|
||||
HostRowData,
|
||||
HostsListTableProps,
|
||||
} from './utils';
|
||||
|
||||
export default function HostsListTable({
|
||||
isLoading,
|
||||
isFetching,
|
||||
isError,
|
||||
tableData: data,
|
||||
hostMetricsData,
|
||||
filters,
|
||||
setSelectedHostName,
|
||||
currentPage,
|
||||
setCurrentPage,
|
||||
pageSize,
|
||||
setOrderBy,
|
||||
setPageSize,
|
||||
}: HostsListTableProps): JSX.Element {
|
||||
const columns = useMemo(() => getHostsListColumns(), []);
|
||||
|
||||
const sentAnyHostMetricsData = useMemo(
|
||||
() => data?.payload?.data?.sentAnyHostMetricsData || false,
|
||||
[data],
|
||||
);
|
||||
|
||||
const isSendingIncorrectK8SAgentMetrics = useMemo(
|
||||
() => data?.payload?.data?.isSendingK8SAgentMetrics || false,
|
||||
[data],
|
||||
);
|
||||
|
||||
const formattedHostMetricsData = useMemo(
|
||||
() => formatDataForTable(hostMetricsData),
|
||||
[hostMetricsData],
|
||||
);
|
||||
|
||||
const totalCount = data?.payload?.data?.total || 0;
|
||||
|
||||
const handleTableChange: TableProps<HostRowData>['onChange'] = useCallback(
|
||||
(
|
||||
pagination: TablePaginationConfig,
|
||||
_filters: Record<string, (string | number | boolean)[] | null>,
|
||||
sorter: SorterResult<HostRowData> | SorterResult<HostRowData>[],
|
||||
): void => {
|
||||
if (pagination.current) {
|
||||
setCurrentPage(pagination.current);
|
||||
}
|
||||
|
||||
if ('field' in sorter && sorter.order) {
|
||||
setOrderBy({
|
||||
columnName: sorter.field as string,
|
||||
order: sorter.order === 'ascend' ? 'asc' : 'desc',
|
||||
});
|
||||
} else {
|
||||
setOrderBy(null);
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
const handleRowClick = (record: HostRowData): void => {
|
||||
setSelectedHostName(record.hostName);
|
||||
logEvent('Infra Monitoring: Hosts list item clicked', {
|
||||
host: record.hostName,
|
||||
});
|
||||
};
|
||||
|
||||
const showNoFilteredHostsMessage =
|
||||
!isFetching &&
|
||||
!isLoading &&
|
||||
formattedHostMetricsData.length === 0 &&
|
||||
filters.items.length > 0;
|
||||
|
||||
const showHostsEmptyState =
|
||||
!isFetching &&
|
||||
!isLoading &&
|
||||
(!sentAnyHostMetricsData || isSendingIncorrectK8SAgentMetrics) &&
|
||||
!filters.items.length;
|
||||
|
||||
if (isError) {
|
||||
return <Typography>{data?.error || 'Something went wrong'}</Typography>;
|
||||
}
|
||||
|
||||
if (showHostsEmptyState) {
|
||||
return (
|
||||
<HostsEmptyOrIncorrectMetrics
|
||||
noData={!sentAnyHostMetricsData}
|
||||
incorrectData={isSendingIncorrectK8SAgentMetrics}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (showNoFilteredHostsMessage) {
|
||||
return (
|
||||
<div className="no-filtered-hosts-message-container">
|
||||
<div className="no-filtered-hosts-message-content">
|
||||
<img
|
||||
src="/Icons/emptyState.svg"
|
||||
alt="thinking-emoji"
|
||||
className="empty-state-svg"
|
||||
/>
|
||||
|
||||
<Typography.Text className="no-filtered-hosts-message">
|
||||
This query had no results. Edit your query and try again!
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading || isFetching) {
|
||||
return (
|
||||
<div className="hosts-list-loading-state">
|
||||
<Skeleton.Input
|
||||
className="hosts-list-loading-state-item"
|
||||
size="large"
|
||||
block
|
||||
active
|
||||
/>
|
||||
<Skeleton.Input
|
||||
className="hosts-list-loading-state-item"
|
||||
size="large"
|
||||
block
|
||||
active
|
||||
/>
|
||||
<Skeleton.Input
|
||||
className="hosts-list-loading-state-item"
|
||||
size="large"
|
||||
block
|
||||
active
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Table
|
||||
className="hosts-list-table"
|
||||
dataSource={isLoading || isFetching ? [] : formattedHostMetricsData}
|
||||
columns={columns}
|
||||
pagination={{
|
||||
current: currentPage,
|
||||
pageSize,
|
||||
total: totalCount,
|
||||
showSizeChanger: true,
|
||||
hideOnSinglePage: false,
|
||||
onChange: (page, pageSize): void => {
|
||||
setCurrentPage(page);
|
||||
setPageSize(pageSize);
|
||||
},
|
||||
}}
|
||||
scroll={{ x: true }}
|
||||
loading={{
|
||||
spinning: isFetching || isLoading,
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
tableLayout="fixed"
|
||||
rowKey={(record): string => record.hostName}
|
||||
onChange={handleTableChange}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
}
|
||||
|
||||
.hosts-list-controls {
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
|
||||
display: flex;
|
||||
@@ -51,6 +52,40 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.hosts-list-content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.hosts-quick-filters-container {
|
||||
width: 280px;
|
||||
min-width: 280px;
|
||||
border-right: 1px solid var(--bg-slate-400);
|
||||
|
||||
.hosts-quick-filters-container-header {
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid var(--bg-slate-400);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hosts-list-table-container {
|
||||
flex: 1;
|
||||
|
||||
.hosts-list-table-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.quick-filters-toggle-container {
|
||||
padding: 0 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hosts-list-table {
|
||||
.ant-table {
|
||||
.ant-table-thead > tr > th {
|
||||
@@ -164,7 +199,7 @@
|
||||
margin: 0;
|
||||
|
||||
// this is to offset intercom icon till we improve the design
|
||||
padding-right: 72px;
|
||||
right: 20px;
|
||||
|
||||
.ant-pagination-item {
|
||||
border-radius: 4px;
|
||||
@@ -214,6 +249,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
flex: 1;
|
||||
|
||||
.hosts-list-loading-state-item {
|
||||
height: 48px;
|
||||
@@ -222,6 +258,7 @@
|
||||
}
|
||||
|
||||
.no-filtered-hosts-message-container {
|
||||
flex: 1;
|
||||
height: 30vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -246,6 +283,7 @@
|
||||
.hosts-empty-state-container {
|
||||
padding: 16px;
|
||||
height: 40vh;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
@@ -3,9 +3,22 @@ import './InfraMonitoring.styles.scss';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Progress, TabsProps, Tag } from 'antd';
|
||||
import { ColumnType } from 'antd/es/table';
|
||||
import { HostData, HostListPayload } from 'api/infraMonitoring/getHostLists';
|
||||
import {
|
||||
HostData,
|
||||
HostListPayload,
|
||||
HostListResponse,
|
||||
} from 'api/infraMonitoring/getHostLists';
|
||||
import {
|
||||
FiltersType,
|
||||
IQuickFiltersConfig,
|
||||
} from 'components/QuickFilters/types';
|
||||
import TabLabel from 'components/TabLabel';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import HostsList from './HostsList';
|
||||
|
||||
@@ -18,6 +31,29 @@ export interface HostRowData {
|
||||
active: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface HostsListTableProps {
|
||||
isLoading: boolean;
|
||||
isError: boolean;
|
||||
isFetching: boolean;
|
||||
tableData:
|
||||
| SuccessResponse<HostListResponse, unknown>
|
||||
| ErrorResponse
|
||||
| undefined;
|
||||
hostMetricsData: HostData[];
|
||||
filters: TagFilter;
|
||||
setSelectedHostName: Dispatch<SetStateAction<string | null>>;
|
||||
currentPage: number;
|
||||
setCurrentPage: Dispatch<SetStateAction<number>>;
|
||||
pageSize: number;
|
||||
setOrderBy: Dispatch<
|
||||
SetStateAction<{
|
||||
columnName: string;
|
||||
order: 'asc' | 'desc';
|
||||
} | null>
|
||||
>;
|
||||
setPageSize: (pageSize: number) => void;
|
||||
}
|
||||
|
||||
export const getHostListsQuery = (): HostListPayload => ({
|
||||
filters: {
|
||||
items: [],
|
||||
@@ -132,3 +168,36 @@ export const formatDataForTable = (data: HostData[]): HostRowData[] =>
|
||||
wait: `${Number((host.wait * 100).toFixed(1))}%`,
|
||||
load15: host.load15,
|
||||
}));
|
||||
|
||||
export const HostsQuickFiltersConfig: IQuickFiltersConfig[] = [
|
||||
{
|
||||
type: FiltersType.CHECKBOX,
|
||||
title: 'Host Name',
|
||||
attributeKey: {
|
||||
key: 'host_name',
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: 'system_cpu_load_average_15m',
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: true,
|
||||
},
|
||||
{
|
||||
type: FiltersType.CHECKBOX,
|
||||
title: 'OS Type',
|
||||
attributeKey: {
|
||||
key: 'os_type',
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: 'system_cpu_load_average_15m',
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: true,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -37,6 +37,25 @@ function K8sFiltersSidePanel({
|
||||
}
|
||||
}, [searchValue]);
|
||||
|
||||
// Close side panel when clicking outside of it
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
sidePanelRef.current &&
|
||||
!sidePanelRef.current.contains(event.target as Node)
|
||||
) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="k8s-filters-side-panel-container">
|
||||
<div className="k8s-filters-side-panel" ref={sidePanelRef}>
|
||||
|
||||
@@ -88,6 +88,8 @@ function QueryBuilderSearch({
|
||||
[pathname],
|
||||
);
|
||||
|
||||
const [isEditingTag, setIsEditingTag] = useState(false);
|
||||
|
||||
const {
|
||||
updateTag,
|
||||
handleClearTag,
|
||||
@@ -133,6 +135,16 @@ function QueryBuilderSearch({
|
||||
|
||||
const { handleRunQuery, currentQuery } = useQueryBuilder();
|
||||
|
||||
const toggleEditMode = useCallback(
|
||||
(value: boolean) => {
|
||||
// Editing mode is required only in infra monitoring mode
|
||||
if (isInfraMonitoring) {
|
||||
setIsEditingTag(value);
|
||||
}
|
||||
},
|
||||
[isInfraMonitoring],
|
||||
);
|
||||
|
||||
const onTagRender = ({
|
||||
value,
|
||||
closable,
|
||||
@@ -146,12 +158,16 @@ function QueryBuilderSearch({
|
||||
|
||||
const onCloseHandler = (): void => {
|
||||
onClose();
|
||||
// Editing is done after closing a tag
|
||||
toggleEditMode(false);
|
||||
handleSearch('');
|
||||
setSearchKey('');
|
||||
};
|
||||
|
||||
const tagEditHandler = (value: string): void => {
|
||||
updateTag(value);
|
||||
// Editing starts
|
||||
toggleEditMode(true);
|
||||
if (isInfraMonitoring) {
|
||||
setSearchValue(value);
|
||||
} else {
|
||||
@@ -188,6 +204,11 @@ function QueryBuilderSearch({
|
||||
if (isMulti || event.key === 'Backspace') handleKeyDown(event);
|
||||
if (isExistsNotExistsOperator(searchValue)) handleKeyDown(event);
|
||||
|
||||
// Editing is done after enter key press
|
||||
if (event.key === 'Enter') {
|
||||
toggleEditMode(false);
|
||||
}
|
||||
|
||||
if (
|
||||
!disableNavigationShortcuts &&
|
||||
(event.ctrlKey || event.metaKey) &&
|
||||
@@ -270,7 +291,14 @@ function QueryBuilderSearch({
|
||||
};
|
||||
});
|
||||
|
||||
onChange(initialTagFilters);
|
||||
// If in infra monitoring, only run the onChange query when editing is finsished.
|
||||
if (isInfraMonitoring) {
|
||||
if (!isEditingTag) {
|
||||
onChange(initialTagFilters);
|
||||
}
|
||||
} else {
|
||||
onChange(initialTagFilters);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [sourceKeys]);
|
||||
|
||||
@@ -367,7 +395,11 @@ function QueryBuilderSearch({
|
||||
)
|
||||
}
|
||||
showAction={['focus']}
|
||||
onBlur={handleOnBlur}
|
||||
onBlur={(e: React.FocusEvent<HTMLInputElement>): void => {
|
||||
handleOnBlur(e);
|
||||
// Editing is done after tapping out of the input
|
||||
toggleEditMode(false);
|
||||
}}
|
||||
popupClassName={isLogsExplorerPage ? 'logs-explorer-popup' : ''}
|
||||
dropdownRender={(menu): ReactElement => (
|
||||
<div>
|
||||
|
||||
@@ -387,6 +387,6 @@ export const getUPlotChartOptions = ({
|
||||
hiddenGraph,
|
||||
isDarkMode,
|
||||
}),
|
||||
axes: getAxes(isDarkMode, yAxisUnit),
|
||||
axes: getAxes({ isDarkMode, yAxisUnit, panelType }),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -123,6 +123,7 @@ export const getUplotHistogramChartOptions = ({
|
||||
setGraphsVisibilityStates,
|
||||
mergeAllQueries,
|
||||
onClickHandler = _noop,
|
||||
panelType,
|
||||
}: GetUplotHistogramChartOptionsProps): uPlot.Options =>
|
||||
({
|
||||
id,
|
||||
@@ -210,5 +211,5 @@ export const getUplotHistogramChartOptions = ({
|
||||
},
|
||||
],
|
||||
},
|
||||
axes: getAxes(isDarkMode),
|
||||
axes: getAxes({ isDarkMode, panelType }),
|
||||
} as uPlot.Options);
|
||||
|
||||
@@ -1,12 +1,27 @@
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
// @ts-nocheck
|
||||
import { getToolTipValue } from 'components/Graph/yAxisConfig';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
|
||||
import { uPlotXAxisValuesFormat } from './constants';
|
||||
import getGridColor from './getGridColor';
|
||||
|
||||
const PANEL_TYPES_WITH_X_AXIS_DATETIME_FORMAT = [
|
||||
PANEL_TYPES.TIME_SERIES,
|
||||
PANEL_TYPES.BAR,
|
||||
PANEL_TYPES.PIE,
|
||||
];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const getAxes = (isDarkMode: boolean, yAxisUnit?: string): any => [
|
||||
const getAxes = ({
|
||||
isDarkMode,
|
||||
yAxisUnit,
|
||||
panelType,
|
||||
}: {
|
||||
isDarkMode: boolean;
|
||||
yAxisUnit?: string;
|
||||
panelType?: PANEL_TYPES;
|
||||
}): any => [
|
||||
{
|
||||
stroke: isDarkMode ? 'white' : 'black', // Color of the axis line
|
||||
grid: {
|
||||
@@ -19,7 +34,11 @@ const getAxes = (isDarkMode: boolean, yAxisUnit?: string): any => [
|
||||
width: 0.3, // Width of the tick lines,
|
||||
show: true,
|
||||
},
|
||||
values: uPlotXAxisValuesFormat,
|
||||
...(PANEL_TYPES_WITH_X_AXIS_DATETIME_FORMAT.includes(panelType)
|
||||
? {
|
||||
values: uPlotXAxisValuesFormat,
|
||||
}
|
||||
: {}),
|
||||
gap: 5,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -43,7 +43,7 @@ function AlertHeader({ alertDetails }: AlertHeaderProps): JSX.Element {
|
||||
</div>
|
||||
</div>
|
||||
<div className="bottom-section">
|
||||
<AlertSeverity severity="warning" />
|
||||
{labels.severity && <AlertSeverity severity={labels.severity} />}
|
||||
|
||||
{/* // TODO(shaheer): Get actual data when we are able to get alert firing from state from API */}
|
||||
{/* <AlertStatus
|
||||
|
||||
@@ -2950,6 +2950,58 @@ func (r *ClickHouseReader) FetchTemporality(ctx context.Context, metricNames []s
|
||||
return metricNameToTemporality, nil
|
||||
}
|
||||
|
||||
func (r *ClickHouseReader) GetTemporalitySwitchPoints(ctx context.Context, metricName string, startTime int64, endTime int64) ([]v3.TemporalityChangePoint, error) {
|
||||
// Initialize slice to store temporality switch points
|
||||
var temporalitySwitches []v3.TemporalityChangePoint
|
||||
|
||||
query := fmt.Sprintf(`
|
||||
SELECT
|
||||
temporality,
|
||||
unix_milli,
|
||||
lag_temporality
|
||||
FROM (
|
||||
SELECT
|
||||
metric_name,
|
||||
temporality,
|
||||
unix_milli,
|
||||
lagInFrame(temporality, 1, '') OVER (
|
||||
PARTITION BY metric_name ORDER BY unix_milli
|
||||
) AS lag_temporality
|
||||
FROM %s.%s
|
||||
WHERE unix_milli >= %d
|
||||
AND unix_milli <= %d
|
||||
AND metric_name = '%s'
|
||||
) AS subquery
|
||||
WHERE lag_temporality != temporality
|
||||
AND lag_temporality != ''
|
||||
ORDER BY unix_milli ASC;
|
||||
`, signozMetricDBName, signozTSLocalTableNameV4, startTime, endTime, metricName)
|
||||
|
||||
rows, err := r.db.Query(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var temporality string
|
||||
var timestamp int64
|
||||
var lagTemporality string
|
||||
err := rows.Scan(&temporality, ×tamp, &lagTemporality)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Store each temporality switch point with both temporalities
|
||||
temporalitySwitches = append(temporalitySwitches, v3.TemporalityChangePoint{
|
||||
Timestamp: timestamp,
|
||||
FromTemporality: v3.Temporality(lagTemporality),
|
||||
ToTemporality: v3.Temporality(temporality),
|
||||
})
|
||||
}
|
||||
|
||||
return temporalitySwitches, nil
|
||||
}
|
||||
|
||||
func (r *ClickHouseReader) GetTimeSeriesInfo(ctx context.Context) (map[string]interface{}, error) {
|
||||
|
||||
queryStr := fmt.Sprintf("SELECT countDistinct(fingerprint) as count from %s.%s where metric_name not like 'signoz_%%' group by metric_name order by count desc;", signozMetricDBName, signozTSTableNameV41Day)
|
||||
|
||||
@@ -655,6 +655,9 @@ func (aH *APIHandler) PopulateTemporality(ctx context.Context, qp *v3.QueryRange
|
||||
} else {
|
||||
query.Temporality = v3.Unspecified
|
||||
}
|
||||
if len(aH.temporalityMap[query.AggregateAttribute.Key]) > 1 {
|
||||
query.MultipleTemporalities = true
|
||||
}
|
||||
}
|
||||
// we don't have temporality for this metric
|
||||
if query.DataSource == v3.DataSourceMetrics && query.Temporality == "" {
|
||||
@@ -682,6 +685,9 @@ func (aH *APIHandler) PopulateTemporality(ctx context.Context, qp *v3.QueryRange
|
||||
} else {
|
||||
query.Temporality = v3.Unspecified
|
||||
}
|
||||
if len(nameToTemporality[query.AggregateAttribute.Key]) > 1 {
|
||||
query.MultipleTemporalities = true
|
||||
}
|
||||
aH.temporalityMap[query.AggregateAttribute.Key] = nameToTemporality[query.AggregateAttribute.Key]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"go.signoz.io/signoz/pkg/query-service/model"
|
||||
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
||||
"go.signoz.io/signoz/pkg/query-service/postprocess"
|
||||
"golang.org/x/exp/maps"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
@@ -332,23 +333,53 @@ func (h *HostsRepo) DidSendHostMetricsData(ctx context.Context, req model.HostLi
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func (h *HostsRepo) IsSendingK8SAgentMetrics(ctx context.Context, req model.HostListRequest) (bool, error) {
|
||||
func (h *HostsRepo) IsSendingK8SAgentMetrics(ctx context.Context, req model.HostListRequest) ([]string, []string, error) {
|
||||
names := []string{}
|
||||
for _, metricName := range metricNamesForHosts {
|
||||
names = append(names, metricName)
|
||||
}
|
||||
namesStr := "'" + strings.Join(names, "','") + "'"
|
||||
|
||||
queryForRecentFingerprints := fmt.Sprintf(`
|
||||
SELECT DISTINCT fingerprint
|
||||
FROM %s.%s
|
||||
WHERE metric_name IN (%s)
|
||||
AND unix_milli >= toUnixTimestamp(now() - INTERVAL 5 MINUTE) * 1000`,
|
||||
constants.SIGNOZ_METRIC_DBNAME, constants.SIGNOZ_SAMPLES_V4_TABLENAME, namesStr)
|
||||
|
||||
query := fmt.Sprintf(`
|
||||
SELECT count()
|
||||
SELECT DISTINCT JSONExtractString(labels, 'k8s_cluster_name') as k8s_cluster_name, JSONExtractString(labels, 'k8s_node_name') as k8s_node_name
|
||||
FROM %s.%s
|
||||
WHERE metric_name IN (%s)
|
||||
AND unix_milli >= toUnixTimestamp(now() - INTERVAL 60 MINUTE) * 1000
|
||||
AND JSONExtractString(labels, 'host_name') LIKE '%%-otel-agent%%'`,
|
||||
constants.SIGNOZ_METRIC_DBNAME, constants.SIGNOZ_TIMESERIES_V4_TABLENAME, namesStr)
|
||||
AND JSONExtractString(labels, 'host_name') LIKE '%%-otel-agent%%'
|
||||
AND fingerprint GLOBAL IN (%s)`,
|
||||
constants.SIGNOZ_METRIC_DBNAME, constants.SIGNOZ_TIMESERIES_V4_TABLENAME, namesStr, queryForRecentFingerprints)
|
||||
|
||||
count, err := h.reader.GetCountOfThings(ctx, query)
|
||||
return count > 0, err
|
||||
result, err := h.reader.GetListResultV3(ctx, query)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
clusterNames := make(map[string]struct{})
|
||||
nodeNames := make(map[string]struct{})
|
||||
|
||||
for _, row := range result {
|
||||
switch v := row.Data["k8s_cluster_name"].(type) {
|
||||
case string:
|
||||
clusterNames[v] = struct{}{}
|
||||
case *string:
|
||||
clusterNames[*v] = struct{}{}
|
||||
}
|
||||
switch v := row.Data["k8s_node_name"].(type) {
|
||||
case string:
|
||||
nodeNames[v] = struct{}{}
|
||||
case *string:
|
||||
nodeNames[*v] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
return maps.Keys(clusterNames), maps.Keys(nodeNames), nil
|
||||
}
|
||||
|
||||
func (h *HostsRepo) GetHostList(ctx context.Context, req model.HostListRequest) (model.HostListResponse, error) {
|
||||
@@ -372,8 +403,10 @@ func (h *HostsRepo) GetHostList(ctx context.Context, req model.HostListRequest)
|
||||
}
|
||||
|
||||
// don't fail the request if we can't get these values
|
||||
if sendingK8SAgentMetrics, err := h.IsSendingK8SAgentMetrics(ctx, req); err == nil {
|
||||
resp.IsSendingK8SAgentMetrics = sendingK8SAgentMetrics
|
||||
if clusterNames, nodeNames, err := h.IsSendingK8SAgentMetrics(ctx, req); err == nil {
|
||||
resp.IsSendingK8SAgentMetrics = len(clusterNames) > 0 || len(nodeNames) > 0
|
||||
resp.ClusterNames = clusterNames
|
||||
resp.NodeNames = nodeNames
|
||||
}
|
||||
if sentAnyHostMetricsData, err := h.DidSendHostMetricsData(ctx, req); err == nil {
|
||||
resp.SentAnyHostMetricsData = sentAnyHostMetricsData
|
||||
|
||||
@@ -177,7 +177,23 @@ func (q *querier) runBuilderQueries(ctx context.Context, params *v3.QueryRangePa
|
||||
for queryName, builderQuery := range params.CompositeQuery.BuilderQueries {
|
||||
if queryName == builderQuery.Expression {
|
||||
wg.Add(1)
|
||||
go q.runBuilderQuery(ctx, builderQuery, params, cacheKeys, ch, &wg)
|
||||
if builderQuery.MultipleTemporalities == true {
|
||||
go func() {
|
||||
|
||||
temporalitySwitches, err := q.reader.GetTemporalitySwitchPoints(ctx, builderQuery.AggregateAttribute.Key, params.Start, params.End)
|
||||
if err != nil {
|
||||
ch <- channelResult{Err: err, Name: queryName}
|
||||
return
|
||||
}
|
||||
if len(temporalitySwitches) == 0 {
|
||||
q.runBuilderQuery(ctx, builderQuery, params, cacheKeys, ch, &wg)
|
||||
} else {
|
||||
q.handleTemporalitySwitches(ctx, temporalitySwitches, &wg, builderQuery, params, cacheKeys, ch, queryName)
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
go q.runBuilderQuery(ctx, builderQuery, params, cacheKeys, ch, &wg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,6 +225,58 @@ func (q *querier) runBuilderQueries(ctx context.Context, params *v3.QueryRangePa
|
||||
return results, errQueriesByName, err
|
||||
}
|
||||
|
||||
func (q *querier) handleTemporalitySwitches(ctx context.Context, temporalitySwitches []v3.TemporalityChangePoint, wg *sync.WaitGroup, builderQuery *v3.BuilderQuery, params *v3.QueryRangeParamsV3, cacheKeys map[string]string, ch chan channelResult, queryName string) {
|
||||
defer wg.Done()
|
||||
|
||||
tempCh := make(chan channelResult, len(temporalitySwitches)+1)
|
||||
|
||||
var tempWg sync.WaitGroup
|
||||
// Handle each segment between switch points
|
||||
for i := 0; i <= len(temporalitySwitches); i++ {
|
||||
tempWg.Add(1)
|
||||
go func(idx int) {
|
||||
queryWithTemporality := *builderQuery
|
||||
queryParams := *params
|
||||
if i == 0 {
|
||||
queryParams.End = temporalitySwitches[idx].Timestamp
|
||||
queryWithTemporality.Temporality = temporalitySwitches[idx].FromTemporality
|
||||
} else if idx < len(temporalitySwitches) {
|
||||
queryParams.Start = temporalitySwitches[idx-1].Timestamp
|
||||
queryParams.End = temporalitySwitches[idx].Timestamp
|
||||
queryWithTemporality.Temporality = temporalitySwitches[idx].FromTemporality
|
||||
queryWithTemporality.ShiftBy = 0
|
||||
} else if idx == len(temporalitySwitches) {
|
||||
queryParams.Start = temporalitySwitches[idx-1].Timestamp
|
||||
queryParams.End = params.End
|
||||
queryWithTemporality.Temporality = temporalitySwitches[idx-1].ToTemporality
|
||||
}
|
||||
|
||||
q.runBuilderQuery(ctx, &queryWithTemporality, &queryParams, cacheKeys, tempCh, &tempWg)
|
||||
}(i)
|
||||
}
|
||||
// Wait for all temporal queries to complete
|
||||
tempWg.Wait()
|
||||
close(tempCh)
|
||||
|
||||
// Combine results from all temporal queries
|
||||
var combinedSeries []*v3.Series
|
||||
var lastErr error
|
||||
|
||||
for result := range tempCh {
|
||||
if result.Err != nil {
|
||||
lastErr = result.Err
|
||||
continue
|
||||
}
|
||||
combinedSeries = append(combinedSeries, result.Series...)
|
||||
}
|
||||
|
||||
ch <- channelResult{
|
||||
Series: combinedSeries,
|
||||
Err: lastErr,
|
||||
Name: queryName,
|
||||
}
|
||||
}
|
||||
|
||||
func (q *querier) runPromQueries(ctx context.Context, params *v3.QueryRangeParamsV3) ([]*v3.Result, map[string]error, error) {
|
||||
channelResults := make(chan channelResult, len(params.CompositeQuery.PromQueries))
|
||||
var wg sync.WaitGroup
|
||||
|
||||
@@ -115,6 +115,8 @@ type Reader interface {
|
||||
//trace
|
||||
GetTraceFields(ctx context.Context) (*model.GetFieldsResponse, *model.ApiError)
|
||||
UpdateTraceField(ctx context.Context, field *model.UpdateField) *model.ApiError
|
||||
|
||||
GetTemporalitySwitchPoints(ctx context.Context, metricName string, startTime int64, endTime int64) ([]v3.TemporalityChangePoint, error)
|
||||
}
|
||||
|
||||
type Querier interface {
|
||||
|
||||
@@ -42,6 +42,8 @@ type HostListResponse struct {
|
||||
Total int `json:"total"`
|
||||
SentAnyHostMetricsData bool `json:"sentAnyHostMetricsData"`
|
||||
IsSendingK8SAgentMetrics bool `json:"isSendingK8SAgentMetrics"`
|
||||
ClusterNames []string `json:"clusterNames"`
|
||||
NodeNames []string `json:"nodeNames"`
|
||||
}
|
||||
|
||||
func (r *HostListResponse) SortBy(orderBy *v3.OrderBy) {
|
||||
|
||||
@@ -807,33 +807,34 @@ func (m *MetricValueFilter) Clone() *MetricValueFilter {
|
||||
}
|
||||
|
||||
type BuilderQuery struct {
|
||||
QueryName string `json:"queryName"`
|
||||
StepInterval int64 `json:"stepInterval"`
|
||||
DataSource DataSource `json:"dataSource"`
|
||||
AggregateOperator AggregateOperator `json:"aggregateOperator"`
|
||||
AggregateAttribute AttributeKey `json:"aggregateAttribute,omitempty"`
|
||||
Temporality Temporality `json:"temporality,omitempty"`
|
||||
Filters *FilterSet `json:"filters,omitempty"`
|
||||
GroupBy []AttributeKey `json:"groupBy,omitempty"`
|
||||
Expression string `json:"expression"`
|
||||
Disabled bool `json:"disabled"`
|
||||
Having []Having `json:"having,omitempty"`
|
||||
Legend string `json:"legend,omitempty"`
|
||||
Limit uint64 `json:"limit"`
|
||||
Offset uint64 `json:"offset"`
|
||||
PageSize uint64 `json:"pageSize"`
|
||||
OrderBy []OrderBy `json:"orderBy,omitempty"`
|
||||
ReduceTo ReduceToOperator `json:"reduceTo,omitempty"`
|
||||
SelectColumns []AttributeKey `json:"selectColumns,omitempty"`
|
||||
TimeAggregation TimeAggregation `json:"timeAggregation,omitempty"`
|
||||
SpaceAggregation SpaceAggregation `json:"spaceAggregation,omitempty"`
|
||||
QueryName string `json:"queryName"`
|
||||
StepInterval int64 `json:"stepInterval"`
|
||||
DataSource DataSource `json:"dataSource"`
|
||||
AggregateOperator AggregateOperator `json:"aggregateOperator"`
|
||||
AggregateAttribute AttributeKey `json:"aggregateAttribute,omitempty"`
|
||||
Temporality Temporality `json:"temporality,omitempty"`
|
||||
Filters *FilterSet `json:"filters,omitempty"`
|
||||
GroupBy []AttributeKey `json:"groupBy,omitempty"`
|
||||
Expression string `json:"expression"`
|
||||
Disabled bool `json:"disabled"`
|
||||
Having []Having `json:"having,omitempty"`
|
||||
Legend string `json:"legend,omitempty"`
|
||||
Limit uint64 `json:"limit"`
|
||||
Offset uint64 `json:"offset"`
|
||||
PageSize uint64 `json:"pageSize"`
|
||||
OrderBy []OrderBy `json:"orderBy,omitempty"`
|
||||
ReduceTo ReduceToOperator `json:"reduceTo,omitempty"`
|
||||
SelectColumns []AttributeKey `json:"selectColumns,omitempty"`
|
||||
TimeAggregation TimeAggregation `json:"timeAggregation,omitempty"`
|
||||
SpaceAggregation SpaceAggregation `json:"spaceAggregation,omitempty"`
|
||||
SecondaryAggregation SecondaryAggregation `json:"seriesAggregation,omitempty"`
|
||||
Functions []Function `json:"functions,omitempty"`
|
||||
ShiftBy int64
|
||||
IsAnomaly bool
|
||||
QueriesUsedInFormula []string
|
||||
MetricTableHints *MetricTableHints `json:"-"`
|
||||
MetricValueFilter *MetricValueFilter `json:"-"`
|
||||
Functions []Function `json:"functions,omitempty"`
|
||||
ShiftBy int64
|
||||
IsAnomaly bool
|
||||
QueriesUsedInFormula []string
|
||||
MetricTableHints *MetricTableHints `json:"-"`
|
||||
MetricValueFilter *MetricValueFilter `json:"-"`
|
||||
MultipleTemporalities bool
|
||||
}
|
||||
|
||||
func (b *BuilderQuery) SetShiftByFromFunc() {
|
||||
@@ -1406,3 +1407,9 @@ type QBOptions struct {
|
||||
IsLivetailQuery bool
|
||||
PreferRPM bool
|
||||
}
|
||||
|
||||
type TemporalityChangePoint struct {
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
FromTemporality Temporality `json:"from_temporality"`
|
||||
ToTemporality Temporality `json:"to_temporality"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user