Compare commits

...

11 Commits

Author SHA1 Message Date
Srikanth Chekuri
85d7075d3b Merge branch 'main' into fix/6175 2025-01-30 13:17:44 +05:30
Amlan Kumar Nandy
ffd72cf406 chore: fix visibility toggle in host filters (#6976) 2025-01-30 00:15:19 +05:30
Vikrant Gupta
6dfea14219 chore(license): default to basic plan if license validation fails (#6972)
* chore(license): default to basic plan if license validation fails 3 times

* chore(license): revert the on-boot validation check

* chore(license): reset the atomic counter

* chore(license): revert the table creation removal

* chore(license): remove verify issue workflow

* chore(license): add proper log level

* chore(license): add proper log level

* chore(license): close the validation go routine post defaulting to basic plan

* chore(license): set the validator running flag to false as well
2025-01-29 18:17:50 +00:00
Shaheer Kochai
f2be856f63 fix: apply x axis date/time formatting only to panel types that have date/time in x axis (#6956) 2025-01-29 18:09:04 +00:00
Shaheer Kochai
f04589a0b2 fix: properly get the alert severity (#6974) 2025-01-29 17:30:51 +00:00
Amlan Kumar Nandy
1378590429 chore: fix filter editing issue in infra monitoring (#6961) 2025-01-29 17:21:57 +00:00
Srikanth Chekuri
88084af4d4 chore: add cluster, nodes that are sending incorrect host metrics (#6969) 2025-01-29 14:15:48 +00:00
Amlan Kumar Nandy
d0eefa0cf2 feat: add hostname and os type quick filter to hosts list (#6926) 2025-01-29 13:47:23 +00:00
Vikrant Gupta
cc9eb32c50 chore(license): clean up the older endpoints (#6971) 2025-01-29 18:13:50 +05:30
Aniket
c648b72ace feat: added simultaneous temporality changes | 6175 2025-01-24 11:22:11 +05:30
Aniket
a449698dfe feat: added simultaneous temporality changes | 6175 2025-01-24 11:16:28 +05:30
27 changed files with 711 additions and 480 deletions

View File

@@ -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 }}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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")
}

View File

@@ -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"`

View File

@@ -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';

View File

@@ -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';

View File

@@ -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),
};

View File

@@ -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(
() => ({

View File

@@ -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

View 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',
})}
/>
);
}

View File

@@ -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;

View File

@@ -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,
},
];

View File

@@ -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}>

View File

@@ -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>

View File

@@ -387,6 +387,6 @@ export const getUPlotChartOptions = ({
hiddenGraph,
isDarkMode,
}),
axes: getAxes(isDarkMode, yAxisUnit),
axes: getAxes({ isDarkMode, yAxisUnit, panelType }),
};
};

View File

@@ -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);

View File

@@ -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,
},
{

View File

@@ -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

View File

@@ -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, &timestamp, &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)

View File

@@ -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]
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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"`
}