mirror of
https://github.com/SigNoz/signoz.git
synced 2026-04-02 18:30:25 +01:00
Compare commits
25 Commits
user-roles
...
user-v2-up
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80b8ae1b21 | ||
|
|
419bd60a41 | ||
|
|
fd7ad90d9f | ||
|
|
fbe1e227a1 | ||
|
|
ed65d24518 | ||
|
|
794377d766 | ||
|
|
b6b689902d | ||
|
|
65402ca367 | ||
|
|
f71d5bf8f1 | ||
|
|
5abfd0732a | ||
|
|
e0e3ab2ef4 | ||
|
|
407409fc00 | ||
|
|
b795085189 | ||
|
|
e1f6943e1a | ||
|
|
4f273b296e | ||
|
|
1d20af668c | ||
|
|
bb2babdbb4 | ||
|
|
3dc0a7c8ce | ||
|
|
f11c73bbe6 | ||
|
|
c8c6a52e36 | ||
|
|
1080553905 | ||
|
|
820d26617b | ||
|
|
23e3c75d24 | ||
|
|
42415e0873 | ||
|
|
bad80399a6 |
@@ -327,27 +327,6 @@ components:
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
AuthtypesStorableRole:
|
||||
properties:
|
||||
createdAt:
|
||||
format: date-time
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
orgId:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
updatedAt:
|
||||
format: date-time
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
type: object
|
||||
AuthtypesTransaction:
|
||||
properties:
|
||||
object:
|
||||
@@ -371,7 +350,7 @@ components:
|
||||
id:
|
||||
type: string
|
||||
role:
|
||||
$ref: '#/components/schemas/AuthtypesStorableRole'
|
||||
$ref: '#/components/schemas/AuthtypesRole'
|
||||
roleId:
|
||||
type: string
|
||||
updatedAt:
|
||||
@@ -381,6 +360,11 @@ components:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- userId
|
||||
- roleId
|
||||
- createdAt
|
||||
- updatedAt
|
||||
- role
|
||||
type: object
|
||||
AuthtypesUserWithRoles:
|
||||
properties:
|
||||
@@ -2313,15 +2297,6 @@ components:
|
||||
- status
|
||||
- error
|
||||
type: object
|
||||
RulestatehistorytypesAlertState:
|
||||
enum:
|
||||
- inactive
|
||||
- pending
|
||||
- recovering
|
||||
- firing
|
||||
- nodata
|
||||
- disabled
|
||||
type: string
|
||||
RulestatehistorytypesGettableRuleStateHistory:
|
||||
properties:
|
||||
fingerprint:
|
||||
@@ -2333,15 +2308,15 @@ components:
|
||||
nullable: true
|
||||
type: array
|
||||
overallState:
|
||||
$ref: '#/components/schemas/RulestatehistorytypesAlertState'
|
||||
$ref: '#/components/schemas/RuletypesAlertState'
|
||||
overallStateChanged:
|
||||
type: boolean
|
||||
ruleID:
|
||||
ruleId:
|
||||
type: string
|
||||
ruleName:
|
||||
type: string
|
||||
state:
|
||||
$ref: '#/components/schemas/RulestatehistorytypesAlertState'
|
||||
$ref: '#/components/schemas/RuletypesAlertState'
|
||||
stateChanged:
|
||||
type: boolean
|
||||
unixMilli:
|
||||
@@ -2351,7 +2326,7 @@ components:
|
||||
format: double
|
||||
type: number
|
||||
required:
|
||||
- ruleID
|
||||
- ruleId
|
||||
- ruleName
|
||||
- overallState
|
||||
- overallStateChanged
|
||||
@@ -2441,12 +2416,21 @@ components:
|
||||
format: int64
|
||||
type: integer
|
||||
state:
|
||||
$ref: '#/components/schemas/RulestatehistorytypesAlertState'
|
||||
$ref: '#/components/schemas/RuletypesAlertState'
|
||||
required:
|
||||
- state
|
||||
- start
|
||||
- end
|
||||
type: object
|
||||
RuletypesAlertState:
|
||||
enum:
|
||||
- inactive
|
||||
- pending
|
||||
- recovering
|
||||
- firing
|
||||
- nodata
|
||||
- disabled
|
||||
type: string
|
||||
ServiceaccounttypesGettableFactorAPIKey:
|
||||
properties:
|
||||
createdAt:
|
||||
@@ -8469,7 +8453,7 @@ paths:
|
||||
- in: query
|
||||
name: state
|
||||
schema:
|
||||
$ref: '#/components/schemas/RulestatehistorytypesAlertState'
|
||||
$ref: '#/components/schemas/RuletypesAlertState'
|
||||
- in: query
|
||||
name: filterExpression
|
||||
schema:
|
||||
|
||||
@@ -32,7 +32,7 @@ func (s Seasonality) IsValid() bool {
|
||||
}
|
||||
|
||||
type AnomaliesRequest struct {
|
||||
Params qbtypes.QueryRangeRequest
|
||||
Params *qbtypes.QueryRangeRequest
|
||||
Seasonality Seasonality
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ type anomalyQueryParams struct {
|
||||
Past3SeasonQuery qbtypes.QueryRangeRequest
|
||||
}
|
||||
|
||||
func prepareAnomalyQueryParams(req qbtypes.QueryRangeRequest, seasonality Seasonality) *anomalyQueryParams {
|
||||
func prepareAnomalyQueryParams(req *qbtypes.QueryRangeRequest, seasonality Seasonality) *anomalyQueryParams {
|
||||
start := req.Start
|
||||
end := req.End
|
||||
|
||||
|
||||
@@ -76,12 +76,12 @@ func (provider *provider) Start(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (provider *provider) Audit(ctx context.Context, event audittypes.AuditEvent) {
|
||||
if event.PrincipalOrgID.IsZero() {
|
||||
if event.PrincipalAttributes.PrincipalOrgID.IsZero() {
|
||||
provider.settings.Logger().WarnContext(ctx, "audit event dropped as org_id is zero")
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := provider.licensing.GetActive(ctx, event.PrincipalOrgID); err != nil {
|
||||
if _, err := provider.licensing.GetActive(ctx, event.PrincipalAttributes.PrincipalOrgID); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ func (h *handler) QueryRange(rw http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
|
||||
if anomalyQuery, ok := queryRangeRequest.IsAnomalyRequest(); ok {
|
||||
anomalies, err := h.handleAnomalyQuery(ctx, orgID, anomalyQuery, queryRangeRequest)
|
||||
anomalies, err := h.handleAnomalyQuery(ctx, orgID, anomalyQuery, &queryRangeRequest)
|
||||
if err != nil {
|
||||
render.Error(rw, errors.NewInternalf(errors.CodeInternal, "failed to get anomalies: %v", err))
|
||||
return
|
||||
@@ -149,7 +149,7 @@ func (h *handler) createAnomalyProvider(seasonality anomalyV2.Seasonality) anoma
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) handleAnomalyQuery(ctx context.Context, orgID valuer.UUID, anomalyQuery *qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation], queryRangeRequest qbtypes.QueryRangeRequest) (*anomalyV2.AnomaliesResponse, error) {
|
||||
func (h *handler) handleAnomalyQuery(ctx context.Context, orgID valuer.UUID, anomalyQuery *qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation], queryRangeRequest *qbtypes.QueryRangeRequest) (*anomalyV2.AnomaliesResponse, error) {
|
||||
seasonality := extractSeasonality(anomalyQuery)
|
||||
provider := h.createAnomalyProvider(seasonality)
|
||||
|
||||
|
||||
@@ -49,7 +49,6 @@ import (
|
||||
opAmpModel "github.com/SigNoz/signoz/pkg/query-service/app/opamp/model"
|
||||
baseconst "github.com/SigNoz/signoz/pkg/query-service/constants"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/healthcheck"
|
||||
baseint "github.com/SigNoz/signoz/pkg/query-service/interfaces"
|
||||
baserules "github.com/SigNoz/signoz/pkg/query-service/rules"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils"
|
||||
)
|
||||
@@ -99,7 +98,6 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
|
||||
)
|
||||
|
||||
rm, err := makeRulesManager(
|
||||
reader,
|
||||
signoz.Cache,
|
||||
signoz.Alertmanager,
|
||||
signoz.SQLStore,
|
||||
@@ -229,7 +227,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
|
||||
s.config.APIServer.Timeout.Default,
|
||||
s.config.APIServer.Timeout.Max,
|
||||
).Wrap)
|
||||
r.Use(middleware.NewLogging(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes).Wrap)
|
||||
r.Use(middleware.NewAudit(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes, nil).Wrap)
|
||||
r.Use(middleware.NewComment().Wrap)
|
||||
|
||||
apiHandler.RegisterRoutes(r, am)
|
||||
@@ -345,7 +343,7 @@ func (s *Server) Stop(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func makeRulesManager(ch baseint.Reader, cache cache.Cache, alertmanager alertmanager.Alertmanager, sqlstore sqlstore.SQLStore, telemetryStore telemetrystore.TelemetryStore, metadataStore telemetrytypes.MetadataStore, prometheus prometheus.Prometheus, orgGetter organization.Getter, ruleStateHistoryModule rulestatehistory.Module, querier querier.Querier, providerSettings factory.ProviderSettings, queryParser queryparser.QueryParser) (*baserules.Manager, error) {
|
||||
func makeRulesManager(cache cache.Cache, alertmanager alertmanager.Alertmanager, sqlstore sqlstore.SQLStore, telemetryStore telemetrystore.TelemetryStore, metadataStore telemetrytypes.MetadataStore, prometheus prometheus.Prometheus, orgGetter organization.Getter, ruleStateHistoryModule rulestatehistory.Module, querier querier.Querier, providerSettings factory.ProviderSettings, queryParser queryparser.QueryParser) (*baserules.Manager, error) {
|
||||
ruleStore := sqlrulestore.NewRuleStore(sqlstore, queryParser, providerSettings)
|
||||
maintenanceStore := sqlrulestore.NewMaintenanceStore(sqlstore)
|
||||
// create manager opts
|
||||
@@ -354,7 +352,6 @@ func makeRulesManager(ch baseint.Reader, cache cache.Cache, alertmanager alertma
|
||||
MetadataStore: metadataStore,
|
||||
Prometheus: prometheus,
|
||||
Context: context.Background(),
|
||||
Reader: ch,
|
||||
Querier: querier,
|
||||
Logger: providerSettings.Logger,
|
||||
Cache: cache,
|
||||
@@ -365,7 +362,7 @@ func makeRulesManager(ch baseint.Reader, cache cache.Cache, alertmanager alertma
|
||||
OrgGetter: orgGetter,
|
||||
RuleStore: ruleStore,
|
||||
MaintenanceStore: maintenanceStore,
|
||||
SqlStore: sqlstore,
|
||||
SQLStore: sqlstore,
|
||||
QueryParser: queryParser,
|
||||
RuleStateHistoryModule: ruleStateHistoryModule,
|
||||
}
|
||||
|
||||
@@ -5,58 +5,34 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"math"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/ee/query-service/anomaly"
|
||||
"github.com/SigNoz/signoz/pkg/cache"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/common"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
"github.com/SigNoz/signoz/pkg/transition"
|
||||
"github.com/SigNoz/signoz/pkg/querier"
|
||||
"github.com/SigNoz/signoz/pkg/types/rulestatehistorytypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
|
||||
querierV2 "github.com/SigNoz/signoz/pkg/query-service/app/querier/v2"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/queryBuilder"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/interfaces"
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils/labels"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils/times"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils/timestamp"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/units"
|
||||
|
||||
baserules "github.com/SigNoz/signoz/pkg/query-service/rules"
|
||||
|
||||
querierV5 "github.com/SigNoz/signoz/pkg/querier"
|
||||
|
||||
anomalyV2 "github.com/SigNoz/signoz/ee/anomaly"
|
||||
"github.com/SigNoz/signoz/ee/anomaly"
|
||||
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
)
|
||||
|
||||
const (
|
||||
RuleTypeAnomaly = "anomaly_rule"
|
||||
)
|
||||
|
||||
type AnomalyRule struct {
|
||||
*baserules.BaseRule
|
||||
|
||||
mtx sync.Mutex
|
||||
|
||||
reader interfaces.Reader
|
||||
// querier is used for alerts migrated after the introduction of new query builder
|
||||
querier querier.Querier
|
||||
|
||||
// querierV2 is used for alerts created after the introduction of new metrics query builder
|
||||
querierV2 interfaces.Querier
|
||||
|
||||
// querierV5 is used for alerts migrated after the introduction of new query builder
|
||||
querierV5 querierV5.Querier
|
||||
|
||||
provider anomaly.Provider
|
||||
providerV2 anomalyV2.Provider
|
||||
provider anomaly.Provider
|
||||
|
||||
version string
|
||||
logger *slog.Logger
|
||||
@@ -70,18 +46,16 @@ func NewAnomalyRule(
|
||||
id string,
|
||||
orgID valuer.UUID,
|
||||
p *ruletypes.PostableRule,
|
||||
reader interfaces.Reader,
|
||||
querierV5 querierV5.Querier,
|
||||
querier querier.Querier,
|
||||
logger *slog.Logger,
|
||||
cache cache.Cache,
|
||||
opts ...baserules.RuleOption,
|
||||
) (*AnomalyRule, error) {
|
||||
|
||||
logger.Info("creating new AnomalyRule", "rule_id", id)
|
||||
logger.Info("creating new AnomalyRule", slog.String("rule.id", id))
|
||||
|
||||
opts = append(opts, baserules.WithLogger(logger))
|
||||
|
||||
baseRule, err := baserules.NewBaseRule(id, orgID, p, reader, opts...)
|
||||
baseRule, err := baserules.NewBaseRule(id, orgID, p, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -101,93 +75,38 @@ func NewAnomalyRule(
|
||||
t.seasonality = anomaly.SeasonalityDaily
|
||||
}
|
||||
|
||||
logger.Info("using seasonality", "seasonality", t.seasonality.String())
|
||||
logger.Info("using seasonality", slog.String("rule.id", id), slog.String("rule.seasonality", t.seasonality.StringValue()))
|
||||
|
||||
querierOptsV2 := querierV2.QuerierOptions{
|
||||
Reader: reader,
|
||||
Cache: cache,
|
||||
KeyGenerator: queryBuilder.NewKeyGenerator(),
|
||||
}
|
||||
|
||||
t.querierV2 = querierV2.NewQuerier(querierOptsV2)
|
||||
t.reader = reader
|
||||
if t.seasonality == anomaly.SeasonalityHourly {
|
||||
t.provider = anomaly.NewHourlyProvider(
|
||||
anomaly.WithCache[*anomaly.HourlyProvider](cache),
|
||||
anomaly.WithKeyGenerator[*anomaly.HourlyProvider](queryBuilder.NewKeyGenerator()),
|
||||
anomaly.WithReader[*anomaly.HourlyProvider](reader),
|
||||
anomaly.WithQuerier[*anomaly.HourlyProvider](querier),
|
||||
anomaly.WithLogger[*anomaly.HourlyProvider](logger),
|
||||
)
|
||||
} else if t.seasonality == anomaly.SeasonalityDaily {
|
||||
t.provider = anomaly.NewDailyProvider(
|
||||
anomaly.WithCache[*anomaly.DailyProvider](cache),
|
||||
anomaly.WithKeyGenerator[*anomaly.DailyProvider](queryBuilder.NewKeyGenerator()),
|
||||
anomaly.WithReader[*anomaly.DailyProvider](reader),
|
||||
anomaly.WithQuerier[*anomaly.DailyProvider](querier),
|
||||
anomaly.WithLogger[*anomaly.DailyProvider](logger),
|
||||
)
|
||||
} else if t.seasonality == anomaly.SeasonalityWeekly {
|
||||
t.provider = anomaly.NewWeeklyProvider(
|
||||
anomaly.WithCache[*anomaly.WeeklyProvider](cache),
|
||||
anomaly.WithKeyGenerator[*anomaly.WeeklyProvider](queryBuilder.NewKeyGenerator()),
|
||||
anomaly.WithReader[*anomaly.WeeklyProvider](reader),
|
||||
anomaly.WithQuerier[*anomaly.WeeklyProvider](querier),
|
||||
anomaly.WithLogger[*anomaly.WeeklyProvider](logger),
|
||||
)
|
||||
}
|
||||
|
||||
if t.seasonality == anomaly.SeasonalityHourly {
|
||||
t.providerV2 = anomalyV2.NewHourlyProvider(
|
||||
anomalyV2.WithQuerier[*anomalyV2.HourlyProvider](querierV5),
|
||||
anomalyV2.WithLogger[*anomalyV2.HourlyProvider](logger),
|
||||
)
|
||||
} else if t.seasonality == anomaly.SeasonalityDaily {
|
||||
t.providerV2 = anomalyV2.NewDailyProvider(
|
||||
anomalyV2.WithQuerier[*anomalyV2.DailyProvider](querierV5),
|
||||
anomalyV2.WithLogger[*anomalyV2.DailyProvider](logger),
|
||||
)
|
||||
} else if t.seasonality == anomaly.SeasonalityWeekly {
|
||||
t.providerV2 = anomalyV2.NewWeeklyProvider(
|
||||
anomalyV2.WithQuerier[*anomalyV2.WeeklyProvider](querierV5),
|
||||
anomalyV2.WithLogger[*anomalyV2.WeeklyProvider](logger),
|
||||
)
|
||||
}
|
||||
|
||||
t.querierV5 = querierV5
|
||||
t.querier = querier
|
||||
t.version = p.Version
|
||||
t.logger = logger
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
func (r *AnomalyRule) Type() ruletypes.RuleType {
|
||||
return RuleTypeAnomaly
|
||||
return ruletypes.RuleTypeAnomaly
|
||||
}
|
||||
|
||||
func (r *AnomalyRule) prepareQueryRange(ctx context.Context, ts time.Time) (*v3.QueryRangeParamsV3, error) {
|
||||
func (r *AnomalyRule) prepareQueryRange(ctx context.Context, ts time.Time) *qbtypes.QueryRangeRequest {
|
||||
|
||||
r.logger.InfoContext(
|
||||
ctx, "prepare query range request v4", "ts", ts.UnixMilli(), "eval_window", r.EvalWindow().Milliseconds(), "eval_delay", r.EvalDelay().Milliseconds(),
|
||||
)
|
||||
|
||||
st, en := r.Timestamps(ts)
|
||||
start := st.UnixMilli()
|
||||
end := en.UnixMilli()
|
||||
|
||||
compositeQuery := r.Condition().CompositeQuery
|
||||
|
||||
if compositeQuery.PanelType != v3.PanelTypeGraph {
|
||||
compositeQuery.PanelType = v3.PanelTypeGraph
|
||||
}
|
||||
|
||||
// default mode
|
||||
return &v3.QueryRangeParamsV3{
|
||||
Start: start,
|
||||
End: end,
|
||||
Step: int64(math.Max(float64(common.MinAllowedStepInterval(start, end)), 60)),
|
||||
CompositeQuery: compositeQuery,
|
||||
Variables: make(map[string]interface{}, 0),
|
||||
NoCache: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *AnomalyRule) prepareQueryRangeV5(ctx context.Context, ts time.Time) (*qbtypes.QueryRangeRequest, error) {
|
||||
|
||||
r.logger.InfoContext(ctx, "prepare query range request v5", "ts", ts.UnixMilli(), "eval_window", r.EvalWindow().Milliseconds(), "eval_delay", r.EvalDelay().Milliseconds())
|
||||
r.logger.InfoContext(ctx, "prepare query range request", slog.String("rule.id", r.ID()), slog.Int64("ts", ts.UnixMilli()), slog.Int64("eval.window_ms", r.EvalWindow().Milliseconds()), slog.Int64("eval.delay_ms", r.EvalDelay().Milliseconds()))
|
||||
|
||||
startTs, endTs := r.Timestamps(ts)
|
||||
start, end := startTs.UnixMilli(), endTs.UnixMilli()
|
||||
@@ -203,25 +122,14 @@ func (r *AnomalyRule) prepareQueryRangeV5(ctx context.Context, ts time.Time) (*q
|
||||
}
|
||||
req.CompositeQuery.Queries = make([]qbtypes.QueryEnvelope, len(r.Condition().CompositeQuery.Queries))
|
||||
copy(req.CompositeQuery.Queries, r.Condition().CompositeQuery.Queries)
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func (r *AnomalyRule) GetSelectedQuery() string {
|
||||
return r.Condition().GetSelectedQueryName()
|
||||
return req
|
||||
}
|
||||
|
||||
func (r *AnomalyRule) buildAndRunQuery(ctx context.Context, orgID valuer.UUID, ts time.Time) (ruletypes.Vector, error) {
|
||||
|
||||
params, err := r.prepareQueryRange(ctx, ts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = r.PopulateTemporality(ctx, orgID, params)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("internal error while setting temporality")
|
||||
}
|
||||
params := r.prepareQueryRange(ctx, ts)
|
||||
|
||||
anomalies, err := r.provider.GetAnomalies(ctx, orgID, &anomaly.GetAnomaliesRequest{
|
||||
anomalies, err := r.provider.GetAnomalies(ctx, orgID, &anomaly.AnomaliesRequest{
|
||||
Params: params,
|
||||
Seasonality: r.seasonality,
|
||||
})
|
||||
@@ -229,87 +137,43 @@ func (r *AnomalyRule) buildAndRunQuery(ctx context.Context, orgID valuer.UUID, t
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var queryResult *v3.Result
|
||||
var queryResult *qbtypes.TimeSeriesData
|
||||
for _, result := range anomalies.Results {
|
||||
if result.QueryName == r.GetSelectedQuery() {
|
||||
if result.QueryName == r.SelectedQuery(ctx) {
|
||||
queryResult = result
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
hasData := len(queryResult.AnomalyScores) > 0
|
||||
if queryResult == nil {
|
||||
r.logger.WarnContext(ctx, "nil qb result", slog.String("rule.id", r.ID()), slog.Int64("ts", ts.UnixMilli()))
|
||||
return ruletypes.Vector{}, nil
|
||||
}
|
||||
|
||||
hasData := len(queryResult.Aggregations) > 0 &&
|
||||
queryResult.Aggregations[0] != nil &&
|
||||
len(queryResult.Aggregations[0].AnomalyScores) > 0
|
||||
|
||||
if missingDataAlert := r.HandleMissingDataAlert(ctx, ts, hasData); missingDataAlert != nil {
|
||||
return ruletypes.Vector{*missingDataAlert}, nil
|
||||
} else if !hasData {
|
||||
r.logger.WarnContext(ctx, "no anomaly result", slog.String("rule.id", r.ID()))
|
||||
return ruletypes.Vector{}, nil
|
||||
}
|
||||
|
||||
var resultVector ruletypes.Vector
|
||||
|
||||
scoresJSON, _ := json.Marshal(queryResult.AnomalyScores)
|
||||
r.logger.InfoContext(ctx, "anomaly scores", "scores", string(scoresJSON))
|
||||
|
||||
for _, series := range queryResult.AnomalyScores {
|
||||
if !r.Condition().ShouldEval(series) {
|
||||
r.logger.InfoContext(ctx, "not enough data points to evaluate series, skipping", "ruleid", r.ID(), "numPoints", len(series.Points), "requiredPoints", r.Condition().RequiredNumPoints)
|
||||
continue
|
||||
}
|
||||
results, err := r.Threshold.Eval(*series, r.Unit(), ruletypes.EvalData{
|
||||
ActiveAlerts: r.ActiveAlertsLabelFP(),
|
||||
SendUnmatched: r.ShouldSendUnmatched(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resultVector = append(resultVector, results...)
|
||||
}
|
||||
return resultVector, nil
|
||||
}
|
||||
|
||||
func (r *AnomalyRule) buildAndRunQueryV5(ctx context.Context, orgID valuer.UUID, ts time.Time) (ruletypes.Vector, error) {
|
||||
|
||||
params, err := r.prepareQueryRangeV5(ctx, ts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
anomalies, err := r.providerV2.GetAnomalies(ctx, orgID, &anomalyV2.AnomaliesRequest{
|
||||
Params: *params,
|
||||
Seasonality: anomalyV2.Seasonality{String: valuer.NewString(r.seasonality.String())},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var qbResult *qbtypes.TimeSeriesData
|
||||
for _, result := range anomalies.Results {
|
||||
if result.QueryName == r.GetSelectedQuery() {
|
||||
qbResult = result
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if qbResult == nil {
|
||||
r.logger.WarnContext(ctx, "nil qb result", "ts", ts.UnixMilli())
|
||||
}
|
||||
|
||||
queryResult := transition.ConvertV5TimeSeriesDataToV4Result(qbResult)
|
||||
|
||||
hasData := len(queryResult.AnomalyScores) > 0
|
||||
if missingDataAlert := r.HandleMissingDataAlert(ctx, ts, hasData); missingDataAlert != nil {
|
||||
return ruletypes.Vector{*missingDataAlert}, nil
|
||||
}
|
||||
|
||||
var resultVector ruletypes.Vector
|
||||
|
||||
scoresJSON, _ := json.Marshal(queryResult.AnomalyScores)
|
||||
r.logger.InfoContext(ctx, "anomaly scores", "scores", string(scoresJSON))
|
||||
scoresJSON, _ := json.Marshal(queryResult.Aggregations[0].AnomalyScores)
|
||||
// TODO(srikanthccv): this could be noisy but we do this to answer false alert requests
|
||||
r.logger.InfoContext(ctx, "anomaly scores", slog.String("rule.id", r.ID()), slog.String("anomaly.scores", string(scoresJSON)))
|
||||
|
||||
// Filter out new series if newGroupEvalDelay is configured
|
||||
seriesToProcess := queryResult.AnomalyScores
|
||||
seriesToProcess := queryResult.Aggregations[0].AnomalyScores
|
||||
if r.ShouldSkipNewGroups() {
|
||||
filteredSeries, filterErr := r.BaseRule.FilterNewSeries(ctx, ts, seriesToProcess)
|
||||
// In case of error we log the error and continue with the original series
|
||||
if filterErr != nil {
|
||||
r.logger.ErrorContext(ctx, "Error filtering new series, ", errors.Attr(filterErr), "rule_name", r.Name())
|
||||
r.logger.ErrorContext(ctx, "error filtering new series", slog.String("rule.id", r.ID()), errors.Attr(filterErr))
|
||||
} else {
|
||||
seriesToProcess = filteredSeries
|
||||
}
|
||||
@@ -317,10 +181,10 @@ func (r *AnomalyRule) buildAndRunQueryV5(ctx context.Context, orgID valuer.UUID,
|
||||
|
||||
for _, series := range seriesToProcess {
|
||||
if !r.Condition().ShouldEval(series) {
|
||||
r.logger.InfoContext(ctx, "not enough data points to evaluate series, skipping", "ruleid", r.ID(), "numPoints", len(series.Points), "requiredPoints", r.Condition().RequiredNumPoints)
|
||||
r.logger.InfoContext(ctx, "not enough data points to evaluate series, skipping", slog.String("rule.id", r.ID()), slog.Int("series.num_points", len(series.Values)), slog.Int("series.required_points", r.Condition().RequiredNumPoints))
|
||||
continue
|
||||
}
|
||||
results, err := r.Threshold.Eval(*series, r.Unit(), ruletypes.EvalData{
|
||||
results, err := r.Threshold.Eval(series, r.Unit(), ruletypes.EvalData{
|
||||
ActiveAlerts: r.ActiveAlertsLabelFP(),
|
||||
SendUnmatched: r.ShouldSendUnmatched(),
|
||||
})
|
||||
@@ -341,13 +205,9 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (int, error) {
|
||||
var res ruletypes.Vector
|
||||
var err error
|
||||
|
||||
if r.version == "v5" {
|
||||
r.logger.InfoContext(ctx, "running v5 query")
|
||||
res, err = r.buildAndRunQueryV5(ctx, r.OrgID(), ts)
|
||||
} else {
|
||||
r.logger.InfoContext(ctx, "running v4 query")
|
||||
res, err = r.buildAndRunQuery(ctx, r.OrgID(), ts)
|
||||
}
|
||||
r.logger.InfoContext(ctx, "running query", slog.String("rule.id", r.ID()))
|
||||
res, err = r.buildAndRunQuery(ctx, r.OrgID(), ts)
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -371,7 +231,7 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (int, error) {
|
||||
}
|
||||
value := valueFormatter.Format(smpl.V, r.Unit())
|
||||
threshold := valueFormatter.Format(smpl.Target, smpl.TargetUnit)
|
||||
r.logger.DebugContext(ctx, "Alert template data for rule", "rule_name", r.Name(), "formatter", valueFormatter.Name(), "value", value, "threshold", threshold)
|
||||
r.logger.DebugContext(ctx, "alert template data for rule", slog.String("rule.id", r.ID()), slog.String("formatter.name", valueFormatter.Name()), slog.String("alert.value", value), slog.String("alert.threshold", threshold))
|
||||
|
||||
tmplData := ruletypes.AlertTemplateData(l, value, threshold)
|
||||
// Inject some convenience variables that are easier to remember for users
|
||||
@@ -386,35 +246,34 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (int, error) {
|
||||
defs+text,
|
||||
"__alert_"+r.Name(),
|
||||
tmplData,
|
||||
times.Time(timestamp.FromTime(ts)),
|
||||
nil,
|
||||
)
|
||||
result, err := tmpl.Expand()
|
||||
if err != nil {
|
||||
result = fmt.Sprintf("<error expanding template: %s>", err)
|
||||
r.logger.ErrorContext(ctx, "Expanding alert template failed", errors.Attr(err), "data", tmplData, "rule_name", r.Name())
|
||||
r.logger.ErrorContext(ctx, "expanding alert template failed", slog.String("rule.id", r.ID()), errors.Attr(err), slog.Any("alert.template_data", tmplData))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
lb := labels.NewBuilder(smpl.Metric).Del(labels.MetricNameLabel).Del(labels.TemporalityLabel)
|
||||
resultLabels := labels.NewBuilder(smpl.Metric).Del(labels.MetricNameLabel).Del(labels.TemporalityLabel).Labels()
|
||||
lb := ruletypes.NewBuilder(smpl.Metric...).Del(ruletypes.MetricNameLabel).Del(ruletypes.TemporalityLabel)
|
||||
resultLabels := ruletypes.NewBuilder(smpl.Metric...).Del(ruletypes.MetricNameLabel).Del(ruletypes.TemporalityLabel).Labels()
|
||||
|
||||
for name, value := range r.Labels().Map() {
|
||||
lb.Set(name, expand(value))
|
||||
}
|
||||
|
||||
lb.Set(labels.AlertNameLabel, r.Name())
|
||||
lb.Set(labels.AlertRuleIdLabel, r.ID())
|
||||
lb.Set(labels.RuleSourceLabel, r.GeneratorURL())
|
||||
lb.Set(ruletypes.AlertNameLabel, r.Name())
|
||||
lb.Set(ruletypes.AlertRuleIDLabel, r.ID())
|
||||
lb.Set(ruletypes.RuleSourceLabel, r.GeneratorURL())
|
||||
|
||||
annotations := make(labels.Labels, 0, len(r.Annotations().Map()))
|
||||
annotations := make(ruletypes.Labels, 0, len(r.Annotations().Map()))
|
||||
for name, value := range r.Annotations().Map() {
|
||||
annotations = append(annotations, labels.Label{Name: name, Value: expand(value)})
|
||||
annotations = append(annotations, ruletypes.Label{Name: name, Value: expand(value)})
|
||||
}
|
||||
if smpl.IsMissing {
|
||||
lb.Set(labels.AlertNameLabel, "[No data] "+r.Name())
|
||||
lb.Set(labels.NoDataLabel, "true")
|
||||
lb.Set(ruletypes.AlertNameLabel, "[No data] "+r.Name())
|
||||
lb.Set(ruletypes.NoDataLabel, "true")
|
||||
}
|
||||
|
||||
lbs := lb.Labels()
|
||||
@@ -422,17 +281,17 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (int, error) {
|
||||
resultFPs[h] = struct{}{}
|
||||
|
||||
if _, ok := alerts[h]; ok {
|
||||
r.logger.ErrorContext(ctx, "the alert query returns duplicate records", "rule_id", r.ID(), "alert", alerts[h])
|
||||
err = fmt.Errorf("duplicate alert found, vector contains metrics with the same labelset after applying alert labels")
|
||||
r.logger.ErrorContext(ctx, "the alert query returns duplicate records", slog.String("rule.id", r.ID()), slog.Any("alert", alerts[h]))
|
||||
err = errors.NewInternalf(errors.CodeInternal, "duplicate alert found, vector contains metrics with the same labelset after applying alert labels")
|
||||
return 0, err
|
||||
}
|
||||
|
||||
alerts[h] = &ruletypes.Alert{
|
||||
Labels: lbs,
|
||||
QueryResultLables: resultLabels,
|
||||
QueryResultLabels: resultLabels,
|
||||
Annotations: annotations,
|
||||
ActiveAt: ts,
|
||||
State: model.StatePending,
|
||||
State: ruletypes.StatePending,
|
||||
Value: smpl.V,
|
||||
GeneratorURL: r.GeneratorURL(),
|
||||
Receivers: ruleReceiverMap[lbs.Map()[ruletypes.LabelThresholdName]],
|
||||
@@ -441,12 +300,12 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (int, error) {
|
||||
}
|
||||
}
|
||||
|
||||
r.logger.InfoContext(ctx, "number of alerts found", "rule_name", r.Name(), "alerts_count", len(alerts))
|
||||
r.logger.InfoContext(ctx, "number of alerts found", slog.String("rule.id", r.ID()), slog.Int("alert.count", len(alerts)))
|
||||
// alerts[h] is ready, add or update active list now
|
||||
for h, a := range alerts {
|
||||
// Check whether we already have alerting state for the identifying label set.
|
||||
// Update the last value and annotations if so, create a new alert entry otherwise.
|
||||
if alert, ok := r.Active[h]; ok && alert.State != model.StateInactive {
|
||||
if alert, ok := r.Active[h]; ok && alert.State != ruletypes.StateInactive {
|
||||
|
||||
alert.Value = a.Value
|
||||
alert.Annotations = a.Annotations
|
||||
@@ -462,76 +321,76 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (int, error) {
|
||||
r.Active[h] = a
|
||||
}
|
||||
|
||||
itemsToAdd := []model.RuleStateHistory{}
|
||||
itemsToAdd := []rulestatehistorytypes.RuleStateHistory{}
|
||||
|
||||
// Check if any pending alerts should be removed or fire now. Write out alert timeseries.
|
||||
for fp, a := range r.Active {
|
||||
labelsJSON, err := json.Marshal(a.QueryResultLables)
|
||||
labelsJSON, err := json.Marshal(a.QueryResultLabels)
|
||||
if err != nil {
|
||||
r.logger.ErrorContext(ctx, "error marshaling labels", errors.Attr(err), "labels", a.Labels)
|
||||
r.logger.ErrorContext(ctx, "error marshaling labels", slog.String("rule.id", r.ID()), errors.Attr(err), slog.Any("alert.labels", a.Labels))
|
||||
}
|
||||
if _, ok := resultFPs[fp]; !ok {
|
||||
// If the alert was previously firing, keep it around for a given
|
||||
// retention time so it is reported as resolved to the AlertManager.
|
||||
if a.State == model.StatePending || (!a.ResolvedAt.IsZero() && ts.Sub(a.ResolvedAt) > ruletypes.ResolvedRetention) {
|
||||
if a.State == ruletypes.StatePending || (!a.ResolvedAt.IsZero() && ts.Sub(a.ResolvedAt) > ruletypes.ResolvedRetention) {
|
||||
delete(r.Active, fp)
|
||||
}
|
||||
if a.State != model.StateInactive {
|
||||
a.State = model.StateInactive
|
||||
if a.State != ruletypes.StateInactive {
|
||||
a.State = ruletypes.StateInactive
|
||||
a.ResolvedAt = ts
|
||||
itemsToAdd = append(itemsToAdd, model.RuleStateHistory{
|
||||
itemsToAdd = append(itemsToAdd, rulestatehistorytypes.RuleStateHistory{
|
||||
RuleID: r.ID(),
|
||||
RuleName: r.Name(),
|
||||
State: model.StateInactive,
|
||||
State: ruletypes.StateInactive,
|
||||
StateChanged: true,
|
||||
UnixMilli: ts.UnixMilli(),
|
||||
Labels: model.LabelsString(labelsJSON),
|
||||
Fingerprint: a.QueryResultLables.Hash(),
|
||||
Labels: rulestatehistorytypes.LabelsString(labelsJSON),
|
||||
Fingerprint: a.QueryResultLabels.Hash(),
|
||||
Value: a.Value,
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if a.State == model.StatePending && ts.Sub(a.ActiveAt) >= r.HoldDuration().Duration() {
|
||||
a.State = model.StateFiring
|
||||
if a.State == ruletypes.StatePending && ts.Sub(a.ActiveAt) >= r.HoldDuration().Duration() {
|
||||
a.State = ruletypes.StateFiring
|
||||
a.FiredAt = ts
|
||||
state := model.StateFiring
|
||||
state := ruletypes.StateFiring
|
||||
if a.Missing {
|
||||
state = model.StateNoData
|
||||
state = ruletypes.StateNoData
|
||||
}
|
||||
itemsToAdd = append(itemsToAdd, model.RuleStateHistory{
|
||||
itemsToAdd = append(itemsToAdd, rulestatehistorytypes.RuleStateHistory{
|
||||
RuleID: r.ID(),
|
||||
RuleName: r.Name(),
|
||||
State: state,
|
||||
StateChanged: true,
|
||||
UnixMilli: ts.UnixMilli(),
|
||||
Labels: model.LabelsString(labelsJSON),
|
||||
Fingerprint: a.QueryResultLables.Hash(),
|
||||
Labels: rulestatehistorytypes.LabelsString(labelsJSON),
|
||||
Fingerprint: a.QueryResultLabels.Hash(),
|
||||
Value: a.Value,
|
||||
})
|
||||
}
|
||||
|
||||
// We need to change firing alert to recovering if the returned sample meets recovery threshold
|
||||
changeFiringToRecovering := a.State == model.StateFiring && a.IsRecovering
|
||||
changeFiringToRecovering := a.State == ruletypes.StateFiring && a.IsRecovering
|
||||
// We need to change recovering alerts to firing if the returned sample meets target threshold
|
||||
changeRecoveringToFiring := a.State == model.StateRecovering && !a.IsRecovering && !a.Missing
|
||||
changeRecoveringToFiring := a.State == ruletypes.StateRecovering && !a.IsRecovering && !a.Missing
|
||||
// in any of the above case we need to update the status of alert
|
||||
if changeFiringToRecovering || changeRecoveringToFiring {
|
||||
state := model.StateRecovering
|
||||
state := ruletypes.StateRecovering
|
||||
if changeRecoveringToFiring {
|
||||
state = model.StateFiring
|
||||
state = ruletypes.StateFiring
|
||||
}
|
||||
a.State = state
|
||||
r.logger.DebugContext(ctx, "converting alert state", "name", r.Name(), "state", state)
|
||||
itemsToAdd = append(itemsToAdd, model.RuleStateHistory{
|
||||
r.logger.DebugContext(ctx, "converting alert state", slog.String("rule.id", r.ID()), slog.Any("alert.state", state))
|
||||
itemsToAdd = append(itemsToAdd, rulestatehistorytypes.RuleStateHistory{
|
||||
RuleID: r.ID(),
|
||||
RuleName: r.Name(),
|
||||
State: state,
|
||||
StateChanged: true,
|
||||
UnixMilli: ts.UnixMilli(),
|
||||
Labels: model.LabelsString(labelsJSON),
|
||||
Fingerprint: a.QueryResultLables.Hash(),
|
||||
Labels: rulestatehistorytypes.LabelsString(labelsJSON),
|
||||
Fingerprint: a.QueryResultLabels.Hash(),
|
||||
Value: a.Value,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,21 +2,19 @@ package rules
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/SigNoz/signoz/ee/query-service/anomaly"
|
||||
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/clickhouseReader"
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore/telemetrystoretest"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
|
||||
"github.com/SigNoz/signoz/ee/anomaly"
|
||||
)
|
||||
|
||||
// mockAnomalyProvider is a mock implementation of anomaly.Provider for testing.
|
||||
@@ -24,13 +22,13 @@ import (
|
||||
// time periods (current, past period, current season, past season, past 2 seasons,
|
||||
// past 3 seasons), making it cumbersome to create mock data.
|
||||
type mockAnomalyProvider struct {
|
||||
responses []*anomaly.GetAnomaliesResponse
|
||||
responses []*anomaly.AnomaliesResponse
|
||||
callCount int
|
||||
}
|
||||
|
||||
func (m *mockAnomalyProvider) GetAnomalies(ctx context.Context, orgID valuer.UUID, req *anomaly.GetAnomaliesRequest) (*anomaly.GetAnomaliesResponse, error) {
|
||||
func (m *mockAnomalyProvider) GetAnomalies(ctx context.Context, orgID valuer.UUID, req *anomaly.AnomaliesRequest) (*anomaly.AnomaliesResponse, error) {
|
||||
if m.callCount >= len(m.responses) {
|
||||
return &anomaly.GetAnomaliesResponse{Results: []*v3.Result{}}, nil
|
||||
return &anomaly.AnomaliesResponse{Results: []*qbtypes.TimeSeriesData{}}, nil
|
||||
}
|
||||
resp := m.responses[m.callCount]
|
||||
m.callCount++
|
||||
@@ -49,45 +47,46 @@ func TestAnomalyRule_NoData_AlertOnAbsent(t *testing.T) {
|
||||
postableRule := ruletypes.PostableRule{
|
||||
AlertName: "Test anomaly no data",
|
||||
AlertType: ruletypes.AlertTypeMetric,
|
||||
RuleType: RuleTypeAnomaly,
|
||||
RuleType: ruletypes.RuleTypeAnomaly,
|
||||
Evaluation: &ruletypes.EvaluationEnvelope{Kind: ruletypes.RollingEvaluation, Spec: ruletypes.RollingWindow{
|
||||
EvalWindow: evalWindow,
|
||||
Frequency: valuer.MustParseTextDuration("1m"),
|
||||
}},
|
||||
RuleCondition: &ruletypes.RuleCondition{
|
||||
CompareOp: ruletypes.ValueIsAbove,
|
||||
MatchType: ruletypes.AtleastOnce,
|
||||
Target: &target,
|
||||
CompositeQuery: &v3.CompositeQuery{
|
||||
QueryType: v3.QueryTypeBuilder,
|
||||
BuilderQueries: map[string]*v3.BuilderQuery{
|
||||
"A": {
|
||||
QueryName: "A",
|
||||
Expression: "A",
|
||||
DataSource: v3.DataSourceMetrics,
|
||||
Temporality: v3.Unspecified,
|
||||
CompareOperator: ruletypes.ValueIsAbove,
|
||||
MatchType: ruletypes.AtleastOnce,
|
||||
Target: &target,
|
||||
CompositeQuery: &ruletypes.AlertCompositeQuery{
|
||||
QueryType: ruletypes.QueryTypeBuilder,
|
||||
Queries: []qbtypes.QueryEnvelope{{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Name: "A",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
SelectedQuery: "A",
|
||||
Seasonality: "daily",
|
||||
Thresholds: &ruletypes.RuleThresholdData{
|
||||
Kind: ruletypes.BasicThresholdKind,
|
||||
Spec: ruletypes.BasicRuleThresholds{{
|
||||
Name: "Test anomaly no data",
|
||||
TargetValue: &target,
|
||||
MatchType: ruletypes.AtleastOnce,
|
||||
CompareOp: ruletypes.ValueIsAbove,
|
||||
Name: "Test anomaly no data",
|
||||
TargetValue: &target,
|
||||
MatchType: ruletypes.AtleastOnce,
|
||||
CompareOperator: ruletypes.ValueIsAbove,
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
responseNoData := &anomaly.GetAnomaliesResponse{
|
||||
Results: []*v3.Result{
|
||||
responseNoData := &anomaly.AnomaliesResponse{
|
||||
Results: []*qbtypes.TimeSeriesData{
|
||||
{
|
||||
QueryName: "A",
|
||||
AnomalyScores: []*v3.Series{},
|
||||
QueryName: "A",
|
||||
Aggregations: []*qbtypes.AggregationBucket{{
|
||||
AnomalyScores: []*qbtypes.TimeSeries{},
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -115,23 +114,17 @@ func TestAnomalyRule_NoData_AlertOnAbsent(t *testing.T) {
|
||||
t.Run(c.description, func(t *testing.T) {
|
||||
postableRule.RuleCondition.AlertOnAbsent = c.alertOnAbsent
|
||||
|
||||
telemetryStore := telemetrystoretest.New(telemetrystore.Config{}, nil)
|
||||
options := clickhouseReader.NewOptions("primaryNamespace")
|
||||
reader := clickhouseReader.NewReader(slog.Default(), nil, telemetryStore, nil, "", time.Second, nil, nil, options)
|
||||
|
||||
rule, err := NewAnomalyRule(
|
||||
"test-anomaly-rule",
|
||||
valuer.GenerateUUID(),
|
||||
&postableRule,
|
||||
reader,
|
||||
nil,
|
||||
logger,
|
||||
nil,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
rule.provider = &mockAnomalyProvider{
|
||||
responses: []*anomaly.GetAnomaliesResponse{responseNoData},
|
||||
responses: []*anomaly.AnomaliesResponse{responseNoData},
|
||||
}
|
||||
|
||||
alertsFound, err := rule.Eval(context.Background(), evalTime)
|
||||
@@ -156,46 +149,47 @@ func TestAnomalyRule_NoData_AbsentFor(t *testing.T) {
|
||||
postableRule := ruletypes.PostableRule{
|
||||
AlertName: "Test anomaly no data with AbsentFor",
|
||||
AlertType: ruletypes.AlertTypeMetric,
|
||||
RuleType: RuleTypeAnomaly,
|
||||
RuleType: ruletypes.RuleTypeAnomaly,
|
||||
Evaluation: &ruletypes.EvaluationEnvelope{Kind: ruletypes.RollingEvaluation, Spec: ruletypes.RollingWindow{
|
||||
EvalWindow: evalWindow,
|
||||
Frequency: valuer.MustParseTextDuration("1m"),
|
||||
}},
|
||||
RuleCondition: &ruletypes.RuleCondition{
|
||||
CompareOp: ruletypes.ValueIsAbove,
|
||||
MatchType: ruletypes.AtleastOnce,
|
||||
AlertOnAbsent: true,
|
||||
Target: &target,
|
||||
CompositeQuery: &v3.CompositeQuery{
|
||||
QueryType: v3.QueryTypeBuilder,
|
||||
BuilderQueries: map[string]*v3.BuilderQuery{
|
||||
"A": {
|
||||
QueryName: "A",
|
||||
Expression: "A",
|
||||
DataSource: v3.DataSourceMetrics,
|
||||
Temporality: v3.Unspecified,
|
||||
CompareOperator: ruletypes.ValueIsAbove,
|
||||
MatchType: ruletypes.AtleastOnce,
|
||||
AlertOnAbsent: true,
|
||||
Target: &target,
|
||||
CompositeQuery: &ruletypes.AlertCompositeQuery{
|
||||
QueryType: ruletypes.QueryTypeBuilder,
|
||||
Queries: []qbtypes.QueryEnvelope{{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Name: "A",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
SelectedQuery: "A",
|
||||
Seasonality: "daily",
|
||||
Thresholds: &ruletypes.RuleThresholdData{
|
||||
Kind: ruletypes.BasicThresholdKind,
|
||||
Spec: ruletypes.BasicRuleThresholds{{
|
||||
Name: "Test anomaly no data with AbsentFor",
|
||||
TargetValue: &target,
|
||||
MatchType: ruletypes.AtleastOnce,
|
||||
CompareOp: ruletypes.ValueIsAbove,
|
||||
Name: "Test anomaly no data with AbsentFor",
|
||||
TargetValue: &target,
|
||||
MatchType: ruletypes.AtleastOnce,
|
||||
CompareOperator: ruletypes.ValueIsAbove,
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
responseNoData := &anomaly.GetAnomaliesResponse{
|
||||
Results: []*v3.Result{
|
||||
responseNoData := &anomaly.AnomaliesResponse{
|
||||
Results: []*qbtypes.TimeSeriesData{
|
||||
{
|
||||
QueryName: "A",
|
||||
AnomalyScores: []*v3.Series{},
|
||||
QueryName: "A",
|
||||
Aggregations: []*qbtypes.AggregationBucket{{
|
||||
AnomalyScores: []*qbtypes.TimeSeries{},
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -229,32 +223,35 @@ func TestAnomalyRule_NoData_AbsentFor(t *testing.T) {
|
||||
t1 := baseTime.Add(5 * time.Minute)
|
||||
t2 := t1.Add(c.timeBetweenEvals)
|
||||
|
||||
responseWithData := &anomaly.GetAnomaliesResponse{
|
||||
Results: []*v3.Result{
|
||||
responseWithData := &anomaly.AnomaliesResponse{
|
||||
Results: []*qbtypes.TimeSeriesData{
|
||||
{
|
||||
QueryName: "A",
|
||||
AnomalyScores: []*v3.Series{
|
||||
{
|
||||
Labels: map[string]string{"test": "label"},
|
||||
Points: []v3.Point{
|
||||
{Timestamp: baseTime.UnixMilli(), Value: 1.0},
|
||||
{Timestamp: baseTime.Add(time.Minute).UnixMilli(), Value: 1.5},
|
||||
Aggregations: []*qbtypes.AggregationBucket{{
|
||||
AnomalyScores: []*qbtypes.TimeSeries{
|
||||
{
|
||||
Labels: []*qbtypes.Label{
|
||||
{
|
||||
Key: telemetrytypes.TelemetryFieldKey{Name: "Test"},
|
||||
Value: "labels",
|
||||
},
|
||||
},
|
||||
Values: []*qbtypes.TimeSeriesValue{
|
||||
{Timestamp: baseTime.UnixMilli(), Value: 1.0},
|
||||
{Timestamp: baseTime.Add(time.Minute).UnixMilli(), Value: 1.5},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
telemetryStore := telemetrystoretest.New(telemetrystore.Config{}, nil)
|
||||
options := clickhouseReader.NewOptions("primaryNamespace")
|
||||
reader := clickhouseReader.NewReader(slog.Default(), nil, telemetryStore, nil, "", time.Second, nil, nil, options)
|
||||
|
||||
rule, err := NewAnomalyRule("test-anomaly-rule", valuer.GenerateUUID(), &postableRule, reader, nil, logger, nil)
|
||||
rule, err := NewAnomalyRule("test-anomaly-rule", valuer.GenerateUUID(), &postableRule, nil, logger)
|
||||
require.NoError(t, err)
|
||||
|
||||
rule.provider = &mockAnomalyProvider{
|
||||
responses: []*anomaly.GetAnomaliesResponse{responseWithData, responseNoData},
|
||||
responses: []*anomaly.AnomaliesResponse{responseWithData, responseNoData},
|
||||
}
|
||||
|
||||
alertsFound1, err := rule.Eval(context.Background(), t1)
|
||||
|
||||
@@ -11,9 +11,7 @@ import (
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
baserules "github.com/SigNoz/signoz/pkg/query-service/rules"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils/labels"
|
||||
"github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
@@ -23,7 +21,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
|
||||
rules := make([]baserules.Rule, 0)
|
||||
var task baserules.Task
|
||||
|
||||
ruleId := baserules.RuleIdFromTaskName(opts.TaskName)
|
||||
ruleID := baserules.RuleIDFromTaskName(opts.TaskName)
|
||||
evaluation, err := opts.Rule.Evaluation.GetEvaluation()
|
||||
if err != nil {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "evaluation is invalid: %v", err)
|
||||
@@ -32,10 +30,9 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
|
||||
if opts.Rule.RuleType == ruletypes.RuleTypeThreshold {
|
||||
// create a threshold rule
|
||||
tr, err := baserules.NewThresholdRule(
|
||||
ruleId,
|
||||
ruleID,
|
||||
opts.OrgID,
|
||||
opts.Rule,
|
||||
opts.Reader,
|
||||
opts.Querier,
|
||||
opts.Logger,
|
||||
baserules.WithEvalDelay(opts.ManagerOpts.EvalDelay),
|
||||
@@ -58,11 +55,10 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
|
||||
|
||||
// create promql rule
|
||||
pr, err := baserules.NewPromRule(
|
||||
ruleId,
|
||||
ruleID,
|
||||
opts.OrgID,
|
||||
opts.Rule,
|
||||
opts.Logger,
|
||||
opts.Reader,
|
||||
opts.ManagerOpts.Prometheus,
|
||||
baserules.WithSQLStore(opts.SQLStore),
|
||||
baserules.WithQueryParser(opts.ManagerOpts.QueryParser),
|
||||
@@ -82,13 +78,11 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
|
||||
} else if opts.Rule.RuleType == ruletypes.RuleTypeAnomaly {
|
||||
// create anomaly rule
|
||||
ar, err := NewAnomalyRule(
|
||||
ruleId,
|
||||
ruleID,
|
||||
opts.OrgID,
|
||||
opts.Rule,
|
||||
opts.Reader,
|
||||
opts.Querier,
|
||||
opts.Logger,
|
||||
opts.Cache,
|
||||
baserules.WithEvalDelay(opts.ManagerOpts.EvalDelay),
|
||||
baserules.WithSQLStore(opts.SQLStore),
|
||||
baserules.WithQueryParser(opts.ManagerOpts.QueryParser),
|
||||
@@ -105,7 +99,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
|
||||
task = newTask(baserules.TaskTypeCh, opts.TaskName, evaluation.GetFrequency().Duration(), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
|
||||
|
||||
} else {
|
||||
return nil, fmt.Errorf("unsupported rule type %s. Supported types: %s, %s", opts.Rule.RuleType, ruletypes.RuleTypeProm, ruletypes.RuleTypeThreshold)
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported rule type %s. Supported types: %s, %s", opts.Rule.RuleType, ruletypes.RuleTypeProm, ruletypes.RuleTypeThreshold)
|
||||
}
|
||||
|
||||
return task, nil
|
||||
@@ -113,12 +107,12 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
|
||||
|
||||
// TestNotification prepares a dummy rule for given rule parameters and
|
||||
// sends a test notification. returns alert count and error (if any)
|
||||
func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.ApiError) {
|
||||
func TestNotification(opts baserules.PrepareTestRuleOptions) (int, error) {
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
if opts.Rule == nil {
|
||||
return 0, basemodel.BadRequest(fmt.Errorf("rule is required"))
|
||||
return 0, errors.NewInvalidInputf(errors.CodeInvalidInput, "rule is required")
|
||||
}
|
||||
|
||||
parsedRule := opts.Rule
|
||||
@@ -138,15 +132,14 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
|
||||
if parsedRule.RuleType == ruletypes.RuleTypeThreshold {
|
||||
|
||||
// add special labels for test alerts
|
||||
parsedRule.Labels[labels.RuleSourceLabel] = ""
|
||||
parsedRule.Labels[labels.AlertRuleIdLabel] = ""
|
||||
parsedRule.Labels[ruletypes.RuleSourceLabel] = ""
|
||||
parsedRule.Labels[ruletypes.AlertRuleIDLabel] = ""
|
||||
|
||||
// create a threshold rule
|
||||
rule, err = baserules.NewThresholdRule(
|
||||
alertname,
|
||||
opts.OrgID,
|
||||
parsedRule,
|
||||
opts.Reader,
|
||||
opts.Querier,
|
||||
opts.Logger,
|
||||
baserules.WithSendAlways(),
|
||||
@@ -158,7 +151,7 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
|
||||
|
||||
if err != nil {
|
||||
slog.Error("failed to prepare a new threshold rule for test", "name", alertname, errors.Attr(err))
|
||||
return 0, basemodel.BadRequest(err)
|
||||
return 0, err
|
||||
}
|
||||
|
||||
} else if parsedRule.RuleType == ruletypes.RuleTypeProm {
|
||||
@@ -169,7 +162,6 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
|
||||
opts.OrgID,
|
||||
parsedRule,
|
||||
opts.Logger,
|
||||
opts.Reader,
|
||||
opts.ManagerOpts.Prometheus,
|
||||
baserules.WithSendAlways(),
|
||||
baserules.WithSendUnmatched(),
|
||||
@@ -180,7 +172,7 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
|
||||
|
||||
if err != nil {
|
||||
slog.Error("failed to prepare a new promql rule for test", "name", alertname, errors.Attr(err))
|
||||
return 0, basemodel.BadRequest(err)
|
||||
return 0, err
|
||||
}
|
||||
} else if parsedRule.RuleType == ruletypes.RuleTypeAnomaly {
|
||||
// create anomaly rule
|
||||
@@ -188,10 +180,8 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
|
||||
alertname,
|
||||
opts.OrgID,
|
||||
parsedRule,
|
||||
opts.Reader,
|
||||
opts.Querier,
|
||||
opts.Logger,
|
||||
opts.Cache,
|
||||
baserules.WithSendAlways(),
|
||||
baserules.WithSendUnmatched(),
|
||||
baserules.WithSQLStore(opts.SQLStore),
|
||||
@@ -200,10 +190,10 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
|
||||
)
|
||||
if err != nil {
|
||||
slog.Error("failed to prepare a new anomaly rule for test", "name", alertname, errors.Attr(err))
|
||||
return 0, basemodel.BadRequest(err)
|
||||
return 0, err
|
||||
}
|
||||
} else {
|
||||
return 0, basemodel.BadRequest(fmt.Errorf("failed to derive ruletype with given information"))
|
||||
return 0, errors.NewInvalidInputf(errors.CodeInvalidInput, "failed to derive ruletype with given information")
|
||||
}
|
||||
|
||||
// set timestamp to current utc time
|
||||
@@ -212,7 +202,7 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
|
||||
alertsFound, err := rule.Eval(ctx, ts)
|
||||
if err != nil {
|
||||
slog.Error("evaluating rule failed", "rule", rule.Name(), errors.Attr(err))
|
||||
return 0, basemodel.InternalError(fmt.Errorf("rule evaluation failed"))
|
||||
return 0, err
|
||||
}
|
||||
rule.SendAlerts(ctx, ts, 0, time.Minute, opts.NotifyFunc)
|
||||
|
||||
|
||||
@@ -114,11 +114,8 @@ func TestManager_TestNotification_SendUnmatched_ThresholdRule(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
count, apiErr := mgr.TestNotification(context.Background(), orgID, string(ruleBytes))
|
||||
if apiErr != nil {
|
||||
t.Logf("TestNotification error: %v, type: %s", apiErr.Err, apiErr.Typ)
|
||||
}
|
||||
require.Nil(t, apiErr)
|
||||
count, err := mgr.TestNotification(context.Background(), orgID, string(ruleBytes))
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, tc.ExpectAlerts, count)
|
||||
|
||||
if tc.ExpectAlerts > 0 {
|
||||
@@ -268,11 +265,8 @@ func TestManager_TestNotification_SendUnmatched_PromRule(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
count, apiErr := mgr.TestNotification(context.Background(), orgID, string(ruleBytes))
|
||||
if apiErr != nil {
|
||||
t.Logf("TestNotification error: %v, type: %s", apiErr.Err, apiErr.Typ)
|
||||
}
|
||||
require.Nil(t, apiErr)
|
||||
count, err := mgr.TestNotification(context.Background(), orgID, string(ruleBytes))
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, tc.ExpectAlerts, count)
|
||||
|
||||
if tc.ExpectAlerts > 0 {
|
||||
|
||||
@@ -67,9 +67,12 @@ jest.mock('hooks/useGetTenantLicense', () => ({
|
||||
|
||||
// Mock react-query for users fetch
|
||||
let mockUsersData: { email: string }[] = [];
|
||||
jest.mock('api/v1/user/get', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(() => Promise.resolve({ data: mockUsersData })),
|
||||
jest.mock('api/generated/services/users', () => ({
|
||||
...jest.requireActual('api/generated/services/users'),
|
||||
useListUsers: jest.fn(() => ({
|
||||
data: { data: mockUsersData },
|
||||
isFetching: false,
|
||||
})),
|
||||
}));
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
|
||||
@@ -425,39 +425,6 @@ export interface AuthtypesSessionContextDTO {
|
||||
orgs?: AuthtypesOrgSessionContextDTO[] | null;
|
||||
}
|
||||
|
||||
export interface AuthtypesStorableRoleDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
orgId?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
type?: string;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: Date;
|
||||
}
|
||||
|
||||
export interface AuthtypesTransactionDTO {
|
||||
object: AuthtypesObjectDTO;
|
||||
/**
|
||||
@@ -475,25 +442,25 @@ export interface AuthtypesUserRoleDTO {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: Date;
|
||||
createdAt: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id: string;
|
||||
role?: AuthtypesStorableRoleDTO;
|
||||
role: AuthtypesRoleDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
roleId?: string;
|
||||
roleId: string;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: Date;
|
||||
updatedAt: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
userId?: string;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export interface AuthtypesUserWithRolesDTO {
|
||||
@@ -2710,14 +2677,6 @@ export interface RenderErrorResponseDTO {
|
||||
status: string;
|
||||
}
|
||||
|
||||
export enum RulestatehistorytypesAlertStateDTO {
|
||||
inactive = 'inactive',
|
||||
pending = 'pending',
|
||||
recovering = 'recovering',
|
||||
firing = 'firing',
|
||||
nodata = 'nodata',
|
||||
disabled = 'disabled',
|
||||
}
|
||||
export interface RulestatehistorytypesGettableRuleStateHistoryDTO {
|
||||
/**
|
||||
* @type integer
|
||||
@@ -2729,7 +2688,7 @@ export interface RulestatehistorytypesGettableRuleStateHistoryDTO {
|
||||
* @nullable true
|
||||
*/
|
||||
labels: Querybuildertypesv5LabelDTO[] | null;
|
||||
overallState: RulestatehistorytypesAlertStateDTO;
|
||||
overallState: RuletypesAlertStateDTO;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
@@ -2737,12 +2696,12 @@ export interface RulestatehistorytypesGettableRuleStateHistoryDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
ruleID: string;
|
||||
ruleId: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
ruleName: string;
|
||||
state: RulestatehistorytypesAlertStateDTO;
|
||||
state: RuletypesAlertStateDTO;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
@@ -2840,9 +2799,17 @@ export interface RulestatehistorytypesGettableRuleStateWindowDTO {
|
||||
* @format int64
|
||||
*/
|
||||
start: number;
|
||||
state: RulestatehistorytypesAlertStateDTO;
|
||||
state: RuletypesAlertStateDTO;
|
||||
}
|
||||
|
||||
export enum RuletypesAlertStateDTO {
|
||||
inactive = 'inactive',
|
||||
pending = 'pending',
|
||||
recovering = 'recovering',
|
||||
firing = 'firing',
|
||||
nodata = 'nodata',
|
||||
disabled = 'disabled',
|
||||
}
|
||||
export interface ServiceaccounttypesGettableFactorAPIKeyDTO {
|
||||
/**
|
||||
* @type string
|
||||
@@ -4613,7 +4580,7 @@ export type GetRuleHistoryTimelineParams = {
|
||||
/**
|
||||
* @description undefined
|
||||
*/
|
||||
state?: RulestatehistorytypesAlertStateDTO;
|
||||
state?: RuletypesAlertStateDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @description undefined
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
.announcement-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-4);
|
||||
padding: var(--padding-2) var(--padding-4);
|
||||
height: 40px;
|
||||
font-family: var(--font-sans), sans-serif;
|
||||
font-size: var(--label-base-500-font-size);
|
||||
line-height: var(--label-base-500-line-height);
|
||||
font-weight: var(--label-base-500-font-weight);
|
||||
letter-spacing: -0.065px;
|
||||
|
||||
&--warning {
|
||||
background-color: var(--callout-warning-background);
|
||||
color: var(--callout-warning-description);
|
||||
.announcement-banner__action,
|
||||
.announcement-banner__dismiss {
|
||||
background: var(--callout-warning-border);
|
||||
}
|
||||
}
|
||||
|
||||
&--info {
|
||||
background-color: var(--callout-primary-background);
|
||||
color: var(--callout-primary-description);
|
||||
.announcement-banner__action,
|
||||
.announcement-banner__dismiss {
|
||||
background: var(--callout-primary-border);
|
||||
}
|
||||
}
|
||||
|
||||
&--error {
|
||||
background-color: var(--callout-error-background);
|
||||
color: var(--callout-error-description);
|
||||
.announcement-banner__action,
|
||||
.announcement-banner__dismiss {
|
||||
background: var(--callout-error-border);
|
||||
}
|
||||
}
|
||||
|
||||
&--success {
|
||||
background-color: var(--callout-success-background);
|
||||
color: var(--callout-success-description);
|
||||
.announcement-banner__action,
|
||||
.announcement-banner__dismiss {
|
||||
background: var(--callout-success-border);
|
||||
}
|
||||
}
|
||||
|
||||
&__body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__message {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
line-height: var(--line-height-normal);
|
||||
|
||||
strong {
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
}
|
||||
|
||||
&__action {
|
||||
height: 24px;
|
||||
font-size: var(--label-small-500-font-size);
|
||||
color: currentColor;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
&__dismiss {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
color: currentColor;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
import { render, screen, userEvent } from 'tests/test-utils';
|
||||
|
||||
import {
|
||||
AnnouncementBanner,
|
||||
AnnouncementBannerProps,
|
||||
PersistedAnnouncementBanner,
|
||||
} from './index';
|
||||
|
||||
const STORAGE_KEY = 'test-banner-dismissed';
|
||||
|
||||
function renderBanner(props: Partial<AnnouncementBannerProps> = {}): void {
|
||||
render(<AnnouncementBanner message="Test message" {...props} />);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
});
|
||||
|
||||
describe('AnnouncementBanner', () => {
|
||||
it('renders message and default warning variant', () => {
|
||||
renderBanner({ message: <strong>Heads up</strong> });
|
||||
|
||||
const alert = screen.getByRole('alert');
|
||||
expect(alert).toHaveClass('announcement-banner--warning');
|
||||
expect(alert).toHaveTextContent('Heads up');
|
||||
});
|
||||
|
||||
it.each(['warning', 'info', 'success', 'error'] as const)(
|
||||
'renders %s variant correctly',
|
||||
(type) => {
|
||||
renderBanner({ type, message: 'Test message' });
|
||||
const alert = screen.getByRole('alert');
|
||||
expect(alert).toHaveClass(`announcement-banner--${type}`);
|
||||
},
|
||||
);
|
||||
|
||||
it('calls action onClick when action button is clicked', async () => {
|
||||
const onClick = jest.fn() as jest.MockedFunction<() => void>;
|
||||
renderBanner({ action: { label: 'Go to Settings', onClick } });
|
||||
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
await user.click(screen.getByRole('button', { name: /go to settings/i }));
|
||||
|
||||
expect(onClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('hides dismiss button when onClose is not provided and hides icon when icon is null', () => {
|
||||
renderBanner({ onClose: undefined, icon: null });
|
||||
|
||||
expect(
|
||||
screen.queryByRole('button', { name: /dismiss/i }),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole('alert')?.querySelector('.announcement-banner__icon'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('PersistedAnnouncementBanner', () => {
|
||||
it('dismisses on click, calls onDismiss, and persists to localStorage', async () => {
|
||||
const onDismiss = jest.fn() as jest.MockedFunction<() => void>;
|
||||
render(
|
||||
<PersistedAnnouncementBanner
|
||||
message="Test message"
|
||||
storageKey={STORAGE_KEY}
|
||||
onDismiss={onDismiss}
|
||||
/>,
|
||||
);
|
||||
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
await user.click(screen.getByRole('button', { name: /dismiss/i }));
|
||||
|
||||
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
|
||||
expect(onDismiss).toHaveBeenCalledTimes(1);
|
||||
expect(localStorage.getItem(STORAGE_KEY)).toBe('true');
|
||||
});
|
||||
|
||||
it('does not render when storageKey is already set in localStorage', () => {
|
||||
localStorage.setItem(STORAGE_KEY, 'true');
|
||||
render(
|
||||
<PersistedAnnouncementBanner
|
||||
message="Test message"
|
||||
storageKey={STORAGE_KEY}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,84 +0,0 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { Button } from '@signozhq/button';
|
||||
import {
|
||||
CircleAlert,
|
||||
CircleCheckBig,
|
||||
Info,
|
||||
TriangleAlert,
|
||||
X,
|
||||
} from '@signozhq/icons';
|
||||
import cx from 'classnames';
|
||||
|
||||
import './AnnouncementBanner.styles.scss';
|
||||
|
||||
export type AnnouncementBannerType = 'warning' | 'info' | 'error' | 'success';
|
||||
|
||||
export interface AnnouncementBannerAction {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export interface AnnouncementBannerProps {
|
||||
message: ReactNode;
|
||||
type?: AnnouncementBannerType;
|
||||
icon?: ReactNode | null;
|
||||
action?: AnnouncementBannerAction;
|
||||
onClose?: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const DEFAULT_ICONS: Record<AnnouncementBannerType, ReactNode> = {
|
||||
warning: <TriangleAlert size={14} />,
|
||||
info: <Info size={14} />,
|
||||
error: <CircleAlert size={14} />,
|
||||
success: <CircleCheckBig size={14} />,
|
||||
};
|
||||
|
||||
export default function AnnouncementBanner({
|
||||
message,
|
||||
type = 'warning',
|
||||
icon,
|
||||
action,
|
||||
onClose,
|
||||
className,
|
||||
}: AnnouncementBannerProps): JSX.Element {
|
||||
const resolvedIcon = icon === null ? null : icon ?? DEFAULT_ICONS[type];
|
||||
|
||||
return (
|
||||
<div
|
||||
role="alert"
|
||||
className={cx(
|
||||
'announcement-banner',
|
||||
`announcement-banner--${type}`,
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="announcement-banner__body">
|
||||
{resolvedIcon && (
|
||||
<span className="announcement-banner__icon">{resolvedIcon}</span>
|
||||
)}
|
||||
<span className="announcement-banner__message">{message}</span>
|
||||
{action && (
|
||||
<Button
|
||||
type="button"
|
||||
className="announcement-banner__action"
|
||||
onClick={action.onClick}
|
||||
>
|
||||
{action.label}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{onClose && (
|
||||
<Button
|
||||
type="button"
|
||||
aria-label="Dismiss"
|
||||
className="announcement-banner__dismiss"
|
||||
onClick={onClose}
|
||||
>
|
||||
<X size={14} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import AnnouncementBanner, {
|
||||
AnnouncementBannerProps,
|
||||
} from './AnnouncementBanner';
|
||||
|
||||
interface PersistedAnnouncementBannerProps extends AnnouncementBannerProps {
|
||||
storageKey: string;
|
||||
onDismiss?: () => void;
|
||||
}
|
||||
|
||||
function isDismissed(storageKey: string): boolean {
|
||||
return localStorage.getItem(storageKey) === 'true';
|
||||
}
|
||||
|
||||
export default function PersistedAnnouncementBanner({
|
||||
storageKey,
|
||||
onDismiss,
|
||||
...props
|
||||
}: PersistedAnnouncementBannerProps): JSX.Element | null {
|
||||
const [visible, setVisible] = useState(() => !isDismissed(storageKey));
|
||||
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleClose = (): void => {
|
||||
localStorage.setItem(storageKey, 'true');
|
||||
setVisible(false);
|
||||
onDismiss?.();
|
||||
};
|
||||
|
||||
return <AnnouncementBanner {...props} onClose={handleClose} />;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import AnnouncementBanner from './AnnouncementBanner';
|
||||
import PersistedAnnouncementBanner from './PersistedAnnouncementBanner';
|
||||
|
||||
export type {
|
||||
AnnouncementBannerAction,
|
||||
AnnouncementBannerProps,
|
||||
AnnouncementBannerType,
|
||||
} from './AnnouncementBanner';
|
||||
|
||||
export { AnnouncementBanner, PersistedAnnouncementBanner };
|
||||
|
||||
export default AnnouncementBanner;
|
||||
@@ -0,0 +1,75 @@
|
||||
import { Button } from '@signozhq/button';
|
||||
import { DialogFooter, DialogWrapper } from '@signozhq/dialog';
|
||||
import { Trash2, X } from '@signozhq/icons';
|
||||
import { MemberRow } from 'components/MembersTable/MembersTable';
|
||||
|
||||
interface DeleteMemberDialogProps {
|
||||
open: boolean;
|
||||
isInvited: boolean;
|
||||
member: MemberRow | null;
|
||||
isDeleting: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
}
|
||||
|
||||
function DeleteMemberDialog({
|
||||
open,
|
||||
isInvited,
|
||||
member,
|
||||
isDeleting,
|
||||
onClose,
|
||||
onConfirm,
|
||||
}: DeleteMemberDialogProps): JSX.Element {
|
||||
const title = isInvited ? 'Revoke Invite' : 'Delete Member';
|
||||
|
||||
const body = isInvited ? (
|
||||
<>
|
||||
Are you sure you want to revoke the invite for{' '}
|
||||
<strong>{member?.email}</strong>? They will no longer be able to join the
|
||||
workspace using this invite.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Are you sure you want to delete{' '}
|
||||
<strong>{member?.name || member?.email}</strong>? This will remove their
|
||||
access to the workspace.
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<DialogWrapper
|
||||
open={open}
|
||||
onOpenChange={(isOpen): void => {
|
||||
if (!isOpen) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
title={title}
|
||||
width="narrow"
|
||||
className="alert-dialog delete-dialog"
|
||||
showCloseButton={false}
|
||||
disableOutsideClick={false}
|
||||
>
|
||||
<p className="delete-dialog__body">{body}</p>
|
||||
|
||||
<DialogFooter className="delete-dialog__footer">
|
||||
<Button variant="solid" color="secondary" size="sm" onClick={onClose}>
|
||||
<X size={12} />
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="destructive"
|
||||
size="sm"
|
||||
disabled={isDeleting}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
{isDeleting ? 'Processing...' : title}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default DeleteMemberDialog;
|
||||
@@ -45,8 +45,8 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 32px;
|
||||
padding: 0 var(--padding-2);
|
||||
min-height: 32px;
|
||||
padding: var(--padding-1) var(--padding-2);
|
||||
border-radius: 2px;
|
||||
background: var(--l2-background);
|
||||
border: 1px solid var(--border);
|
||||
@@ -57,6 +57,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__disabled-roles {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-2);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__email-text {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-normal);
|
||||
@@ -78,21 +85,23 @@
|
||||
|
||||
&__role-select {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
|
||||
.ant-select-selector {
|
||||
background-color: var(--l2-background) !important;
|
||||
border-color: var(--border) !important;
|
||||
border-radius: 2px;
|
||||
padding: 0 var(--padding-2) !important;
|
||||
padding: var(--padding-1) var(--padding-2) !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
min-height: 32px;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
.ant-select-selection-item {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--l1-foreground);
|
||||
line-height: 32px;
|
||||
line-height: 22px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
@@ -168,6 +177,10 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__tooltip-wrapper {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
&__footer-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -2,38 +2,69 @@ import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { Badge } from '@signozhq/badge';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { DialogFooter, DialogWrapper } from '@signozhq/dialog';
|
||||
import { DrawerWrapper } from '@signozhq/drawer';
|
||||
import {
|
||||
Check,
|
||||
ChevronDown,
|
||||
Copy,
|
||||
LockKeyhole,
|
||||
RefreshCw,
|
||||
Trash2,
|
||||
X,
|
||||
} from '@signozhq/icons';
|
||||
import { LockKeyhole, RefreshCw, Trash2, X } from '@signozhq/icons';
|
||||
import { Input } from '@signozhq/input';
|
||||
import { toast } from '@signozhq/sonner';
|
||||
import { Select } from 'antd';
|
||||
import { Skeleton, Tooltip } from 'antd';
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import type { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import {
|
||||
getResetPasswordToken,
|
||||
useDeleteUser,
|
||||
useUpdateUserDeprecated,
|
||||
useGetUser,
|
||||
useUpdateMyUserV2,
|
||||
useUpdateUser,
|
||||
} from 'api/generated/services/users';
|
||||
import { AxiosError } from 'axios';
|
||||
import { MemberRow } from 'components/MembersTable/MembersTable';
|
||||
import RolesSelect, { useRoles } from 'components/RolesSelect';
|
||||
import SaveErrorItem from 'components/ServiceAccountDrawer/SaveErrorItem';
|
||||
import type { SaveError } from 'components/ServiceAccountDrawer/utils';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { MemberStatus } from 'container/MembersSettings/utils';
|
||||
import { capitalize } from 'lodash-es';
|
||||
import {
|
||||
MemberRoleUpdateFailure,
|
||||
useMemberRoleManager,
|
||||
} from 'hooks/member/useMemberRoleManager';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { ROLES } from 'types/roles';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
import APIError from 'types/api/error';
|
||||
import { toAPIError } from 'utils/errorUtils';
|
||||
|
||||
import DeleteMemberDialog from './DeleteMemberDialog';
|
||||
import ResetLinkDialog from './ResetLinkDialog';
|
||||
|
||||
import './EditMemberDrawer.styles.scss';
|
||||
|
||||
const ROOT_USER_TOOLTIP = 'This operation is not supported for the root user';
|
||||
const SELF_DELETE_TOOLTIP =
|
||||
'You cannot perform this action on your own account';
|
||||
|
||||
function getDeleteTooltip(
|
||||
isRootUser: boolean,
|
||||
isSelf: boolean,
|
||||
): string | undefined {
|
||||
if (isRootUser) {
|
||||
return ROOT_USER_TOOLTIP;
|
||||
}
|
||||
if (isSelf) {
|
||||
return SELF_DELETE_TOOLTIP;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function toSaveApiError(err: unknown): APIError {
|
||||
return (
|
||||
convertToApiError(err as AxiosError<RenderErrorResponseDTO>) ??
|
||||
toAPIError(err as AxiosError<RenderErrorResponseDTO>)
|
||||
);
|
||||
}
|
||||
|
||||
function areSortedArraysEqual(a: string[], b: string[]): boolean {
|
||||
return JSON.stringify([...a].sort()) === JSON.stringify([...b].sort());
|
||||
}
|
||||
|
||||
export interface EditMemberDrawerProps {
|
||||
member: MemberRow | null;
|
||||
open: boolean;
|
||||
@@ -49,9 +80,12 @@ function EditMemberDrawer({
|
||||
onComplete,
|
||||
}: EditMemberDrawerProps): JSX.Element {
|
||||
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||
const { user: currentUser } = useAppContext();
|
||||
|
||||
const [displayName, setDisplayName] = useState('');
|
||||
const [selectedRole, setSelectedRole] = useState<ROLES>('VIEWER');
|
||||
const [localDisplayName, setLocalDisplayName] = useState('');
|
||||
const [localRoles, setLocalRoles] = useState<string[]>([]);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [saveErrors, setSaveErrors] = useState<SaveError[]>([]);
|
||||
const [isGeneratingLink, setIsGeneratingLink] = useState(false);
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
const [resetLink, setResetLink] = useState<string | null>(null);
|
||||
@@ -60,32 +94,63 @@ function EditMemberDrawer({
|
||||
const [linkType, setLinkType] = useState<'invite' | 'reset' | null>(null);
|
||||
|
||||
const isInvited = member?.status === MemberStatus.Invited;
|
||||
const isSelf = !!member?.id && member.id === currentUser?.id;
|
||||
|
||||
const { mutate: updateUser, isLoading: isSaving } = useUpdateUserDeprecated({
|
||||
mutation: {
|
||||
onSuccess: (): void => {
|
||||
toast.success('Member details updated successfully', { richColors: true });
|
||||
onComplete();
|
||||
onClose();
|
||||
},
|
||||
onError: (err): void => {
|
||||
const errMessage =
|
||||
convertToApiError(
|
||||
err as AxiosError<RenderErrorResponseDTO, unknown> | null,
|
||||
)?.getErrorMessage() || 'An error occurred';
|
||||
toast.error(`Failed to update member details: ${errMessage}`, {
|
||||
richColors: true,
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
const {
|
||||
data: fetchedUser,
|
||||
isLoading: isFetchingUser,
|
||||
refetch: refetchUser,
|
||||
} = useGetUser(
|
||||
{ id: member?.id ?? '' },
|
||||
{ query: { enabled: open && !!member?.id } },
|
||||
);
|
||||
|
||||
const isRootUser = !!fetchedUser?.data?.isRoot;
|
||||
|
||||
const {
|
||||
roles: availableRoles,
|
||||
isLoading: rolesLoading,
|
||||
isError: rolesError,
|
||||
error: rolesErrorObj,
|
||||
refetch: refetchRoles,
|
||||
} = useRoles();
|
||||
|
||||
const { fetchedRoleIds, applyDiff } = useMemberRoleManager(
|
||||
member?.id ?? '',
|
||||
open && !!member?.id,
|
||||
);
|
||||
|
||||
const fetchedDisplayName =
|
||||
fetchedUser?.data?.displayName ?? member?.name ?? '';
|
||||
const fetchedUserId = fetchedUser?.data?.id;
|
||||
const fetchedUserDisplayName = fetchedUser?.data?.displayName;
|
||||
|
||||
useEffect(() => {
|
||||
if (fetchedUserId) {
|
||||
setLocalDisplayName(fetchedUserDisplayName ?? member?.name ?? '');
|
||||
}
|
||||
setSaveErrors([]);
|
||||
}, [fetchedUserId, fetchedUserDisplayName, member?.name]);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalRoles(fetchedRoleIds);
|
||||
}, [fetchedRoleIds]);
|
||||
|
||||
const isDirty =
|
||||
member !== null &&
|
||||
fetchedUser != null &&
|
||||
(localDisplayName !== fetchedDisplayName ||
|
||||
!areSortedArraysEqual(localRoles, fetchedRoleIds));
|
||||
|
||||
const { mutateAsync: updateMyUser } = useUpdateMyUserV2();
|
||||
const { mutateAsync: updateUser } = useUpdateUser();
|
||||
|
||||
const { mutate: deleteUser, isLoading: isDeleting } = useDeleteUser({
|
||||
mutation: {
|
||||
onSuccess: (): void => {
|
||||
toast.success(
|
||||
isInvited ? 'Invite revoked successfully' : 'Member deleted successfully',
|
||||
{ richColors: true },
|
||||
{ richColors: true, position: 'top-right' },
|
||||
);
|
||||
setShowDeleteConfirm(false);
|
||||
onComplete();
|
||||
@@ -99,53 +164,163 @@ function EditMemberDrawer({
|
||||
const prefix = isInvited
|
||||
? 'Failed to revoke invite'
|
||||
: 'Failed to delete member';
|
||||
toast.error(`${prefix}: ${errMessage}`, { richColors: true });
|
||||
toast.error(`${prefix}: ${errMessage}`, {
|
||||
richColors: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (member) {
|
||||
setDisplayName(member.name ?? '');
|
||||
setSelectedRole(member.role);
|
||||
}
|
||||
}, [member]);
|
||||
|
||||
const isDirty =
|
||||
member !== null &&
|
||||
(displayName !== (member.name ?? '') || selectedRole !== member.role);
|
||||
|
||||
const formatTimestamp = useCallback(
|
||||
(ts: string | null | undefined): string => {
|
||||
if (!ts) {
|
||||
return '—';
|
||||
const makeRoleRetry = useCallback(
|
||||
(
|
||||
context: string,
|
||||
rawRetry: () => Promise<void>,
|
||||
) => async (): Promise<void> => {
|
||||
try {
|
||||
await rawRetry();
|
||||
setSaveErrors((prev) => prev.filter((e) => e.context !== context));
|
||||
refetchUser();
|
||||
} catch (err) {
|
||||
setSaveErrors((prev) =>
|
||||
prev.map((e) =>
|
||||
e.context === context ? { ...e, apiError: toSaveApiError(err) } : e,
|
||||
),
|
||||
);
|
||||
}
|
||||
const d = new Date(ts);
|
||||
if (Number.isNaN(d.getTime())) {
|
||||
return '—';
|
||||
}
|
||||
return formatTimezoneAdjustedTimestamp(ts, DATE_TIME_FORMATS.DASH_DATETIME);
|
||||
},
|
||||
[formatTimezoneAdjustedTimestamp],
|
||||
[refetchUser],
|
||||
);
|
||||
|
||||
const handleSave = useCallback((): void => {
|
||||
const retryNameUpdate = useCallback(async (): Promise<void> => {
|
||||
if (!member) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (isSelf) {
|
||||
await updateMyUser({ data: { displayName: localDisplayName } });
|
||||
} else {
|
||||
await updateUser({
|
||||
pathParams: { id: member.id },
|
||||
data: { displayName: localDisplayName },
|
||||
});
|
||||
}
|
||||
setSaveErrors((prev) => prev.filter((e) => e.context !== 'Name update'));
|
||||
refetchUser();
|
||||
} catch (err) {
|
||||
setSaveErrors((prev) =>
|
||||
prev.map((e) =>
|
||||
e.context === 'Name update' ? { ...e, apiError: toSaveApiError(err) } : e,
|
||||
),
|
||||
);
|
||||
}
|
||||
}, [member, isSelf, localDisplayName, updateMyUser, updateUser, refetchUser]);
|
||||
|
||||
const handleSave = useCallback(async (): Promise<void> => {
|
||||
if (!member || !isDirty) {
|
||||
return;
|
||||
}
|
||||
updateUser({
|
||||
pathParams: { id: member.id },
|
||||
data: { id: member.id, displayName, role: selectedRole },
|
||||
});
|
||||
}, [member, isDirty, displayName, selectedRole, updateUser]);
|
||||
setSaveErrors([]);
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const nameChanged = localDisplayName !== fetchedDisplayName;
|
||||
const rolesChanged = !areSortedArraysEqual(localRoles, fetchedRoleIds);
|
||||
|
||||
const namePromise = nameChanged
|
||||
? isSelf
|
||||
? updateMyUser({ data: { displayName: localDisplayName } })
|
||||
: updateUser({
|
||||
pathParams: { id: member.id },
|
||||
data: { displayName: localDisplayName },
|
||||
})
|
||||
: Promise.resolve();
|
||||
|
||||
const [nameResult, rolesResult] = await Promise.allSettled([
|
||||
namePromise,
|
||||
rolesChanged ? applyDiff(localRoles, availableRoles) : Promise.resolve([]),
|
||||
]);
|
||||
|
||||
const errors: SaveError[] = [];
|
||||
|
||||
if (nameResult.status === 'rejected') {
|
||||
errors.push({
|
||||
context: 'Name update',
|
||||
apiError: toSaveApiError(nameResult.reason),
|
||||
onRetry: retryNameUpdate,
|
||||
});
|
||||
}
|
||||
|
||||
if (rolesResult.status === 'rejected') {
|
||||
errors.push({
|
||||
context: 'Roles update',
|
||||
apiError: toSaveApiError(rolesResult.reason),
|
||||
onRetry: async (): Promise<void> => {
|
||||
const failures = await applyDiff(localRoles, availableRoles);
|
||||
setSaveErrors((prev) => {
|
||||
const rest = prev.filter((e) => e.context !== 'Roles update');
|
||||
return [
|
||||
...rest,
|
||||
...failures.map((f: MemberRoleUpdateFailure) => {
|
||||
const ctx = `Role '${f.roleName}'`;
|
||||
return {
|
||||
context: ctx,
|
||||
apiError: toSaveApiError(f.error),
|
||||
onRetry: makeRoleRetry(ctx, f.onRetry),
|
||||
};
|
||||
}),
|
||||
];
|
||||
});
|
||||
refetchUser();
|
||||
},
|
||||
});
|
||||
} else {
|
||||
for (const failure of rolesResult.value ?? []) {
|
||||
const context = `Role '${failure.roleName}'`;
|
||||
errors.push({
|
||||
context,
|
||||
apiError: toSaveApiError(failure.error),
|
||||
onRetry: makeRoleRetry(context, failure.onRetry),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
setSaveErrors(errors);
|
||||
} else {
|
||||
toast.success('Member details updated successfully', {
|
||||
richColors: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
onComplete();
|
||||
}
|
||||
|
||||
refetchUser();
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}, [
|
||||
member,
|
||||
isDirty,
|
||||
isSelf,
|
||||
localDisplayName,
|
||||
localRoles,
|
||||
fetchedDisplayName,
|
||||
fetchedRoleIds,
|
||||
updateMyUser,
|
||||
updateUser,
|
||||
applyDiff,
|
||||
availableRoles,
|
||||
refetchUser,
|
||||
retryNameUpdate,
|
||||
makeRoleRetry,
|
||||
onComplete,
|
||||
]);
|
||||
|
||||
const handleDelete = useCallback((): void => {
|
||||
if (!member) {
|
||||
return;
|
||||
}
|
||||
deleteUser({
|
||||
pathParams: { id: member.id },
|
||||
});
|
||||
deleteUser({ pathParams: { id: member.id } });
|
||||
}, [member, deleteUser]);
|
||||
|
||||
const handleGenerateResetLink = useCallback(async (): Promise<void> => {
|
||||
@@ -176,29 +351,28 @@ function EditMemberDrawer({
|
||||
} finally {
|
||||
setIsGeneratingLink(false);
|
||||
}
|
||||
}, [member, isInvited, setLinkType, onClose]);
|
||||
}, [member, isInvited, onClose]);
|
||||
|
||||
const [copyState, copyToClipboard] = useCopyToClipboard();
|
||||
const handleCopyResetLink = useCallback(async (): Promise<void> => {
|
||||
const handleCopyResetLink = useCallback((): void => {
|
||||
if (!resetLink) {
|
||||
return;
|
||||
}
|
||||
copyToClipboard(resetLink);
|
||||
|
||||
setHasCopiedResetLink(true);
|
||||
setTimeout(() => setHasCopiedResetLink(false), 2000);
|
||||
toast.success(
|
||||
const message =
|
||||
linkType === 'invite'
|
||||
? 'Invite link copied to clipboard'
|
||||
: 'Reset link copied to clipboard',
|
||||
{ richColors: true },
|
||||
);
|
||||
: 'Reset link copied to clipboard';
|
||||
toast.success(message, { richColors: true, position: 'top-right' });
|
||||
}, [resetLink, copyToClipboard, linkType]);
|
||||
|
||||
useEffect(() => {
|
||||
if (copyState.error) {
|
||||
toast.error('Failed to copy link', {
|
||||
richColors: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
}
|
||||
}, [copyState.error]);
|
||||
@@ -210,102 +384,183 @@ function EditMemberDrawer({
|
||||
|
||||
const joinedOnLabel = isInvited ? 'Invited On' : 'Joined On';
|
||||
|
||||
const drawerContent = (
|
||||
<div className="edit-member-drawer__layout">
|
||||
<div className="edit-member-drawer__body">
|
||||
<div className="edit-member-drawer__field">
|
||||
<label className="edit-member-drawer__label" htmlFor="member-name">
|
||||
Name
|
||||
</label>
|
||||
const formatTimestamp = useCallback(
|
||||
(ts: string | null | undefined): string => {
|
||||
if (!ts) {
|
||||
return '—';
|
||||
}
|
||||
const d = new Date(ts);
|
||||
if (Number.isNaN(d.getTime())) {
|
||||
return '—';
|
||||
}
|
||||
return formatTimezoneAdjustedTimestamp(ts, DATE_TIME_FORMATS.DASH_DATETIME);
|
||||
},
|
||||
[formatTimezoneAdjustedTimestamp],
|
||||
);
|
||||
|
||||
const drawerBody = isFetchingUser ? (
|
||||
<Skeleton active paragraph={{ rows: 6 }} />
|
||||
) : (
|
||||
<>
|
||||
<div className="edit-member-drawer__field">
|
||||
<label className="edit-member-drawer__label" htmlFor="member-name">
|
||||
Name
|
||||
</label>
|
||||
<Tooltip title={isRootUser ? ROOT_USER_TOOLTIP : undefined}>
|
||||
<Input
|
||||
id="member-name"
|
||||
value={displayName}
|
||||
onChange={(e): void => setDisplayName(e.target.value)}
|
||||
value={localDisplayName}
|
||||
onChange={(e): void => {
|
||||
setLocalDisplayName(e.target.value);
|
||||
setSaveErrors((prev) =>
|
||||
prev.filter((err) => err.context !== 'Name update'),
|
||||
);
|
||||
}}
|
||||
className="edit-member-drawer__input"
|
||||
placeholder="Enter name"
|
||||
disabled={isRootUser}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="edit-member-drawer__field">
|
||||
<label className="edit-member-drawer__label" htmlFor="member-email">
|
||||
Email Address
|
||||
</label>
|
||||
<div className="edit-member-drawer__input-wrapper edit-member-drawer__input-wrapper--disabled">
|
||||
<span className="edit-member-drawer__email-text">
|
||||
{member?.email || '—'}
|
||||
</span>
|
||||
<LockKeyhole size={16} className="edit-member-drawer__lock-icon" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="edit-member-drawer__field">
|
||||
<label className="edit-member-drawer__label" htmlFor="member-role">
|
||||
Roles
|
||||
</label>
|
||||
<Select
|
||||
id="member-role"
|
||||
value={selectedRole}
|
||||
onChange={(role): void => setSelectedRole(role as ROLES)}
|
||||
className="edit-member-drawer__role-select"
|
||||
suffixIcon={<ChevronDown size={14} />}
|
||||
getPopupContainer={popupContainer}
|
||||
>
|
||||
<Select.Option value="ADMIN">{capitalize('ADMIN')}</Select.Option>
|
||||
<Select.Option value="EDITOR">{capitalize('EDITOR')}</Select.Option>
|
||||
<Select.Option value="VIEWER">{capitalize('VIEWER')}</Select.Option>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="edit-member-drawer__meta">
|
||||
<div className="edit-member-drawer__meta-item">
|
||||
<span className="edit-member-drawer__meta-label">Status</span>
|
||||
{member?.status === MemberStatus.Active ? (
|
||||
<Badge color="forest" variant="outline">
|
||||
ACTIVE
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge color="amber" variant="outline">
|
||||
INVITED
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="edit-member-drawer__meta-item">
|
||||
<span className="edit-member-drawer__meta-label">{joinedOnLabel}</span>
|
||||
<Badge color="vanilla">{formatTimestamp(member?.joinedOn)}</Badge>
|
||||
</div>
|
||||
{!isInvited && (
|
||||
<div className="edit-member-drawer__meta-item">
|
||||
<span className="edit-member-drawer__meta-label">Last Modified</span>
|
||||
<Badge color="vanilla">{formatTimestamp(member?.updatedAt)}</Badge>
|
||||
</div>
|
||||
)}
|
||||
<div className="edit-member-drawer__field">
|
||||
<label className="edit-member-drawer__label" htmlFor="member-email">
|
||||
Email Address
|
||||
</label>
|
||||
<div className="edit-member-drawer__input-wrapper edit-member-drawer__input-wrapper--disabled">
|
||||
<span className="edit-member-drawer__email-text">
|
||||
{member?.email || '—'}
|
||||
</span>
|
||||
<LockKeyhole size={16} className="edit-member-drawer__lock-icon" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="edit-member-drawer__field">
|
||||
<label className="edit-member-drawer__label" htmlFor="member-role">
|
||||
Roles
|
||||
</label>
|
||||
{isSelf || isRootUser ? (
|
||||
<Tooltip
|
||||
title={isRootUser ? ROOT_USER_TOOLTIP : 'You cannot modify your own role'}
|
||||
>
|
||||
<div className="edit-member-drawer__input-wrapper edit-member-drawer__input-wrapper--disabled">
|
||||
<div className="edit-member-drawer__disabled-roles">
|
||||
{localRoles.length > 0 ? (
|
||||
localRoles.map((roleId) => {
|
||||
const role = availableRoles.find((r) => r.id === roleId);
|
||||
return (
|
||||
<Badge key={roleId} color="vanilla">
|
||||
{role?.name ?? roleId}
|
||||
</Badge>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<span className="edit-member-drawer__email-text">—</span>
|
||||
)}
|
||||
</div>
|
||||
<LockKeyhole size={16} className="edit-member-drawer__lock-icon" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<RolesSelect
|
||||
id="member-role"
|
||||
mode="multiple"
|
||||
roles={availableRoles}
|
||||
loading={rolesLoading}
|
||||
isError={rolesError}
|
||||
error={rolesErrorObj}
|
||||
onRefetch={refetchRoles}
|
||||
value={localRoles}
|
||||
onChange={(roles): void => {
|
||||
setLocalRoles(roles);
|
||||
setSaveErrors((prev) =>
|
||||
prev.filter(
|
||||
(err) =>
|
||||
err.context !== 'Roles update' && !err.context.startsWith("Role '"),
|
||||
),
|
||||
);
|
||||
}}
|
||||
className="edit-member-drawer__role-select"
|
||||
placeholder="Select roles"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="edit-member-drawer__meta">
|
||||
<div className="edit-member-drawer__meta-item">
|
||||
<span className="edit-member-drawer__meta-label">Status</span>
|
||||
{member?.status === MemberStatus.Active ? (
|
||||
<Badge color="forest" variant="outline">
|
||||
ACTIVE
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge color="amber" variant="outline">
|
||||
INVITED
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="edit-member-drawer__meta-item">
|
||||
<span className="edit-member-drawer__meta-label">{joinedOnLabel}</span>
|
||||
<Badge color="vanilla">{formatTimestamp(member?.joinedOn)}</Badge>
|
||||
</div>
|
||||
{!isInvited && (
|
||||
<div className="edit-member-drawer__meta-item">
|
||||
<span className="edit-member-drawer__meta-label">Last Modified</span>
|
||||
<Badge color="vanilla">{formatTimestamp(member?.updatedAt)}</Badge>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{saveErrors.length > 0 && (
|
||||
<div className="edit-member-drawer__save-errors">
|
||||
{saveErrors.map((e) => (
|
||||
<SaveErrorItem
|
||||
key={e.context}
|
||||
context={e.context}
|
||||
apiError={e.apiError}
|
||||
onRetry={e.onRetry}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
const drawerContent = (
|
||||
<div className="edit-member-drawer__layout">
|
||||
<div className="edit-member-drawer__body">{drawerBody}</div>
|
||||
|
||||
<div className="edit-member-drawer__footer">
|
||||
<div className="edit-member-drawer__footer-left">
|
||||
<Button
|
||||
className="edit-member-drawer__footer-btn edit-member-drawer__footer-btn--danger"
|
||||
onClick={(): void => setShowDeleteConfirm(true)}
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
{isInvited ? 'Revoke Invite' : 'Delete Member'}
|
||||
</Button>
|
||||
<Tooltip title={getDeleteTooltip(isRootUser, isSelf)}>
|
||||
<span className="edit-member-drawer__tooltip-wrapper">
|
||||
<Button
|
||||
className="edit-member-drawer__footer-btn edit-member-drawer__footer-btn--danger"
|
||||
onClick={(): void => setShowDeleteConfirm(true)}
|
||||
disabled={isRootUser || isSelf}
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
{isInvited ? 'Revoke Invite' : 'Delete Member'}
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
|
||||
<div className="edit-member-drawer__footer-divider" />
|
||||
<Button
|
||||
className="edit-member-drawer__footer-btn edit-member-drawer__footer-btn--warning"
|
||||
onClick={handleGenerateResetLink}
|
||||
disabled={isGeneratingLink}
|
||||
>
|
||||
<RefreshCw size={12} />
|
||||
{isGeneratingLink
|
||||
? 'Generating...'
|
||||
: isInvited
|
||||
? 'Copy Invite Link'
|
||||
: 'Generate Password Reset Link'}
|
||||
</Button>
|
||||
<Tooltip title={isRootUser ? ROOT_USER_TOOLTIP : undefined}>
|
||||
<span className="edit-member-drawer__tooltip-wrapper">
|
||||
<Button
|
||||
className="edit-member-drawer__footer-btn edit-member-drawer__footer-btn--warning"
|
||||
onClick={handleGenerateResetLink}
|
||||
disabled={isGeneratingLink || isRootUser}
|
||||
>
|
||||
<RefreshCw size={12} />
|
||||
{isGeneratingLink && 'Generating...'}
|
||||
{!isGeneratingLink && isInvited && 'Copy Invite Link'}
|
||||
{!isGeneratingLink && !isInvited && 'Generate Password Reset Link'}
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="edit-member-drawer__footer-right">
|
||||
@@ -318,7 +573,7 @@ function EditMemberDrawer({
|
||||
variant="solid"
|
||||
color="primary"
|
||||
size="sm"
|
||||
disabled={!isDirty || isSaving}
|
||||
disabled={!isDirty || isSaving || isRootUser}
|
||||
onClick={handleSave}
|
||||
>
|
||||
{isSaving ? 'Saving...' : 'Save Member Details'}
|
||||
@@ -328,22 +583,6 @@ function EditMemberDrawer({
|
||||
</div>
|
||||
);
|
||||
|
||||
const deleteDialogTitle = isInvited ? 'Revoke Invite' : 'Delete Member';
|
||||
const deleteDialogBody = isInvited ? (
|
||||
<>
|
||||
Are you sure you want to revoke the invite for{' '}
|
||||
<strong>{member?.email}</strong>? They will no longer be able to join the
|
||||
workspace using this invite.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Are you sure you want to delete{' '}
|
||||
<strong>{member?.name || member?.email}</strong>? This will remove their
|
||||
access to the workspace.
|
||||
</>
|
||||
);
|
||||
const deleteConfirmLabel = isInvited ? 'Revoke Invite' : 'Delete Member';
|
||||
|
||||
return (
|
||||
<>
|
||||
<DrawerWrapper
|
||||
@@ -363,82 +602,25 @@ function EditMemberDrawer({
|
||||
className="edit-member-drawer"
|
||||
/>
|
||||
|
||||
<DialogWrapper
|
||||
<ResetLinkDialog
|
||||
open={showResetLinkDialog}
|
||||
onOpenChange={(isOpen): void => {
|
||||
if (!isOpen) {
|
||||
setShowResetLinkDialog(false);
|
||||
setLinkType(null);
|
||||
}
|
||||
linkType={linkType}
|
||||
resetLink={resetLink}
|
||||
hasCopied={hasCopiedResetLink}
|
||||
onClose={(): void => {
|
||||
setShowResetLinkDialog(false);
|
||||
}}
|
||||
title={linkType === 'invite' ? 'Invite Link' : 'Password Reset Link'}
|
||||
showCloseButton
|
||||
width="base"
|
||||
className="reset-link-dialog"
|
||||
>
|
||||
<div className="reset-link-dialog__content">
|
||||
<p className="reset-link-dialog__description">
|
||||
{linkType === 'invite'
|
||||
? 'Share this one-time link with the team member to complete their account setup.'
|
||||
: 'This creates a one-time link the team member can use to set a new password for their SigNoz account.'}
|
||||
</p>
|
||||
<div className="reset-link-dialog__link-row">
|
||||
<div className="reset-link-dialog__link-text-wrap">
|
||||
<span className="reset-link-dialog__link-text">{resetLink}</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
onClick={handleCopyResetLink}
|
||||
prefixIcon={
|
||||
hasCopiedResetLink ? <Check size={12} /> : <Copy size={12} />
|
||||
}
|
||||
className="reset-link-dialog__copy-btn"
|
||||
>
|
||||
{hasCopiedResetLink ? 'Copied!' : 'Copy'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogWrapper>
|
||||
onCopy={handleCopyResetLink}
|
||||
/>
|
||||
|
||||
<DialogWrapper
|
||||
<DeleteMemberDialog
|
||||
open={showDeleteConfirm}
|
||||
onOpenChange={(isOpen): void => {
|
||||
if (!isOpen) {
|
||||
setShowDeleteConfirm(false);
|
||||
}
|
||||
}}
|
||||
title={deleteDialogTitle}
|
||||
width="narrow"
|
||||
className="alert-dialog delete-dialog"
|
||||
showCloseButton={false}
|
||||
disableOutsideClick={false}
|
||||
>
|
||||
<p className="delete-dialog__body">{deleteDialogBody}</p>
|
||||
|
||||
<DialogFooter className="delete-dialog__footer">
|
||||
<Button
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
onClick={(): void => setShowDeleteConfirm(false)}
|
||||
>
|
||||
<X size={12} />
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="destructive"
|
||||
size="sm"
|
||||
disabled={isDeleting}
|
||||
onClick={handleDelete}
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
{isDeleting ? 'Processing...' : deleteConfirmLabel}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogWrapper>
|
||||
isInvited={isInvited}
|
||||
member={member}
|
||||
isDeleting={isDeleting}
|
||||
onClose={(): void => setShowDeleteConfirm(false)}
|
||||
onConfirm={handleDelete}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
61
frontend/src/components/EditMemberDrawer/ResetLinkDialog.tsx
Normal file
61
frontend/src/components/EditMemberDrawer/ResetLinkDialog.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { Button } from '@signozhq/button';
|
||||
import { DialogWrapper } from '@signozhq/dialog';
|
||||
import { Check, Copy } from '@signozhq/icons';
|
||||
|
||||
interface ResetLinkDialogProps {
|
||||
open: boolean;
|
||||
linkType: 'invite' | 'reset' | null;
|
||||
resetLink: string | null;
|
||||
hasCopied: boolean;
|
||||
onClose: () => void;
|
||||
onCopy: () => void;
|
||||
}
|
||||
|
||||
function ResetLinkDialog({
|
||||
open,
|
||||
linkType,
|
||||
resetLink,
|
||||
hasCopied,
|
||||
onClose,
|
||||
onCopy,
|
||||
}: ResetLinkDialogProps): JSX.Element {
|
||||
return (
|
||||
<DialogWrapper
|
||||
open={open}
|
||||
onOpenChange={(isOpen): void => {
|
||||
if (!isOpen) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
title={linkType === 'invite' ? 'Invite Link' : 'Password Reset Link'}
|
||||
showCloseButton
|
||||
width="base"
|
||||
className="reset-link-dialog"
|
||||
>
|
||||
<div className="reset-link-dialog__content">
|
||||
<p className="reset-link-dialog__description">
|
||||
{linkType === 'invite'
|
||||
? 'Share this one-time link with the team member to complete their account setup.'
|
||||
: 'This creates a one-time link the team member can use to set a new password for their SigNoz account.'}
|
||||
</p>
|
||||
<div className="reset-link-dialog__link-row">
|
||||
<div className="reset-link-dialog__link-text-wrap">
|
||||
<span className="reset-link-dialog__link-text">{resetLink}</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
onClick={onCopy}
|
||||
prefixIcon={hasCopied ? <Check size={12} /> : <Copy size={12} />}
|
||||
className="reset-link-dialog__copy-btn"
|
||||
>
|
||||
{hasCopied ? 'Copied!' : 'Copy'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default ResetLinkDialog;
|
||||
@@ -4,11 +4,19 @@ import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import {
|
||||
getResetPasswordToken,
|
||||
useDeleteUser,
|
||||
useUpdateUserDeprecated,
|
||||
useGetUser,
|
||||
useRemoveUserRoleByUserIDAndRoleID,
|
||||
useSetRoleByUserID,
|
||||
useUpdateMyUserV2,
|
||||
useUpdateUser,
|
||||
} from 'api/generated/services/users';
|
||||
import { MemberStatus } from 'container/MembersSettings/utils';
|
||||
import {
|
||||
listRolesSuccessResponse,
|
||||
managedRoles,
|
||||
} from 'mocks-server/__mockdata__/roles';
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
import { ROLES } from 'types/roles';
|
||||
|
||||
import EditMemberDrawer, { EditMemberDrawerProps } from '../EditMemberDrawer';
|
||||
|
||||
@@ -44,7 +52,11 @@ jest.mock('@signozhq/dialog', () => ({
|
||||
|
||||
jest.mock('api/generated/services/users', () => ({
|
||||
useDeleteUser: jest.fn(),
|
||||
useUpdateUserDeprecated: jest.fn(),
|
||||
useGetUser: jest.fn(),
|
||||
useUpdateUser: jest.fn(),
|
||||
useUpdateMyUserV2: jest.fn(),
|
||||
useSetRoleByUserID: jest.fn(),
|
||||
useRemoveUserRoleByUserIDAndRoleID: jest.fn(),
|
||||
getResetPasswordToken: jest.fn(),
|
||||
}));
|
||||
|
||||
@@ -69,25 +81,53 @@ jest.mock('react-use', () => ({
|
||||
],
|
||||
}));
|
||||
|
||||
const mockUpdateMutate = jest.fn();
|
||||
const ROLES_ENDPOINT = '*/api/v1/roles';
|
||||
|
||||
const mockDeleteMutate = jest.fn();
|
||||
const mockGetResetPasswordToken = jest.mocked(getResetPasswordToken);
|
||||
|
||||
const mockFetchedUser = {
|
||||
data: {
|
||||
id: 'user-1',
|
||||
displayName: 'Alice Smith',
|
||||
email: 'alice@signoz.io',
|
||||
status: 'active',
|
||||
userRoles: [
|
||||
{
|
||||
id: 'ur-1',
|
||||
roleId: managedRoles[0].id,
|
||||
role: managedRoles[0], // signoz-admin
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const activeMember = {
|
||||
id: 'user-1',
|
||||
name: 'Alice Smith',
|
||||
email: 'alice@signoz.io',
|
||||
role: 'ADMIN' as ROLES,
|
||||
status: MemberStatus.Active,
|
||||
joinedOn: '1700000000000',
|
||||
updatedAt: '1710000000000',
|
||||
};
|
||||
|
||||
const selfMember = {
|
||||
...activeMember,
|
||||
id: 'some-user-id',
|
||||
};
|
||||
|
||||
const rootMockFetchedUser = {
|
||||
data: {
|
||||
...mockFetchedUser.data,
|
||||
id: 'root-user-1',
|
||||
isRoot: true,
|
||||
},
|
||||
};
|
||||
|
||||
const invitedMember = {
|
||||
id: 'abc123',
|
||||
name: '',
|
||||
email: 'bob@signoz.io',
|
||||
role: 'VIEWER' as ROLES,
|
||||
status: MemberStatus.Invited,
|
||||
joinedOn: '1700000000000',
|
||||
};
|
||||
@@ -109,8 +149,30 @@ function renderDrawer(
|
||||
describe('EditMemberDrawer', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(useUpdateUserDeprecated as jest.Mock).mockReturnValue({
|
||||
mutate: mockUpdateMutate,
|
||||
server.use(
|
||||
rest.get(ROLES_ENDPOINT, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(listRolesSuccessResponse)),
|
||||
),
|
||||
);
|
||||
(useGetUser as jest.Mock).mockReturnValue({
|
||||
data: mockFetchedUser,
|
||||
isLoading: false,
|
||||
refetch: jest.fn(),
|
||||
});
|
||||
(useUpdateUser as jest.Mock).mockReturnValue({
|
||||
mutateAsync: jest.fn().mockResolvedValue({}),
|
||||
isLoading: false,
|
||||
});
|
||||
(useUpdateMyUserV2 as jest.Mock).mockReturnValue({
|
||||
mutateAsync: jest.fn().mockResolvedValue({}),
|
||||
isLoading: false,
|
||||
});
|
||||
(useSetRoleByUserID as jest.Mock).mockReturnValue({
|
||||
mutateAsync: jest.fn().mockResolvedValue({}),
|
||||
isLoading: false,
|
||||
});
|
||||
(useRemoveUserRoleByUserIDAndRoleID as jest.Mock).mockReturnValue({
|
||||
mutateAsync: jest.fn().mockResolvedValue({}),
|
||||
isLoading: false,
|
||||
});
|
||||
(useDeleteUser as jest.Mock).mockReturnValue({
|
||||
@@ -119,6 +181,10 @@ describe('EditMemberDrawer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
it('renders active member details and disables Save when form is not dirty', () => {
|
||||
renderDrawer();
|
||||
|
||||
@@ -130,16 +196,15 @@ describe('EditMemberDrawer', () => {
|
||||
).toBeDisabled();
|
||||
});
|
||||
|
||||
it('enables Save after editing name and calls update API on confirm', async () => {
|
||||
it('enables Save after editing name and calls updateUser on confirm', async () => {
|
||||
const onComplete = jest.fn();
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
const mockMutateAsync = jest.fn().mockResolvedValue({});
|
||||
|
||||
(useUpdateUserDeprecated as jest.Mock).mockImplementation((options) => ({
|
||||
mutate: mockUpdateMutate.mockImplementation(() => {
|
||||
options?.mutation?.onSuccess?.();
|
||||
}),
|
||||
(useUpdateUser as jest.Mock).mockReturnValue({
|
||||
mutateAsync: mockMutateAsync,
|
||||
isLoading: false,
|
||||
}));
|
||||
});
|
||||
|
||||
renderDrawer({ onComplete });
|
||||
|
||||
@@ -153,12 +218,92 @@ describe('EditMemberDrawer', () => {
|
||||
await user.click(saveBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateMutate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
pathParams: { id: 'user-1' },
|
||||
data: expect.objectContaining({ displayName: 'Alice Updated' }),
|
||||
}),
|
||||
);
|
||||
expect(mockMutateAsync).toHaveBeenCalledWith({
|
||||
pathParams: { id: 'user-1' },
|
||||
data: { displayName: 'Alice Updated' },
|
||||
});
|
||||
expect(onComplete).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('does not close the drawer after a successful save', async () => {
|
||||
const onClose = jest.fn();
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
renderDrawer({ onClose });
|
||||
|
||||
const nameInput = screen.getByDisplayValue('Alice Smith');
|
||||
await user.clear(nameInput);
|
||||
await user.type(nameInput, 'Alice Updated');
|
||||
|
||||
const saveBtn = screen.getByRole('button', { name: /save member details/i });
|
||||
await waitFor(() => expect(saveBtn).not.toBeDisabled());
|
||||
await user.click(saveBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByRole('button', { name: /save member details/i }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
expect(onClose).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls setRole when a new role is added', async () => {
|
||||
const onComplete = jest.fn();
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
const mockSet = jest.fn().mockResolvedValue({});
|
||||
|
||||
(useSetRoleByUserID as jest.Mock).mockReturnValue({
|
||||
mutateAsync: mockSet,
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
renderDrawer({ onComplete });
|
||||
|
||||
// Open the roles dropdown and select signoz-editor
|
||||
await user.click(screen.getByLabelText('Roles'));
|
||||
await user.click(await screen.findByTitle('signoz-editor'));
|
||||
|
||||
const saveBtn = screen.getByRole('button', { name: /save member details/i });
|
||||
await waitFor(() => expect(saveBtn).not.toBeDisabled());
|
||||
await user.click(saveBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSet).toHaveBeenCalledWith({
|
||||
pathParams: { id: 'user-1' },
|
||||
data: { name: 'signoz-editor' },
|
||||
});
|
||||
expect(onComplete).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('calls removeRole when an existing role is removed', async () => {
|
||||
const onComplete = jest.fn();
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
const mockRemove = jest.fn().mockResolvedValue({});
|
||||
|
||||
(useRemoveUserRoleByUserIDAndRoleID as jest.Mock).mockReturnValue({
|
||||
mutateAsync: mockRemove,
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
renderDrawer({ onComplete });
|
||||
|
||||
// Wait for the signoz-admin tag to appear, then click its remove button
|
||||
const adminTag = await screen.findByTitle('signoz-admin');
|
||||
const removeBtn = adminTag.querySelector(
|
||||
'.ant-select-selection-item-remove',
|
||||
) as Element;
|
||||
await user.click(removeBtn);
|
||||
|
||||
const saveBtn = screen.getByRole('button', { name: /save member details/i });
|
||||
await waitFor(() => expect(saveBtn).not.toBeDisabled());
|
||||
await user.click(saveBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockRemove).toHaveBeenCalledWith({
|
||||
pathParams: { id: 'user-1', roleId: managedRoles[0].id },
|
||||
});
|
||||
expect(onComplete).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -239,16 +384,33 @@ describe('EditMemberDrawer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('calls update API when saving changes for an invited member', async () => {
|
||||
it('calls updateUser when saving name change for an invited member', async () => {
|
||||
const onComplete = jest.fn();
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
const mockMutateAsync = jest.fn().mockResolvedValue({});
|
||||
|
||||
(useUpdateUserDeprecated as jest.Mock).mockImplementation((options) => ({
|
||||
mutate: mockUpdateMutate.mockImplementation(() => {
|
||||
options?.mutation?.onSuccess?.();
|
||||
}),
|
||||
(useGetUser as jest.Mock).mockReturnValue({
|
||||
data: {
|
||||
data: {
|
||||
...mockFetchedUser.data,
|
||||
id: 'abc123',
|
||||
displayName: 'Bob',
|
||||
userRoles: [
|
||||
{
|
||||
id: 'ur-2',
|
||||
roleId: managedRoles[2].id,
|
||||
role: managedRoles[2], // signoz-viewer
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
}));
|
||||
refetch: jest.fn(),
|
||||
});
|
||||
(useUpdateUser as jest.Mock).mockReturnValue({
|
||||
mutateAsync: mockMutateAsync,
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
renderDrawer({ member: { ...invitedMember, name: 'Bob' }, onComplete });
|
||||
|
||||
@@ -261,12 +423,10 @@ describe('EditMemberDrawer', () => {
|
||||
await user.click(saveBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateMutate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
pathParams: { id: 'abc123' },
|
||||
data: expect.objectContaining({ displayName: 'Bob Updated' }),
|
||||
}),
|
||||
);
|
||||
expect(mockMutateAsync).toHaveBeenCalledWith({
|
||||
pathParams: { id: 'abc123' },
|
||||
data: { displayName: 'Bob Updated' },
|
||||
});
|
||||
expect(onComplete).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -280,16 +440,13 @@ describe('EditMemberDrawer', () => {
|
||||
} as ReturnType<typeof convertToApiError>);
|
||||
});
|
||||
|
||||
it('shows API error message when updateUser fails', async () => {
|
||||
it('shows SaveErrorItem when updateUser fails for name change', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
const mockToast = jest.mocked(toast);
|
||||
|
||||
(useUpdateUserDeprecated as jest.Mock).mockImplementation((options) => ({
|
||||
mutate: mockUpdateMutate.mockImplementation(() => {
|
||||
options?.mutation?.onError?.({});
|
||||
}),
|
||||
(useUpdateUser as jest.Mock).mockReturnValue({
|
||||
mutateAsync: jest.fn().mockRejectedValue(new Error('server error')),
|
||||
isLoading: false,
|
||||
}));
|
||||
});
|
||||
|
||||
renderDrawer();
|
||||
|
||||
@@ -302,10 +459,9 @@ describe('EditMemberDrawer', () => {
|
||||
await user.click(saveBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockToast.error).toHaveBeenCalledWith(
|
||||
'Failed to update member details: Something went wrong on server',
|
||||
expect.anything(),
|
||||
);
|
||||
expect(
|
||||
screen.getByText('Name update: Something went wrong on server'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -364,6 +520,96 @@ describe('EditMemberDrawer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('self user (isSelf)', () => {
|
||||
it('disables Delete button when viewing own profile', () => {
|
||||
renderDrawer({ member: selfMember });
|
||||
expect(
|
||||
screen.getByRole('button', { name: /delete member/i }),
|
||||
).toBeDisabled();
|
||||
});
|
||||
|
||||
it('does not open delete confirm dialog when Delete is clicked while disabled (isSelf)', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
renderDrawer({ member: selfMember });
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /delete member/i }));
|
||||
|
||||
expect(
|
||||
screen.queryByText(/are you sure you want to delete/i),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('keeps name input enabled when viewing own profile', () => {
|
||||
renderDrawer({ member: selfMember });
|
||||
expect(screen.getByDisplayValue('Alice Smith')).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it('keeps Reset Link button enabled when viewing own profile', () => {
|
||||
renderDrawer({ member: selfMember });
|
||||
expect(
|
||||
screen.getByRole('button', { name: /generate password reset link/i }),
|
||||
).not.toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('root user', () => {
|
||||
beforeEach(() => {
|
||||
(useGetUser as jest.Mock).mockReturnValue({
|
||||
data: rootMockFetchedUser,
|
||||
isLoading: false,
|
||||
refetch: jest.fn(),
|
||||
});
|
||||
});
|
||||
|
||||
it('disables name input for root user', () => {
|
||||
renderDrawer();
|
||||
expect(screen.getByDisplayValue('Alice Smith')).toBeDisabled();
|
||||
});
|
||||
|
||||
it('disables Delete button for root user', () => {
|
||||
renderDrawer();
|
||||
expect(
|
||||
screen.getByRole('button', { name: /delete member/i }),
|
||||
).toBeDisabled();
|
||||
});
|
||||
|
||||
it('disables Reset Link button for root user', () => {
|
||||
renderDrawer();
|
||||
expect(
|
||||
screen.getByRole('button', { name: /generate password reset link/i }),
|
||||
).toBeDisabled();
|
||||
});
|
||||
|
||||
it('disables Save button for root user', () => {
|
||||
renderDrawer();
|
||||
expect(
|
||||
screen.getByRole('button', { name: /save member details/i }),
|
||||
).toBeDisabled();
|
||||
});
|
||||
|
||||
it('does not open delete confirm dialog when Delete is clicked while disabled (root)', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
renderDrawer();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /delete member/i }));
|
||||
|
||||
expect(
|
||||
screen.queryByText(/are you sure you want to delete/i),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not call getResetPasswordToken when Reset Link is clicked while disabled (root)', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
renderDrawer();
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: /generate password reset link/i }),
|
||||
);
|
||||
|
||||
expect(mockGetResetPasswordToken).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Generate Password Reset Link', () => {
|
||||
beforeEach(() => {
|
||||
mockCopyToClipboard.mockClear();
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { useEffect } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { refreshIntervalOptions } from 'container/TopNav/AutoRefreshV2/constants';
|
||||
import { Time } from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { useGlobalTimeStore } from 'store/globalTime/globalTimeStore';
|
||||
import { createCustomTimeRange } from 'store/globalTime/utils';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
/**
|
||||
* Adapter component that syncs Redux global time state to Zustand store.
|
||||
* This component should be rendered once at the app level.
|
||||
*
|
||||
* It reads from the Redux globalTime reducer and updates the Zustand store
|
||||
* to provide a migration path from Redux to Zustand.
|
||||
*/
|
||||
export function GlobalTimeStoreAdapter(): null {
|
||||
const globalTime = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const setSelectedTime = useGlobalTimeStore((s) => s.setSelectedTime);
|
||||
|
||||
useEffect(() => {
|
||||
// Convert the selectedTime to the new format
|
||||
// If it's 'custom', store the min/max times in the custom format
|
||||
const selectedTime =
|
||||
globalTime.selectedTime === 'custom'
|
||||
? createCustomTimeRange(globalTime.minTime, globalTime.maxTime)
|
||||
: (globalTime.selectedTime as Time);
|
||||
|
||||
// Find refresh interval from Redux state
|
||||
const refreshOption = refreshIntervalOptions.find(
|
||||
(option) => option.key === globalTime.selectedAutoRefreshInterval,
|
||||
);
|
||||
|
||||
const refreshInterval =
|
||||
!globalTime.isAutoRefreshDisabled && refreshOption ? refreshOption.value : 0;
|
||||
|
||||
setSelectedTime(selectedTime, refreshInterval);
|
||||
}, [
|
||||
globalTime.selectedTime,
|
||||
globalTime.isAutoRefreshDisabled,
|
||||
globalTime.selectedAutoRefreshInterval,
|
||||
globalTime.minTime,
|
||||
globalTime.maxTime,
|
||||
setSelectedTime,
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Provider } from 'react-redux';
|
||||
import { act, render, renderHook } from '@testing-library/react';
|
||||
import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants';
|
||||
import configureStore, { MockStoreEnhanced } from 'redux-mock-store';
|
||||
import { useGlobalTimeStore } from 'store/globalTime/globalTimeStore';
|
||||
import { createCustomTimeRange } from 'store/globalTime/utils';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { GlobalTimeStoreAdapter } from '../GlobalTimeStoreAdapter';
|
||||
|
||||
const mockStore = configureStore<Partial<AppState>>([]);
|
||||
|
||||
const randomTime = 1700000000000000000;
|
||||
|
||||
describe('GlobalTimeStoreAdapter', () => {
|
||||
let store: MockStoreEnhanced<Partial<AppState>>;
|
||||
|
||||
const createGlobalTimeState = (
|
||||
overrides: Partial<GlobalReducer> = {},
|
||||
): GlobalReducer => ({
|
||||
minTime: randomTime,
|
||||
maxTime: randomTime,
|
||||
loading: false,
|
||||
selectedTime: '15m',
|
||||
isAutoRefreshDisabled: true,
|
||||
selectedAutoRefreshInterval: 'off',
|
||||
...overrides,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset Zustand store before each test
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
act(() => {
|
||||
result.current.setSelectedTime(DEFAULT_TIME_RANGE, 0);
|
||||
});
|
||||
});
|
||||
|
||||
it('should render null because it just an adapter', () => {
|
||||
store = mockStore({
|
||||
globalTime: createGlobalTimeState(),
|
||||
});
|
||||
|
||||
const { container } = render(
|
||||
<Provider store={store}>
|
||||
<GlobalTimeStoreAdapter />
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it('should sync relative time from Redux to Zustand store', () => {
|
||||
store = mockStore({
|
||||
globalTime: createGlobalTimeState({
|
||||
selectedTime: '15m',
|
||||
isAutoRefreshDisabled: true,
|
||||
selectedAutoRefreshInterval: 'off',
|
||||
}),
|
||||
});
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<GlobalTimeStoreAdapter />
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
expect(result.current.selectedTime).toBe('15m');
|
||||
expect(result.current.refreshInterval).toBe(0);
|
||||
expect(result.current.isRefreshEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('should sync custom time from Redux to Zustand store', () => {
|
||||
store = mockStore({
|
||||
globalTime: createGlobalTimeState({
|
||||
selectedTime: 'custom',
|
||||
minTime: randomTime,
|
||||
maxTime: randomTime,
|
||||
isAutoRefreshDisabled: true,
|
||||
}),
|
||||
});
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<GlobalTimeStoreAdapter />
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
expect(result.current.selectedTime).toBe(
|
||||
createCustomTimeRange(randomTime, randomTime),
|
||||
);
|
||||
expect(result.current.isRefreshEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('should sync refresh interval when auto refresh is enabled', () => {
|
||||
store = mockStore({
|
||||
globalTime: createGlobalTimeState({
|
||||
selectedTime: '15m',
|
||||
isAutoRefreshDisabled: false,
|
||||
selectedAutoRefreshInterval: '5s',
|
||||
}),
|
||||
});
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<GlobalTimeStoreAdapter />
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
expect(result.current.selectedTime).toBe('15m');
|
||||
expect(result.current.refreshInterval).toBe(5000); // 5s = 5000ms
|
||||
expect(result.current.isRefreshEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it('should set refreshInterval to 0 when auto refresh is disabled', () => {
|
||||
store = mockStore({
|
||||
globalTime: createGlobalTimeState({
|
||||
selectedTime: '15m',
|
||||
isAutoRefreshDisabled: true,
|
||||
selectedAutoRefreshInterval: '5s', // Even with interval set, should be 0 when disabled
|
||||
}),
|
||||
});
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<GlobalTimeStoreAdapter />
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
expect(result.current.refreshInterval).toBe(0);
|
||||
expect(result.current.isRefreshEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('should update Zustand store when Redux state changes', () => {
|
||||
store = mockStore({
|
||||
globalTime: createGlobalTimeState({
|
||||
selectedTime: '15m',
|
||||
isAutoRefreshDisabled: true,
|
||||
}),
|
||||
});
|
||||
|
||||
const { rerender } = render(
|
||||
<Provider store={store}>
|
||||
<GlobalTimeStoreAdapter />
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
// Verify initial state
|
||||
let zustandState = renderHook(() => useGlobalTimeStore());
|
||||
expect(zustandState.result.current.selectedTime).toBe('15m');
|
||||
|
||||
// Update Redux store
|
||||
const newStore = mockStore({
|
||||
globalTime: createGlobalTimeState({
|
||||
selectedTime: '1h',
|
||||
isAutoRefreshDisabled: false,
|
||||
selectedAutoRefreshInterval: '30s',
|
||||
}),
|
||||
});
|
||||
|
||||
rerender(
|
||||
<Provider store={newStore}>
|
||||
<GlobalTimeStoreAdapter />
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
// Verify updated state
|
||||
zustandState = renderHook(() => useGlobalTimeStore());
|
||||
expect(zustandState.result.current.selectedTime).toBe('1h');
|
||||
expect(zustandState.result.current.refreshInterval).toBe(30000); // 30s = 30000ms
|
||||
expect(zustandState.result.current.isRefreshEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle various refresh interval options', () => {
|
||||
const testCases = [
|
||||
{ key: '5s', expectedValue: 5000 },
|
||||
{ key: '10s', expectedValue: 10000 },
|
||||
{ key: '30s', expectedValue: 30000 },
|
||||
{ key: '1m', expectedValue: 60000 },
|
||||
{ key: '5m', expectedValue: 300000 },
|
||||
];
|
||||
|
||||
testCases.forEach(({ key, expectedValue }) => {
|
||||
store = mockStore({
|
||||
globalTime: createGlobalTimeState({
|
||||
selectedTime: '15m',
|
||||
isAutoRefreshDisabled: false,
|
||||
selectedAutoRefreshInterval: key,
|
||||
}),
|
||||
});
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<GlobalTimeStoreAdapter />
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
expect(result.current.refreshInterval).toBe(expectedValue);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle unknown refresh interval by setting 0', () => {
|
||||
store = mockStore({
|
||||
globalTime: createGlobalTimeState({
|
||||
selectedTime: '15m',
|
||||
isAutoRefreshDisabled: false,
|
||||
selectedAutoRefreshInterval: 'unknown-interval',
|
||||
}),
|
||||
});
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<GlobalTimeStoreAdapter />
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
expect(result.current.refreshInterval).toBe(0);
|
||||
expect(result.current.isRefreshEnabled).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -197,13 +197,16 @@ function InviteMembersModal({
|
||||
})),
|
||||
});
|
||||
}
|
||||
toast.success('Invites sent successfully', { richColors: true });
|
||||
toast.success('Invites sent successfully', {
|
||||
richColors: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
resetAndClose();
|
||||
onComplete?.();
|
||||
} catch (err) {
|
||||
const apiErr = err as APIError;
|
||||
const errorMessage = apiErr?.getErrorMessage?.() ?? 'An error occurred';
|
||||
toast.error(errorMessage, { richColors: true });
|
||||
toast.error(errorMessage, { richColors: true, position: 'top-right' });
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { MemberStatus } from 'container/MembersSettings/utils';
|
||||
import { render, screen, userEvent } from 'tests/test-utils';
|
||||
import { ROLES } from 'types/roles';
|
||||
|
||||
import MembersTable, { MemberRow } from '../MembersTable';
|
||||
|
||||
@@ -9,7 +8,6 @@ const mockActiveMembers: MemberRow[] = [
|
||||
id: 'user-1',
|
||||
name: 'Alice Smith',
|
||||
email: 'alice@signoz.io',
|
||||
role: 'ADMIN' as ROLES,
|
||||
status: MemberStatus.Active,
|
||||
joinedOn: '1700000000000',
|
||||
},
|
||||
@@ -17,7 +15,6 @@ const mockActiveMembers: MemberRow[] = [
|
||||
id: 'user-2',
|
||||
name: 'Bob Jones',
|
||||
email: 'bob@signoz.io',
|
||||
role: 'VIEWER' as ROLES,
|
||||
status: MemberStatus.Active,
|
||||
joinedOn: null,
|
||||
},
|
||||
@@ -27,7 +24,6 @@ const mockInvitedMember: MemberRow = {
|
||||
id: 'inv-abc',
|
||||
name: '',
|
||||
email: 'charlie@signoz.io',
|
||||
role: 'EDITOR' as ROLES,
|
||||
status: MemberStatus.Invited,
|
||||
joinedOn: null,
|
||||
};
|
||||
@@ -47,12 +43,11 @@ describe('MembersTable', () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders member rows with name, email, role badge, and ACTIVE status', () => {
|
||||
it('renders member rows with name, email, and ACTIVE status', () => {
|
||||
render(<MembersTable {...defaultProps} data={mockActiveMembers} />);
|
||||
|
||||
expect(screen.getByText('Alice Smith')).toBeInTheDocument();
|
||||
expect(screen.getByText('alice@signoz.io')).toBeInTheDocument();
|
||||
expect(screen.getByText('Admin')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('ACTIVE')).toHaveLength(2);
|
||||
});
|
||||
|
||||
@@ -67,7 +62,6 @@ describe('MembersTable', () => {
|
||||
|
||||
expect(screen.getByText('INVITED')).toBeInTheDocument();
|
||||
expect(screen.getByText('charlie@signoz.io')).toBeInTheDocument();
|
||||
expect(screen.getByText('Editor')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onRowClick with the member data when a row is clicked', async () => {
|
||||
@@ -99,7 +93,6 @@ describe('MembersTable', () => {
|
||||
id: 'user-del',
|
||||
name: 'Dave Deleted',
|
||||
email: 'dave@signoz.io',
|
||||
role: 'VIEWER' as ROLES,
|
||||
status: MemberStatus.Deleted,
|
||||
joinedOn: null,
|
||||
};
|
||||
|
||||
@@ -165,7 +165,17 @@ function KeysTab({
|
||||
return (
|
||||
<div className="keys-tab__empty">
|
||||
<KeyRound size={24} className="keys-tab__empty-icon" />
|
||||
<p className="keys-tab__empty-text">No keys. Start by creating one.</p>
|
||||
<p className="keys-tab__empty-text">
|
||||
No keys. Start by creating one.{' '}
|
||||
<a
|
||||
href="https://signoz.io/docs/manage/administrator-guide/iam/service-accounts/#step-3-generate-an-api-key"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="keys-tab__learn-more"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</p>
|
||||
<Button
|
||||
type="button"
|
||||
className="keys-tab__learn-more"
|
||||
|
||||
@@ -294,6 +294,7 @@ function ServiceAccountDrawer({
|
||||
} else {
|
||||
toast.success('Service account updated successfully', {
|
||||
richColors: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
onSuccess({ closeDrawer: false });
|
||||
}
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
export const REACT_QUERY_KEY = {
|
||||
/**
|
||||
* For any query that should support AutoRefresh and min/max time is from DateTimeSelectionV2
|
||||
* You can prefix the query with this KEY, it will allow the queries to be automatically refreshed
|
||||
* when the user clicks in the refresh button, or alert the user when the data is being refreshed.
|
||||
*/
|
||||
AUTO_REFRESH_QUERY: 'AUTO_REFRESH_QUERY',
|
||||
|
||||
GET_PUBLIC_DASHBOARD: 'GET_PUBLIC_DASHBOARD',
|
||||
GET_PUBLIC_DASHBOARD_META: 'GET_PUBLIC_DASHBOARD_META',
|
||||
GET_PUBLIC_DASHBOARD_WIDGET_DATA: 'GET_PUBLIC_DASHBOARD_WIDGET_DATA',
|
||||
|
||||
@@ -248,5 +248,35 @@ export function createShortcutActions(deps: ActionDeps): CmdAction[] {
|
||||
roles: ['ADMIN', 'EDITOR'],
|
||||
perform: (): void => navigate(ROUTES.BILLING),
|
||||
},
|
||||
{
|
||||
id: 'my-settings-service-accounts',
|
||||
name: 'Go to Service Accounts',
|
||||
shortcut: [GlobalShortcutsName.NavigateToSettingsServiceAccounts],
|
||||
keywords: 'settings service accounts',
|
||||
section: 'Settings',
|
||||
icon: <Settings size={14} />,
|
||||
roles: ['ADMIN'],
|
||||
perform: (): void => navigate(ROUTES.SERVICE_ACCOUNTS_SETTINGS),
|
||||
},
|
||||
{
|
||||
id: 'my-settings-roles',
|
||||
name: 'Go to Roles',
|
||||
shortcut: [GlobalShortcutsName.NavigateToSettingsRoles],
|
||||
keywords: 'settings roles',
|
||||
section: 'Settings',
|
||||
icon: <Settings size={14} />,
|
||||
roles: ['ADMIN'],
|
||||
perform: (): void => navigate(ROUTES.ROLES_SETTINGS),
|
||||
},
|
||||
{
|
||||
id: 'my-settings-members',
|
||||
name: 'Go to Members',
|
||||
shortcut: [GlobalShortcutsName.NavigateToSettingsMembers],
|
||||
keywords: 'settings members',
|
||||
section: 'Settings',
|
||||
icon: <Settings size={14} />,
|
||||
roles: ['ADMIN'],
|
||||
perform: (): void => navigate(ROUTES.MEMBERS_SETTINGS),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -27,6 +27,9 @@ export const GlobalShortcuts = {
|
||||
NavigateToSettingsIngestion: 'shift+g+i',
|
||||
NavigateToSettingsBilling: 'shift+g+b',
|
||||
NavigateToSettingsNotificationChannels: 'shift+g+n',
|
||||
NavigateToSettingsServiceAccounts: 'shift+g+k',
|
||||
NavigateToSettingsRoles: 'shift+g+r',
|
||||
NavigateToSettingsMembers: 'shift+g+m',
|
||||
};
|
||||
|
||||
export const GlobalShortcutsName = {
|
||||
@@ -47,6 +50,9 @@ export const GlobalShortcutsName = {
|
||||
NavigateToSettingsIngestion: 'shift+g+i',
|
||||
NavigateToSettingsBilling: 'shift+g+b',
|
||||
NavigateToSettingsNotificationChannels: 'shift+g+n',
|
||||
NavigateToSettingsServiceAccounts: 'shift+g+k',
|
||||
NavigateToSettingsRoles: 'shift+g+r',
|
||||
NavigateToSettingsMembers: 'shift+g+m',
|
||||
NavigateToLogs: 'shift+l',
|
||||
NavigateToLogsPipelines: 'shift+l+p',
|
||||
NavigateToLogsViews: 'shift+l+v',
|
||||
@@ -74,4 +80,7 @@ export const GlobalShortcutsDescription = {
|
||||
'Navigate to Notification Channels Settings',
|
||||
NavigateToLogsPipelines: 'Navigate to Logs Pipelines',
|
||||
NavigateToLogsViews: 'Navigate to Logs Views',
|
||||
NavigateToSettingsServiceAccounts: 'Navigate to Service Accounts Settings',
|
||||
NavigateToSettingsRoles: 'Navigate to Roles Settings',
|
||||
NavigateToSettingsMembers: 'Navigate to Members Settings',
|
||||
};
|
||||
|
||||
@@ -3,12 +3,12 @@ import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Compass, Dot, House, Plus, Wrench } from '@signozhq/icons';
|
||||
import { PersistedAnnouncementBanner } from '@signozhq/ui';
|
||||
import { Button, Popover } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { useGetMetricsOnboardingStatus } from 'api/generated/services/metrics';
|
||||
import listUserPreferences from 'api/v1/user/preferences/list';
|
||||
import updateUserPreferenceAPI from 'api/v1/user/preferences/name/update';
|
||||
import { PersistedAnnouncementBanner } from 'components/AnnouncementBanner';
|
||||
import Header from 'components/Header/Header';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
@@ -265,20 +265,19 @@ export default function Home(): JSX.Element {
|
||||
return (
|
||||
<div className="home-container">
|
||||
<PersistedAnnouncementBanner
|
||||
type="warning"
|
||||
type="info"
|
||||
storageKey={LOCALSTORAGE.DISMISSED_API_KEYS_DEPRECATION_BANNER}
|
||||
message={
|
||||
<>
|
||||
<strong>API Keys</strong> have been deprecated and replaced by{' '}
|
||||
<strong>Service Accounts</strong>. Please migrate to Service Accounts for
|
||||
programmatic API access.
|
||||
</>
|
||||
}
|
||||
action={{
|
||||
label: 'Go to Service Accounts',
|
||||
onClick: (): void => history.push(ROUTES.SERVICE_ACCOUNTS_SETTINGS),
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<>
|
||||
<strong>API keys</strong> have been deprecated in favour of{' '}
|
||||
<strong>Service accounts</strong>. The existing API Keys have been migrated
|
||||
to service accounts.
|
||||
</>
|
||||
</PersistedAnnouncementBanner>
|
||||
|
||||
<div className="sticky-header">
|
||||
<Header
|
||||
|
||||
@@ -284,6 +284,15 @@ export default function TableViewActions(
|
||||
error,
|
||||
);
|
||||
}
|
||||
// If the value is valid JSON (object or array), pretty-print it for copying
|
||||
try {
|
||||
const parsed = JSON.parse(text);
|
||||
if (typeof parsed === 'object' && parsed !== null) {
|
||||
return JSON.stringify(parsed, null, 2);
|
||||
}
|
||||
} catch {
|
||||
// not JSON, return as-is
|
||||
}
|
||||
return text;
|
||||
}, [fieldData.value]);
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import EditMemberDrawer from 'components/EditMemberDrawer/EditMemberDrawer';
|
||||
import InviteMembersModal from 'components/InviteMembersModal/InviteMembersModal';
|
||||
import MembersTable, { MemberRow } from 'components/MembersTable/MembersTable';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { toISOString } from 'utils/app';
|
||||
|
||||
import { FilterMode, MemberStatus, toMemberStatus } from './utils';
|
||||
@@ -20,7 +19,6 @@ import './MembersSettings.styles.scss';
|
||||
const PAGE_SIZE = 20;
|
||||
|
||||
function MembersSettings(): JSX.Element {
|
||||
const { org } = useAppContext();
|
||||
const history = useHistory();
|
||||
const urlQuery = useUrlQuery();
|
||||
|
||||
@@ -33,9 +31,7 @@ function MembersSettings(): JSX.Element {
|
||||
const [isInviteModalOpen, setIsInviteModalOpen] = useState(false);
|
||||
const [selectedMember, setSelectedMember] = useState<MemberRow | null>(null);
|
||||
|
||||
const { data: usersData, isLoading, refetch: refetchUsers } = useListUsers({
|
||||
query: { queryKey: ['getOrgUser', org?.[0]?.id] },
|
||||
});
|
||||
const { data: usersData, isLoading, refetch: refetchUsers } = useListUsers();
|
||||
|
||||
const allMembers = useMemo(
|
||||
(): MemberRow[] =>
|
||||
@@ -143,7 +139,6 @@ function MembersSettings(): JSX.Element {
|
||||
|
||||
const handleMemberEditComplete = useCallback((): void => {
|
||||
refetchUsers();
|
||||
setSelectedMember(null);
|
||||
}, [refetchUsers]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { TypesUserDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
import { render, screen, userEvent } from 'tests/test-utils';
|
||||
import { UserResponse } from 'types/api/user/getUser';
|
||||
|
||||
import MembersSettings from '../MembersSettings';
|
||||
|
||||
@@ -11,47 +11,39 @@ jest.mock('@signozhq/sonner', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const USERS_ENDPOINT = '*/api/v1/user';
|
||||
const USERS_ENDPOINT = '*/api/v2/users';
|
||||
|
||||
const mockUsers: UserResponse[] = [
|
||||
const mockUsers: TypesUserDTO[] = [
|
||||
{
|
||||
id: 'user-1',
|
||||
displayName: 'Alice Smith',
|
||||
email: 'alice@signoz.io',
|
||||
role: 'ADMIN',
|
||||
status: 'active',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
organization: 'TestOrg',
|
||||
createdAt: new Date('2024-01-01T00:00:00.000Z'),
|
||||
orgId: 'org-1',
|
||||
},
|
||||
{
|
||||
id: 'user-2',
|
||||
displayName: 'Bob Jones',
|
||||
email: 'bob@signoz.io',
|
||||
role: 'VIEWER',
|
||||
status: 'active',
|
||||
createdAt: '2024-01-02T00:00:00.000Z',
|
||||
organization: 'TestOrg',
|
||||
createdAt: new Date('2024-01-02T00:00:00.000Z'),
|
||||
orgId: 'org-1',
|
||||
},
|
||||
{
|
||||
id: 'inv-1',
|
||||
displayName: '',
|
||||
email: 'charlie@signoz.io',
|
||||
role: 'EDITOR',
|
||||
status: 'pending_invite',
|
||||
createdAt: '2024-01-03T00:00:00.000Z',
|
||||
organization: 'TestOrg',
|
||||
createdAt: new Date('2024-01-03T00:00:00.000Z'),
|
||||
orgId: 'org-1',
|
||||
},
|
||||
{
|
||||
id: 'user-3',
|
||||
displayName: 'Dave Deleted',
|
||||
email: 'dave@signoz.io',
|
||||
role: 'VIEWER',
|
||||
status: 'deleted',
|
||||
createdAt: '2024-01-04T00:00:00.000Z',
|
||||
organization: 'TestOrg',
|
||||
createdAt: new Date('2024-01-04T00:00:00.000Z'),
|
||||
orgId: 'org-1',
|
||||
},
|
||||
];
|
||||
@@ -106,7 +98,7 @@ describe('MembersSettings (integration)', () => {
|
||||
await screen.findByText('Alice Smith');
|
||||
|
||||
await user.type(
|
||||
screen.getByPlaceholderText(/Search by name, email, or role/i),
|
||||
screen.getByPlaceholderText(/Search by name or email/i),
|
||||
'bob',
|
||||
);
|
||||
|
||||
|
||||
@@ -22,9 +22,12 @@ jest.mock('react-use', () => ({
|
||||
],
|
||||
}));
|
||||
|
||||
jest.mock('api/v1/user/id/update', () => ({
|
||||
__esModule: true,
|
||||
default: (...args: unknown[]): Promise<unknown> => editUserFn(...args),
|
||||
jest.mock('api/generated/services/users', () => ({
|
||||
...jest.requireActual('api/generated/services/users'),
|
||||
useUpdateMyUserV2: jest.fn(() => ({
|
||||
mutateAsync: (...args: unknown[]): Promise<unknown> => editUserFn(...args),
|
||||
isLoading: false,
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/useDarkMode', () => ({
|
||||
|
||||
@@ -3,7 +3,7 @@ import { toast } from '@signozhq/sonner';
|
||||
import { Button, Form, Input } from 'antd';
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import { useUpdateMyOrganization } from 'api/generated/services/orgs';
|
||||
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import type { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { AxiosError } from 'axios';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { IUser } from 'providers/App/types';
|
||||
|
||||
@@ -198,15 +198,14 @@ function ServiceAccountsSettings(): JSX.Element {
|
||||
<h1 className="sa-settings__title">Service Accounts</h1>
|
||||
<p className="sa-settings__subtitle">
|
||||
Overview of service accounts added to this workspace.{' '}
|
||||
{/* Todo: to add doc links */}
|
||||
{/* <a
|
||||
href="https://signoz.io/docs/service-accounts"
|
||||
<a
|
||||
href="https://signoz.io/docs/manage/administrator-guide/iam/service-accounts"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="sa-settings__learn-more"
|
||||
>
|
||||
Learn more
|
||||
</a> */}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -695,6 +695,15 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
registerShortcut(GlobalShortcuts.NavigateToSettingsNotificationChannels, () =>
|
||||
onClickHandler(ROUTES.ALL_CHANNELS, null),
|
||||
);
|
||||
registerShortcut(GlobalShortcuts.NavigateToSettingsServiceAccounts, () =>
|
||||
onClickHandler(ROUTES.SERVICE_ACCOUNTS_SETTINGS, null),
|
||||
);
|
||||
registerShortcut(GlobalShortcuts.NavigateToSettingsRoles, () =>
|
||||
onClickHandler(ROUTES.ROLES_SETTINGS, null),
|
||||
);
|
||||
registerShortcut(GlobalShortcuts.NavigateToSettingsMembers, () =>
|
||||
onClickHandler(ROUTES.MEMBERS_SETTINGS, null),
|
||||
);
|
||||
registerShortcut(GlobalShortcuts.NavigateToLogsPipelines, () =>
|
||||
onClickHandler(ROUTES.LOGS_PIPELINES, null),
|
||||
);
|
||||
@@ -718,6 +727,9 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
deregisterShortcut(GlobalShortcuts.NavigateToSettingsIngestion);
|
||||
deregisterShortcut(GlobalShortcuts.NavigateToSettingsBilling);
|
||||
deregisterShortcut(GlobalShortcuts.NavigateToSettingsNotificationChannels);
|
||||
deregisterShortcut(GlobalShortcuts.NavigateToSettingsServiceAccounts);
|
||||
deregisterShortcut(GlobalShortcuts.NavigateToSettingsRoles);
|
||||
deregisterShortcut(GlobalShortcuts.NavigateToSettingsMembers);
|
||||
deregisterShortcut(GlobalShortcuts.NavigateToLogsPipelines);
|
||||
deregisterShortcut(GlobalShortcuts.NavigateToLogsViews);
|
||||
deregisterShortcut(GlobalShortcuts.NavigateToTracesViews);
|
||||
|
||||
@@ -14,6 +14,10 @@ import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import NewExplorerCTA from 'container/NewExplorerCTA';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import {
|
||||
useGlobalTimeQueryInvalidate,
|
||||
useIsGlobalTimeQueryRefreshing,
|
||||
} from 'hooks/globalTime';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
@@ -352,7 +356,10 @@ function DateTimeSelection({
|
||||
],
|
||||
);
|
||||
|
||||
const isRefreshingQueries = useIsGlobalTimeQueryRefreshing();
|
||||
const invalidateQueries = useGlobalTimeQueryInvalidate();
|
||||
const onRefreshHandler = (): void => {
|
||||
invalidateQueries();
|
||||
onSelectHandler(selectedTime);
|
||||
onLastRefreshHandler();
|
||||
};
|
||||
@@ -732,7 +739,11 @@ function DateTimeSelection({
|
||||
{showAutoRefresh && selectedTime !== 'custom' && (
|
||||
<div className="refresh-actions">
|
||||
<FormItem hidden={refreshButtonHidden} className="refresh-btn">
|
||||
<Button icon={<SyncOutlined />} onClick={onRefreshHandler} />
|
||||
<Button
|
||||
icon={<SyncOutlined />}
|
||||
loading={!!isRefreshingQueries}
|
||||
onClick={onRefreshHandler}
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
<FormItem>
|
||||
|
||||
2
frontend/src/hooks/globalTime/index.ts
Normal file
2
frontend/src/hooks/globalTime/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { useGlobalTimeQueryInvalidate } from './useGlobalTimeQueryInvalidate';
|
||||
export { useIsGlobalTimeQueryRefreshing } from './useIsGlobalTimeQueryRefreshing';
|
||||
@@ -0,0 +1,16 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
|
||||
/**
|
||||
* Use when you want to invalida any query tracked by {@link REACT_QUERY_KEY.AUTO_REFRESH_QUERY}
|
||||
*/
|
||||
export function useGlobalTimeQueryInvalidate(): () => Promise<void> {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useCallback(async () => {
|
||||
return await queryClient.invalidateQueries({
|
||||
queryKey: [REACT_QUERY_KEY.AUTO_REFRESH_QUERY],
|
||||
});
|
||||
}, [queryClient]);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { useIsFetching } from 'react-query';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
|
||||
/**
|
||||
* Use when you want to know if any query tracked by {@link REACT_QUERY_KEY.AUTO_REFRESH_QUERY} is refreshing
|
||||
*/
|
||||
export function useIsGlobalTimeQueryRefreshing(): boolean {
|
||||
return (
|
||||
useIsFetching({
|
||||
queryKey: [REACT_QUERY_KEY.AUTO_REFRESH_QUERY],
|
||||
}) > 0
|
||||
);
|
||||
}
|
||||
101
frontend/src/hooks/member/useMemberRoleManager.ts
Normal file
101
frontend/src/hooks/member/useMemberRoleManager.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import type { AuthtypesRoleDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import {
|
||||
useGetUser,
|
||||
useRemoveUserRoleByUserIDAndRoleID,
|
||||
useSetRoleByUserID,
|
||||
} from 'api/generated/services/users';
|
||||
|
||||
export interface MemberRoleUpdateFailure {
|
||||
roleName: string;
|
||||
error: unknown;
|
||||
onRetry: () => Promise<void>;
|
||||
}
|
||||
|
||||
interface UseMemberRoleManagerResult {
|
||||
fetchedRoleIds: string[];
|
||||
isLoading: boolean;
|
||||
applyDiff: (
|
||||
localRoleIds: string[],
|
||||
availableRoles: AuthtypesRoleDTO[],
|
||||
) => Promise<MemberRoleUpdateFailure[]>;
|
||||
}
|
||||
|
||||
export function useMemberRoleManager(
|
||||
userId: string,
|
||||
enabled: boolean,
|
||||
): UseMemberRoleManagerResult {
|
||||
const { data: fetchedUser, isLoading } = useGetUser(
|
||||
{ id: userId },
|
||||
{ query: { enabled: !!userId && enabled } },
|
||||
);
|
||||
|
||||
const currentUserRoles = useMemo(() => fetchedUser?.data?.userRoles ?? [], [
|
||||
fetchedUser,
|
||||
]);
|
||||
|
||||
const fetchedRoleIds = useMemo(
|
||||
() =>
|
||||
currentUserRoles
|
||||
.map((ur) => ur.role?.id ?? ur.roleId)
|
||||
.filter((id): id is string => Boolean(id)),
|
||||
[currentUserRoles],
|
||||
);
|
||||
|
||||
const { mutateAsync: setRole } = useSetRoleByUserID();
|
||||
const { mutateAsync: removeRole } = useRemoveUserRoleByUserIDAndRoleID();
|
||||
|
||||
const applyDiff = useCallback(
|
||||
async (
|
||||
localRoleIds: string[],
|
||||
availableRoles: AuthtypesRoleDTO[],
|
||||
): Promise<MemberRoleUpdateFailure[]> => {
|
||||
const currentRoleIdSet = new Set(fetchedRoleIds);
|
||||
const desiredRoleIdSet = new Set(localRoleIds.filter(Boolean));
|
||||
|
||||
const toRemove = currentUserRoles.filter((ur) => {
|
||||
const id = ur.role?.id ?? ur.roleId;
|
||||
return id && !desiredRoleIdSet.has(id);
|
||||
});
|
||||
const toAdd = availableRoles.filter(
|
||||
(r) => r.id && desiredRoleIdSet.has(r.id) && !currentRoleIdSet.has(r.id),
|
||||
);
|
||||
|
||||
const allOps = [
|
||||
...toRemove.map((ur) => ({
|
||||
roleName: ur.role?.name ?? 'unknown',
|
||||
run: (): ReturnType<typeof removeRole> =>
|
||||
removeRole({
|
||||
pathParams: {
|
||||
id: userId,
|
||||
roleId: ur.role?.id ?? ur.roleId ?? '',
|
||||
},
|
||||
}),
|
||||
})),
|
||||
...toAdd.map((role) => ({
|
||||
roleName: role.name ?? 'unknown',
|
||||
run: (): ReturnType<typeof setRole> =>
|
||||
setRole({
|
||||
pathParams: { id: userId },
|
||||
data: { name: role.name ?? '' },
|
||||
}),
|
||||
})),
|
||||
];
|
||||
|
||||
const results = await Promise.allSettled(allOps.map((op) => op.run()));
|
||||
|
||||
const failures: MemberRoleUpdateFailure[] = [];
|
||||
results.forEach((result, i) => {
|
||||
if (result.status === 'rejected') {
|
||||
const { roleName, run } = allOps[i];
|
||||
failures.push({ roleName, error: result.reason, onRetry: run });
|
||||
}
|
||||
});
|
||||
|
||||
return failures;
|
||||
},
|
||||
[userId, fetchedRoleIds, currentUserRoles, setRole, removeRole],
|
||||
);
|
||||
|
||||
return { fetchedRoleIds, isLoading, applyDiff };
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { Provider } from 'react-redux';
|
||||
import AppRoutes from 'AppRoutes';
|
||||
import { AxiosError } from 'axios';
|
||||
import { GlobalTimeStoreAdapter } from 'components/GlobalTimeStoreAdapter/GlobalTimeStoreAdapter';
|
||||
import { ThemeProvider } from 'hooks/useDarkMode';
|
||||
import { NuqsAdapter } from 'nuqs/adapters/react';
|
||||
import { AppProvider } from 'providers/App/App';
|
||||
@@ -51,6 +52,7 @@ if (container) {
|
||||
<TimezoneProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Provider store={store}>
|
||||
<GlobalTimeStoreAdapter />
|
||||
<AppProvider>
|
||||
<AppRoutes />
|
||||
</AppProvider>
|
||||
|
||||
@@ -143,7 +143,9 @@ function SettingsPage(): JSX.Element {
|
||||
isEnabled:
|
||||
item.key === ROUTES.ORG_SETTINGS ||
|
||||
item.key === ROUTES.MEMBERS_SETTINGS ||
|
||||
item.key === ROUTES.SERVICE_ACCOUNTS_SETTINGS
|
||||
item.key === ROUTES.SERVICE_ACCOUNTS_SETTINGS ||
|
||||
item.key === ROUTES.ROLES_SETTINGS ||
|
||||
item.key === ROUTES.ROLE_DETAILS
|
||||
? true
|
||||
: item.isEnabled,
|
||||
}));
|
||||
|
||||
@@ -62,12 +62,16 @@ export const getRoutes = (
|
||||
settings.push(...alertChannels(t));
|
||||
|
||||
if (isAdmin) {
|
||||
settings.push(...membersSettings(t), ...serviceAccountsSettings(t));
|
||||
settings.push(
|
||||
...membersSettings(t),
|
||||
...serviceAccountsSettings(t),
|
||||
...rolesSettings(t),
|
||||
...roleDetails(t),
|
||||
);
|
||||
}
|
||||
|
||||
// todo: Sagar - check the condition for role list and details page, to whom we want to serve
|
||||
if ((isCloudUser || isEnterpriseSelfHostedUser) && isAdmin) {
|
||||
settings.push(...billingSettings(t), ...rolesSettings(t), ...roleDetails(t));
|
||||
settings.push(...billingSettings(t));
|
||||
}
|
||||
|
||||
settings.push(
|
||||
|
||||
@@ -85,7 +85,11 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
|
||||
query: { enabled: isLoggedIn },
|
||||
});
|
||||
|
||||
const { data: orgData, isFetching: isFetchingOrgData } = useGetMyOrganization({
|
||||
const {
|
||||
data: orgData,
|
||||
isFetching: isFetchingOrgData,
|
||||
error: orgFetchDataError,
|
||||
} = useGetMyOrganization({
|
||||
query: { enabled: isLoggedIn },
|
||||
});
|
||||
|
||||
@@ -100,7 +104,8 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
|
||||
|
||||
const isFetchingUser =
|
||||
isFetchingUserData || isFetchingOrgData || isFetchingPermissions;
|
||||
const userFetchError = userFetchDataError || errorOnPermissions;
|
||||
const userFetchError =
|
||||
userFetchDataError || orgFetchDataError || errorOnPermissions;
|
||||
|
||||
const userRole = useMemo(() => {
|
||||
if (permissionsResult?.[IsAdminPermission]?.isGranted) {
|
||||
@@ -151,10 +156,18 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
|
||||
return [{ createdAt: 0, id: orgId, displayName: orgDisplayName ?? '' }];
|
||||
}
|
||||
const orgIndex = prev.findIndex((e) => e.id === orgId);
|
||||
|
||||
if (orgIndex === -1) {
|
||||
return [
|
||||
...prev,
|
||||
{ createdAt: 0, id: orgId, displayName: orgDisplayName ?? '' },
|
||||
];
|
||||
}
|
||||
|
||||
const updatedOrg: Organization[] = [
|
||||
...prev.slice(0, orgIndex),
|
||||
{ createdAt: 0, id: orgId, displayName: orgDisplayName ?? '' },
|
||||
...prev.slice(orgIndex + 1, prev.length),
|
||||
...prev.slice(orgIndex + 1),
|
||||
];
|
||||
return updatedOrg;
|
||||
});
|
||||
@@ -288,6 +301,9 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
|
||||
(orgId: string, updatedOrgName: string): void => {
|
||||
if (org && org.length > 0) {
|
||||
const orgIndex = org.findIndex((e) => e.id === orgId);
|
||||
if (orgIndex === -1) {
|
||||
return;
|
||||
}
|
||||
const updatedOrg: Organization[] = [
|
||||
...org.slice(0, orgIndex),
|
||||
{
|
||||
@@ -295,7 +311,7 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
|
||||
id: orgId,
|
||||
displayName: updatedOrgName,
|
||||
},
|
||||
...org.slice(orgIndex + 1, org.length),
|
||||
...org.slice(orgIndex + 1),
|
||||
];
|
||||
setOrg(updatedOrg);
|
||||
setDefaultUser((prev) => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ReactElement } from 'react';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import setLocalStorageApi from 'api/browser/localstorage/set';
|
||||
import {
|
||||
import type {
|
||||
AuthtypesGettableTransactionDTO,
|
||||
AuthtypesTransactionDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
@@ -15,6 +15,8 @@ import { USER_ROLES } from 'types/roles';
|
||||
import { AppProvider, useAppContext } from '../App';
|
||||
|
||||
const AUTHZ_CHECK_URL = 'http://localhost/api/v1/authz/check';
|
||||
const MY_USER_URL = 'http://localhost/api/v2/users/me';
|
||||
const MY_ORG_URL = 'http://localhost/api/v2/orgs/me';
|
||||
|
||||
jest.mock('constants/env', () => ({
|
||||
ENVIRONMENT: { baseURL: 'http://localhost', wsURL: '' },
|
||||
@@ -227,6 +229,132 @@ describe('AppProvider user.role from permissions', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('AppProvider user and org data from v2 APIs', () => {
|
||||
beforeEach(() => {
|
||||
queryClient.clear();
|
||||
setLocalStorageApi(LOCALSTORAGE.IS_LOGGED_IN, 'true');
|
||||
});
|
||||
|
||||
it('populates user fields from GET /api/v2/users/me', async () => {
|
||||
server.use(
|
||||
rest.get(MY_USER_URL, (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
data: {
|
||||
id: 'u-123',
|
||||
displayName: 'Test User',
|
||||
email: 'test@signoz.io',
|
||||
orgId: 'org-abc',
|
||||
isRoot: false,
|
||||
status: 'active',
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
rest.get(MY_ORG_URL, (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({ data: { id: 'org-abc', displayName: 'My Org' } }),
|
||||
),
|
||||
),
|
||||
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
|
||||
const payload = await req.json();
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json(authzMockResponse(payload, [false, false, false])),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
const wrapper = createWrapper();
|
||||
const { result } = renderHook(() => useAppContext(), { wrapper });
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(result.current.user.id).toBe('u-123');
|
||||
expect(result.current.user.displayName).toBe('Test User');
|
||||
expect(result.current.user.email).toBe('test@signoz.io');
|
||||
expect(result.current.user.orgId).toBe('org-abc');
|
||||
},
|
||||
{ timeout: 2000 },
|
||||
);
|
||||
});
|
||||
|
||||
it('populates org state from GET /api/v2/orgs/me', async () => {
|
||||
server.use(
|
||||
rest.get(MY_ORG_URL, (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
data: {
|
||||
id: 'org-abc',
|
||||
displayName: 'My Org',
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
rest.get(MY_USER_URL, (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({ data: { id: 'u-default', email: 'default@signoz.io' } }),
|
||||
),
|
||||
),
|
||||
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
|
||||
const payload = await req.json();
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json(authzMockResponse(payload, [false, false, false])),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
const wrapper = createWrapper();
|
||||
const { result } = renderHook(() => useAppContext(), { wrapper });
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(result.current.org).not.toBeNull();
|
||||
const org = result.current.org?.[0];
|
||||
expect(org?.id).toBe('org-abc');
|
||||
expect(org?.displayName).toBe('My Org');
|
||||
},
|
||||
{ timeout: 2000 },
|
||||
);
|
||||
});
|
||||
|
||||
it('sets isFetchingUser false once both user and org calls complete', async () => {
|
||||
server.use(
|
||||
rest.get(MY_USER_URL, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ data: { id: 'u-1', email: 'a@b.com' } })),
|
||||
),
|
||||
rest.get(MY_ORG_URL, (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({ data: { id: 'org-1', displayName: 'Org' } }),
|
||||
),
|
||||
),
|
||||
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
|
||||
const payload = await req.json();
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json(authzMockResponse(payload, [false, false, false])),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
const wrapper = createWrapper();
|
||||
const { result } = renderHook(() => useAppContext(), { wrapper });
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(result.current.isFetchingUser).toBe(false);
|
||||
},
|
||||
{ timeout: 2000 },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('AppProvider when authz/check fails', () => {
|
||||
beforeEach(() => {
|
||||
queryClient.clear();
|
||||
|
||||
204
frontend/src/store/globalTime/__tests__/globalTimeStore.test.ts
Normal file
204
frontend/src/store/globalTime/__tests__/globalTimeStore.test.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants';
|
||||
|
||||
import { useGlobalTimeStore } from '../globalTimeStore';
|
||||
import { GlobalTimeSelectedTime } from '../types';
|
||||
import { createCustomTimeRange, NANO_SECOND_MULTIPLIER } from '../utils';
|
||||
|
||||
describe('globalTimeStore', () => {
|
||||
beforeEach(() => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
act(() => {
|
||||
result.current.setSelectedTime(DEFAULT_TIME_RANGE, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('initial state', () => {
|
||||
it(`should have default selectedTime of ${DEFAULT_TIME_RANGE}`, () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
expect(result.current.selectedTime).toBe(DEFAULT_TIME_RANGE);
|
||||
});
|
||||
|
||||
it('should have isRefreshEnabled as false by default', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
expect(result.current.isRefreshEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('should have refreshInterval as 0 by default', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
expect(result.current.refreshInterval).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setSelectedTime', () => {
|
||||
it('should update selectedTime', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime('15m');
|
||||
});
|
||||
|
||||
expect(result.current.selectedTime).toBe('15m');
|
||||
});
|
||||
|
||||
it('should update refreshInterval when provided', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime('15m', 5000);
|
||||
});
|
||||
|
||||
expect(result.current.refreshInterval).toBe(5000);
|
||||
});
|
||||
|
||||
it('should keep existing refreshInterval when not provided', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime('15m', 5000);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime('1h');
|
||||
});
|
||||
|
||||
expect(result.current.refreshInterval).toBe(5000);
|
||||
});
|
||||
|
||||
it('should enable refresh for relative time with refreshInterval > 0', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime('15m', 5000);
|
||||
});
|
||||
|
||||
expect(result.current.isRefreshEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it('should disable refresh for relative time with refreshInterval = 0', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime('15m', 0);
|
||||
});
|
||||
|
||||
expect(result.current.isRefreshEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('should disable refresh for custom time range even with refreshInterval > 0', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
const customTime = createCustomTimeRange(1000000000, 2000000000);
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime(customTime, 5000);
|
||||
});
|
||||
|
||||
expect(result.current.isRefreshEnabled).toBe(false);
|
||||
expect(result.current.refreshInterval).toBe(5000);
|
||||
});
|
||||
|
||||
it('should handle various relative time formats', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
const timeFormats: GlobalTimeSelectedTime[] = [
|
||||
'1m',
|
||||
'5m',
|
||||
'15m',
|
||||
'30m',
|
||||
'1h',
|
||||
'3h',
|
||||
'6h',
|
||||
'1d',
|
||||
'1w',
|
||||
];
|
||||
|
||||
timeFormats.forEach((time) => {
|
||||
act(() => {
|
||||
result.current.setSelectedTime(time, 10000);
|
||||
});
|
||||
|
||||
expect(result.current.selectedTime).toBe(time);
|
||||
expect(result.current.isRefreshEnabled).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMinMaxTime', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(new Date('2024-01-15T12:00:00.000Z'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should return min/max time for custom time range', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
const minTime = 1000000000;
|
||||
const maxTime = 2000000000;
|
||||
const customTime = createCustomTimeRange(minTime, maxTime);
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime(customTime);
|
||||
});
|
||||
|
||||
const {
|
||||
minTime: resultMin,
|
||||
maxTime: resultMax,
|
||||
} = result.current.getMinMaxTime();
|
||||
expect(resultMin).toBe(minTime);
|
||||
expect(resultMax).toBe(maxTime);
|
||||
});
|
||||
|
||||
it('should compute fresh min/max time for relative time', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime('15m');
|
||||
});
|
||||
|
||||
const { minTime, maxTime } = result.current.getMinMaxTime();
|
||||
const now = Date.now() * NANO_SECOND_MULTIPLIER;
|
||||
const fifteenMinutesNs = 15 * 60 * 1000 * NANO_SECOND_MULTIPLIER;
|
||||
|
||||
expect(maxTime).toBe(now);
|
||||
expect(minTime).toBe(now - fifteenMinutesNs);
|
||||
});
|
||||
|
||||
it('should return different values on subsequent calls for relative time', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime('15m');
|
||||
});
|
||||
|
||||
const first = result.current.getMinMaxTime();
|
||||
|
||||
// Advance time by 1 second
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(1000);
|
||||
});
|
||||
|
||||
const second = result.current.getMinMaxTime();
|
||||
|
||||
// maxTime should be different (1 second later)
|
||||
expect(second.maxTime).toBe(first.maxTime + 1000 * NANO_SECOND_MULTIPLIER);
|
||||
expect(second.minTime).toBe(first.minTime + 1000 * NANO_SECOND_MULTIPLIER);
|
||||
});
|
||||
});
|
||||
|
||||
describe('store isolation', () => {
|
||||
it('should share state between multiple hook instances', () => {
|
||||
const { result: result1 } = renderHook(() => useGlobalTimeStore());
|
||||
const { result: result2 } = renderHook(() => useGlobalTimeStore());
|
||||
|
||||
act(() => {
|
||||
result1.current.setSelectedTime('1h', 10000);
|
||||
});
|
||||
|
||||
expect(result2.current.selectedTime).toBe('1h');
|
||||
expect(result2.current.refreshInterval).toBe(10000);
|
||||
expect(result2.current.isRefreshEnabled).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
139
frontend/src/store/globalTime/__tests__/utils.test.ts
Normal file
139
frontend/src/store/globalTime/__tests__/utils.test.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import {
|
||||
createCustomTimeRange,
|
||||
CUSTOM_TIME_SEPARATOR,
|
||||
isCustomTimeRange,
|
||||
NANO_SECOND_MULTIPLIER,
|
||||
parseCustomTimeRange,
|
||||
parseSelectedTime,
|
||||
} from '../utils';
|
||||
|
||||
describe('globalTime/utils', () => {
|
||||
describe('CUSTOM_TIME_SEPARATOR', () => {
|
||||
it('should be defined as ||_||', () => {
|
||||
expect(CUSTOM_TIME_SEPARATOR).toBe('||_||');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isCustomTimeRange', () => {
|
||||
it('should return true for custom time range strings', () => {
|
||||
expect(isCustomTimeRange('1000000000||_||2000000000')).toBe(true);
|
||||
expect(isCustomTimeRange('0||_||0')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for relative time strings', () => {
|
||||
expect(isCustomTimeRange('15m')).toBe(false);
|
||||
expect(isCustomTimeRange('1h')).toBe(false);
|
||||
expect(isCustomTimeRange('1d')).toBe(false);
|
||||
expect(isCustomTimeRange('30s')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for empty string', () => {
|
||||
expect(isCustomTimeRange('')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createCustomTimeRange', () => {
|
||||
it('should create a custom time range string from min and max times', () => {
|
||||
const minTime = 1000000000;
|
||||
const maxTime = 2000000000;
|
||||
const result = createCustomTimeRange(minTime, maxTime);
|
||||
expect(result).toBe(`${minTime}${CUSTOM_TIME_SEPARATOR}${maxTime}`);
|
||||
});
|
||||
|
||||
it('should handle zero values', () => {
|
||||
const result = createCustomTimeRange(0, 0);
|
||||
expect(result).toBe(`0${CUSTOM_TIME_SEPARATOR}0`);
|
||||
});
|
||||
|
||||
it('should handle large nanosecond timestamps', () => {
|
||||
const minTime = 1700000000000000000;
|
||||
const maxTime = 1700000001000000000;
|
||||
const result = createCustomTimeRange(minTime, maxTime);
|
||||
expect(result).toBe(`${minTime}${CUSTOM_TIME_SEPARATOR}${maxTime}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseCustomTimeRange', () => {
|
||||
it('should parse a valid custom time range string', () => {
|
||||
const minTime = 1000000000;
|
||||
const maxTime = 2000000000;
|
||||
const timeString = `${minTime}${CUSTOM_TIME_SEPARATOR}${maxTime}`;
|
||||
const result = parseCustomTimeRange(timeString);
|
||||
expect(result).toEqual({ minTime, maxTime });
|
||||
});
|
||||
|
||||
it('should return null for non-custom time range strings', () => {
|
||||
expect(parseCustomTimeRange('15m')).toBeNull();
|
||||
expect(parseCustomTimeRange('1h')).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for invalid numeric values', () => {
|
||||
expect(parseCustomTimeRange(`abc${CUSTOM_TIME_SEPARATOR}def`)).toBeNull();
|
||||
expect(parseCustomTimeRange(`123${CUSTOM_TIME_SEPARATOR}def`)).toBeNull();
|
||||
expect(parseCustomTimeRange(`abc${CUSTOM_TIME_SEPARATOR}456`)).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle zero values', () => {
|
||||
const result = parseCustomTimeRange(`0${CUSTOM_TIME_SEPARATOR}0`);
|
||||
expect(result).toEqual({ minTime: 0, maxTime: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseSelectedTime', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(new Date('2024-01-15T12:00:00.000Z'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should parse custom time range and return min/max values', () => {
|
||||
const minTime = 1000000000;
|
||||
const maxTime = 2000000000;
|
||||
const timeString = createCustomTimeRange(minTime, maxTime);
|
||||
const result = parseSelectedTime(timeString);
|
||||
expect(result).toEqual({ minTime, maxTime });
|
||||
});
|
||||
|
||||
it('should return fallback for invalid custom time range', () => {
|
||||
const invalidCustom = `invalid${CUSTOM_TIME_SEPARATOR}values`;
|
||||
const result = parseSelectedTime(invalidCustom);
|
||||
const now = Date.now() * NANO_SECOND_MULTIPLIER;
|
||||
const fallbackDuration = 30 * 1000 * NANO_SECOND_MULTIPLIER; // 30s in nanoseconds
|
||||
expect(result.maxTime).toBe(now);
|
||||
expect(result.minTime).toBe(now - fallbackDuration);
|
||||
});
|
||||
|
||||
it('should parse relative time strings using getMinMaxForSelectedTime', () => {
|
||||
const result = parseSelectedTime('15m');
|
||||
const now = Date.now() * NANO_SECOND_MULTIPLIER;
|
||||
// 15 minutes in nanoseconds
|
||||
const fifteenMinutesNs = 15 * 60 * 1000 * NANO_SECOND_MULTIPLIER;
|
||||
|
||||
expect(result.maxTime).toBe(now);
|
||||
expect(result.minTime).toBe(now - fifteenMinutesNs);
|
||||
});
|
||||
|
||||
it('should parse 1h relative time', () => {
|
||||
const result = parseSelectedTime('1h');
|
||||
const now = Date.now() * NANO_SECOND_MULTIPLIER;
|
||||
// 1 hour in nanoseconds
|
||||
const oneHourNs = 60 * 60 * 1000 * NANO_SECOND_MULTIPLIER;
|
||||
|
||||
expect(result.maxTime).toBe(now);
|
||||
expect(result.minTime).toBe(now - oneHourNs);
|
||||
});
|
||||
|
||||
it('should parse 1d relative time', () => {
|
||||
const result = parseSelectedTime('1d');
|
||||
const now = Date.now() * NANO_SECOND_MULTIPLIER;
|
||||
// 1 day in nanoseconds
|
||||
const oneDayNs = 24 * 60 * 60 * 1000 * NANO_SECOND_MULTIPLIER;
|
||||
|
||||
expect(result.maxTime).toBe(now);
|
||||
expect(result.minTime).toBe(now - oneDayNs);
|
||||
});
|
||||
});
|
||||
});
|
||||
33
frontend/src/store/globalTime/globalTimeStore.ts
Normal file
33
frontend/src/store/globalTime/globalTimeStore.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants';
|
||||
import { create } from 'zustand';
|
||||
|
||||
import {
|
||||
IGlobalTimeStoreActions,
|
||||
IGlobalTimeStoreState,
|
||||
ParsedTimeRange,
|
||||
} from './types';
|
||||
import { isCustomTimeRange, parseSelectedTime } from './utils';
|
||||
|
||||
export type IGlobalTimeStore = IGlobalTimeStoreState & IGlobalTimeStoreActions;
|
||||
|
||||
export const useGlobalTimeStore = create<IGlobalTimeStore>((set, get) => ({
|
||||
selectedTime: DEFAULT_TIME_RANGE,
|
||||
isRefreshEnabled: false,
|
||||
refreshInterval: 0,
|
||||
setSelectedTime: (selectedTime, refreshInterval): void => {
|
||||
set((state) => {
|
||||
const newRefreshInterval = refreshInterval ?? state.refreshInterval;
|
||||
const isCustom = isCustomTimeRange(selectedTime);
|
||||
|
||||
return {
|
||||
selectedTime,
|
||||
refreshInterval: newRefreshInterval,
|
||||
isRefreshEnabled: !isCustom && newRefreshInterval > 0,
|
||||
};
|
||||
});
|
||||
},
|
||||
getMinMaxTime: (): ParsedTimeRange => {
|
||||
const { selectedTime } = get();
|
||||
return parseSelectedTime(selectedTime);
|
||||
},
|
||||
}));
|
||||
9
frontend/src/store/globalTime/index.ts
Normal file
9
frontend/src/store/globalTime/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export { useGlobalTimeStore } from './globalTimeStore';
|
||||
export type { IGlobalTimeStoreState, ParsedTimeRange } from './types';
|
||||
export {
|
||||
createCustomTimeRange,
|
||||
CUSTOM_TIME_SEPARATOR,
|
||||
isCustomTimeRange,
|
||||
parseCustomTimeRange,
|
||||
parseSelectedTime,
|
||||
} from './utils';
|
||||
52
frontend/src/store/globalTime/types.ts
Normal file
52
frontend/src/store/globalTime/types.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Time } from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
|
||||
export type CustomTimeRangeSeparator = '||_||';
|
||||
export type CustomTimeRange = `${number}${CustomTimeRangeSeparator}${number}`;
|
||||
export type GlobalTimeSelectedTime = Time | CustomTimeRange;
|
||||
|
||||
export interface IGlobalTimeStoreState {
|
||||
/**
|
||||
* The selected time range, can be:
|
||||
* - Relative duration: '1m', '5m', '15m', '1h', '1d', etc.
|
||||
* - Custom range: '<minTimeUnixNano>||_||<maxTimeUnixNano>' format
|
||||
*/
|
||||
selectedTime: GlobalTimeSelectedTime;
|
||||
|
||||
/**
|
||||
* Whether auto-refresh is enabled.
|
||||
* Automatically computed: true for duration-based times, false for custom ranges.
|
||||
*/
|
||||
isRefreshEnabled: boolean;
|
||||
|
||||
/**
|
||||
* The refresh interval in milliseconds (e.g., 5000 for 5s, 30000 for 30s)
|
||||
* Only used when isRefreshEnabled is true
|
||||
*/
|
||||
refreshInterval: number;
|
||||
}
|
||||
|
||||
export interface ParsedTimeRange {
|
||||
minTime: number;
|
||||
maxTime: number;
|
||||
}
|
||||
|
||||
export interface IGlobalTimeStoreActions {
|
||||
/**
|
||||
* Set the selected time and optionally the refresh interval.
|
||||
* isRefreshEnabled is automatically computed:
|
||||
* - Custom time ranges: always false
|
||||
* - Duration times with refreshInterval > 0: true
|
||||
* - Duration times with refreshInterval = 0: false
|
||||
*/
|
||||
setSelectedTime: (
|
||||
selectedTime: GlobalTimeSelectedTime,
|
||||
refreshInterval?: number,
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* Get the current min/max time values parsed from selectedTime.
|
||||
* For durations, computes fresh values based on Date.now().
|
||||
* For custom ranges, extracts the stored values.
|
||||
*/
|
||||
getMinMaxTime: () => ParsedTimeRange;
|
||||
}
|
||||
87
frontend/src/store/globalTime/utils.ts
Normal file
87
frontend/src/store/globalTime/utils.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { Time } from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { getMinMaxForSelectedTime } from 'lib/getMinMax';
|
||||
|
||||
import { REACT_QUERY_KEY } from '../../constants/reactQueryKeys';
|
||||
import {
|
||||
CustomTimeRange,
|
||||
CustomTimeRangeSeparator,
|
||||
GlobalTimeSelectedTime,
|
||||
ParsedTimeRange,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* Custom time range separator used in the selectedTime string
|
||||
*/
|
||||
export const CUSTOM_TIME_SEPARATOR: CustomTimeRangeSeparator = '||_||';
|
||||
|
||||
/**
|
||||
* Check if selectedTime represents a custom time range
|
||||
*/
|
||||
export function isCustomTimeRange(selectedTime: string): boolean {
|
||||
return selectedTime.includes(CUSTOM_TIME_SEPARATOR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a custom time range string from min/max times (in nanoseconds)
|
||||
*/
|
||||
export function createCustomTimeRange(
|
||||
minTime: number,
|
||||
maxTime: number,
|
||||
): CustomTimeRange {
|
||||
return `${minTime}${CUSTOM_TIME_SEPARATOR}${maxTime}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the custom time range string to get min/max times (in nanoseconds)
|
||||
*/
|
||||
export function parseCustomTimeRange(
|
||||
selectedTime: string,
|
||||
): ParsedTimeRange | null {
|
||||
if (!isCustomTimeRange(selectedTime)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [minStr, maxStr] = selectedTime.split(CUSTOM_TIME_SEPARATOR);
|
||||
const minTime = parseInt(minStr, 10);
|
||||
const maxTime = parseInt(maxStr, 10);
|
||||
|
||||
if (Number.isNaN(minTime) || Number.isNaN(maxTime)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { minTime, maxTime };
|
||||
}
|
||||
|
||||
export const NANO_SECOND_MULTIPLIER = 1000000;
|
||||
const fallbackDurationInNanoSeconds = 30 * 1000 * NANO_SECOND_MULTIPLIER; // 30s
|
||||
|
||||
/**
|
||||
* Parse the selectedTime string to get min/max time values.
|
||||
* For relative times, computes fresh values based on Date.now().
|
||||
* For custom times, extracts the stored min/max values.
|
||||
*/
|
||||
export function parseSelectedTime(selectedTime: string): ParsedTimeRange {
|
||||
if (isCustomTimeRange(selectedTime)) {
|
||||
const parsed = parseCustomTimeRange(selectedTime);
|
||||
if (parsed) {
|
||||
return parsed;
|
||||
}
|
||||
// Fallback to current time if parsing fails
|
||||
const now = Date.now() * NANO_SECOND_MULTIPLIER;
|
||||
return { minTime: now - fallbackDurationInNanoSeconds, maxTime: now };
|
||||
}
|
||||
|
||||
// It's a relative time like '15m', '1h', etc.
|
||||
// Use getMinMaxForSelectedTime which computes from Date.now()
|
||||
return getMinMaxForSelectedTime(selectedTime as Time, 0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use to build your react-query key for auto-refresh queries
|
||||
*/
|
||||
export function getAutoRefreshQueryKey(
|
||||
selectedTime: GlobalTimeSelectedTime,
|
||||
...queryParts: unknown[]
|
||||
): unknown[] {
|
||||
return [REACT_QUERY_KEY.AUTO_REFRESH_QUERY, ...queryParts, selectedTime];
|
||||
}
|
||||
256
grammar/HavingExpression.g4
Normal file
256
grammar/HavingExpression.g4
Normal file
@@ -0,0 +1,256 @@
|
||||
grammar HavingExpression;
|
||||
|
||||
/*
|
||||
* Parser Rules
|
||||
*/
|
||||
|
||||
query
|
||||
: expression EOF
|
||||
;
|
||||
|
||||
// Expression with standard boolean precedence:
|
||||
// - parentheses > NOT > AND > OR
|
||||
expression
|
||||
: orExpression
|
||||
;
|
||||
|
||||
// OR expressions
|
||||
orExpression
|
||||
: andExpression ( OR andExpression )*
|
||||
;
|
||||
|
||||
// AND expressions + optional chaining with implicit AND if no OR is present
|
||||
andExpression
|
||||
: primary ( AND primary | primary )*
|
||||
;
|
||||
|
||||
// Primary: an optionally negated expression.
|
||||
// NOT can be applied to a parenthesized expression or a bare comparison / IN-test.
|
||||
// E.g.: NOT (count() > 100 AND sum(bytes) < 500)
|
||||
// NOT count() > 100
|
||||
// count() IN (1, 2, 3) -- NOT here is part of comparison, see below
|
||||
// count() NOT IN (1, 2, 3)
|
||||
primary
|
||||
: NOT? LPAREN orExpression RPAREN
|
||||
| NOT? comparison
|
||||
;
|
||||
|
||||
/*
|
||||
* Comparison between two arithmetic operands, or an IN / NOT IN membership test.
|
||||
* E.g.: count() > 100, total_duration >= 500, __result_0 != 0
|
||||
* count() IN (1, 2, 3), sum(bytes) NOT IN (0, -1)
|
||||
* count() IN [1, 2, 3], sum(bytes) NOT IN [0, -1]
|
||||
*/
|
||||
comparison
|
||||
: operand compOp operand
|
||||
| operand NOT? IN LPAREN inList RPAREN
|
||||
| operand NOT? IN LBRACK inList RBRACK
|
||||
;
|
||||
|
||||
compOp
|
||||
: EQUALS
|
||||
| NOT_EQUALS
|
||||
| NEQ
|
||||
| LT
|
||||
| LE
|
||||
| GT
|
||||
| GE
|
||||
;
|
||||
|
||||
/*
|
||||
* IN-list: a comma-separated list of numeric literals, each optionally signed.
|
||||
* E.g.: (1, 2, 3), [100, 200, 500], (-1, 0, 1)
|
||||
*/
|
||||
inList
|
||||
: signedNumber ( COMMA signedNumber )*
|
||||
;
|
||||
|
||||
/*
|
||||
* A signed number allows an optional leading +/- before a numeric literal.
|
||||
* Used in IN-lists where a bare minus is unambiguous (no binary operand to the left).
|
||||
*/
|
||||
signedNumber
|
||||
: (PLUS | MINUS)? NUMBER
|
||||
;
|
||||
|
||||
/*
|
||||
* Operands support additive arithmetic (+/-).
|
||||
* E.g.: sum(a) + sum(b) > 1000, count() - 10 > 0
|
||||
*/
|
||||
operand
|
||||
: operand (PLUS | MINUS) term
|
||||
| term
|
||||
;
|
||||
|
||||
/*
|
||||
* Terms support multiplicative arithmetic (*, /, %)
|
||||
* E.g.: count() * 2 > 100, sum(bytes) / 1024 > 10
|
||||
*/
|
||||
term
|
||||
: term (STAR | SLASH | PERCENT) factor
|
||||
| factor
|
||||
;
|
||||
|
||||
/*
|
||||
* Factors: atoms, parenthesized operands, or unary-signed sub-factors.
|
||||
* E.g.: (sum(a) + sum(b)) * 2 > 100, -count() > 0, -(avg(x) + 1) > 0
|
||||
* -10 (unary minus applied to the literal 10), count() - 10 > 0
|
||||
*
|
||||
* Note: the NUMBER rule does NOT include a leading sign, so `-10` is always
|
||||
* tokenised as MINUS followed by NUMBER(10). Unary minus in `factor` handles
|
||||
* negative literals just as it handles negative function calls or identifiers,
|
||||
* and the binary MINUS in `operand` handles `count()-10` naturally.
|
||||
*/
|
||||
factor
|
||||
: (PLUS | MINUS) factor
|
||||
| LPAREN operand RPAREN
|
||||
| atom
|
||||
;
|
||||
|
||||
/*
|
||||
* Atoms are the basic building blocks of arithmetic operands:
|
||||
* - aggregate function calls: count(), sum(bytes), avg(duration)
|
||||
* - identifier references: aliases, result refs (__result, __result_0, __result0)
|
||||
* - numeric literals: 100, 0.5, 1e6
|
||||
* - string literals: 'xyz' — recognized so we can give a friendly error
|
||||
*
|
||||
* String literals in HAVING are always invalid (aggregator results are numeric),
|
||||
* but we accept them here so the visitor can produce a clear error message instead
|
||||
* of a raw syntax error.
|
||||
*/
|
||||
atom
|
||||
: functionCall
|
||||
| identifier
|
||||
| NUMBER
|
||||
| STRING
|
||||
;
|
||||
|
||||
/*
|
||||
* Aggregate function calls, e.g.:
|
||||
* count(), sum(bytes), avg(duration_nano)
|
||||
* countIf(level='error'), sumIf(bytes, status > 400)
|
||||
* p99(duration), avg(sum(cpu_usage))
|
||||
*
|
||||
* Function arguments are parsed as a permissive token sequence (funcArgToken+)
|
||||
* so that complex aggregation expressions — including nested function calls and
|
||||
* filter predicates with string literals — can be referenced verbatim in the
|
||||
* HAVING expression. The visitor looks up the full call text (whitespace-free,
|
||||
* via ctx.GetText()) in the column map, which stores normalized (space-stripped)
|
||||
* aggregation expression keys.
|
||||
*/
|
||||
functionCall
|
||||
: IDENTIFIER LPAREN functionArgList? RPAREN
|
||||
;
|
||||
|
||||
functionArgList
|
||||
: funcArg ( COMMA funcArg )*
|
||||
;
|
||||
|
||||
/*
|
||||
* A single function argument is one or more consecutive arg-tokens.
|
||||
* Commas at the top level separate arguments; closing parens terminate the list.
|
||||
*/
|
||||
funcArg
|
||||
: funcArgToken+
|
||||
;
|
||||
|
||||
/*
|
||||
* Permissive token set for function argument content. Covers:
|
||||
* - simple identifiers: bytes, duration
|
||||
* - string literals: 'error', "info"
|
||||
* - numeric literals: 200, 3.14
|
||||
* - comparison operators: level='error', status > 400
|
||||
* - arithmetic operators: x + y
|
||||
* - boolean connectives: level='error' AND status=200
|
||||
* - balanced parens: nested calls like sum(duration)
|
||||
*/
|
||||
funcArgToken
|
||||
: IDENTIFIER
|
||||
| STRING
|
||||
| NUMBER
|
||||
| BOOL
|
||||
| EQUALS | NOT_EQUALS | NEQ | LT | LE | GT | GE
|
||||
| PLUS | MINUS | STAR | SLASH | PERCENT
|
||||
| NOT | AND | OR
|
||||
| LPAREN funcArgToken* RPAREN
|
||||
;
|
||||
|
||||
// Identifier references: aliases, field names, result references
|
||||
// Examples: total_logs, error_count, __result, __result_0, __result0, p99
|
||||
identifier
|
||||
: IDENTIFIER
|
||||
;
|
||||
|
||||
/*
|
||||
* Lexer Rules
|
||||
*/
|
||||
|
||||
// Punctuation
|
||||
LPAREN : '(' ;
|
||||
RPAREN : ')' ;
|
||||
LBRACK : '[' ;
|
||||
RBRACK : ']' ;
|
||||
COMMA : ',' ;
|
||||
|
||||
// Comparison operators
|
||||
EQUALS : '=' | '==' ;
|
||||
NOT_EQUALS : '!=' ;
|
||||
NEQ : '<>' ; // alternate not-equals operator
|
||||
LT : '<' ;
|
||||
LE : '<=' ;
|
||||
GT : '>' ;
|
||||
GE : '>=' ;
|
||||
|
||||
// Arithmetic operators
|
||||
PLUS : '+' ;
|
||||
MINUS : '-' ;
|
||||
STAR : '*' ;
|
||||
SLASH : '/' ;
|
||||
PERCENT : '%' ;
|
||||
|
||||
// Boolean logic (case-insensitive)
|
||||
NOT : [Nn][Oo][Tt] ;
|
||||
AND : [Aa][Nn][Dd] ;
|
||||
OR : [Oo][Rr] ;
|
||||
IN : [Ii][Nn] ;
|
||||
|
||||
// Boolean constants (case-insensitive)
|
||||
BOOL
|
||||
: [Tt][Rr][Uu][Ee]
|
||||
| [Ff][Aa][Ll][Ss][Ee]
|
||||
;
|
||||
|
||||
fragment SIGN : [+-] ;
|
||||
|
||||
// Numbers: digits, optional decimal, optional scientific notation.
|
||||
// No leading sign — a leading +/- is always a separate PLUS/MINUS token, which
|
||||
// lets the parser treat it as either a binary operator (count()-10) or unary
|
||||
// sign (-count(), -10). Signed exponents like 1e-3 remain valid.
|
||||
// E.g.: 100, 0.5, 1.5e3, .75
|
||||
NUMBER
|
||||
: DIGIT+ ('.' DIGIT*)? ([eE] SIGN? DIGIT+)?
|
||||
| '.' DIGIT+ ([eE] SIGN? DIGIT+)?
|
||||
;
|
||||
|
||||
// Identifiers: start with a letter or underscore, followed by alphanumeric/underscores.
|
||||
// Optionally dotted for nested field paths.
|
||||
// Covers: count, sum, p99, total_logs, error_count, __result, __result_0, __result0,
|
||||
// service.name, span.duration
|
||||
IDENTIFIER
|
||||
: [a-zA-Z_] [a-zA-Z0-9_]* ( '.' [a-zA-Z_] [a-zA-Z0-9_]* )*
|
||||
;
|
||||
|
||||
// Quoted string literals (single or double-quoted).
|
||||
// These are valid tokens inside function arguments (e.g. countIf(level='error'))
|
||||
// but are always rejected in comparison-operand position by the visitor.
|
||||
STRING
|
||||
: '\'' (~'\'')* '\''
|
||||
| '"' (~'"')* '"'
|
||||
;
|
||||
|
||||
// Skip whitespace
|
||||
WS
|
||||
: [ \t\r\n]+ -> skip
|
||||
;
|
||||
|
||||
fragment DIGIT : [0-9] ;
|
||||
@@ -50,6 +50,11 @@ func (handler *healthOpenAPIHandler) ServeOpenAPI(opCtx openapi.OperationContext
|
||||
)
|
||||
}
|
||||
|
||||
func (handler *healthOpenAPIHandler) AuditDef() *pkghandler.AuditDef {
|
||||
// Health endpoints are not audited since they don't represent user actions and are called frequently by monitoring systems, which would create noise in the audit logs.
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *provider) addRegistryRoutes(router *mux.Router) error {
|
||||
if err := router.Handle("/api/v2/healthz", newHealthOpenAPIHandler(
|
||||
provider.authZ.OpenAccess(provider.factoryHandler.Healthz),
|
||||
|
||||
@@ -21,11 +21,15 @@ func newTestSettings() factory.ScopedProviderSettings {
|
||||
|
||||
func newTestEvent(resource string, action audittypes.Action) audittypes.AuditEvent {
|
||||
return audittypes.AuditEvent{
|
||||
Timestamp: time.Now(),
|
||||
EventName: audittypes.NewEventName(resource, action),
|
||||
ResourceName: resource,
|
||||
Action: action,
|
||||
Outcome: audittypes.OutcomeSuccess,
|
||||
Timestamp: time.Now(),
|
||||
EventName: audittypes.NewEventName(resource, action),
|
||||
AuditAttributes: audittypes.AuditAttributes{
|
||||
Action: action,
|
||||
Outcome: audittypes.OutcomeSuccess,
|
||||
},
|
||||
ResourceAttributes: audittypes.ResourceAttributes{
|
||||
ResourceName: resource,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
parser "github.com/SigNoz/signoz/pkg/parser/grammar"
|
||||
parser "github.com/SigNoz/signoz/pkg/parser/filterquery/grammar"
|
||||
"github.com/antlr4-go/antlr/v4"
|
||||
"golang.org/x/exp/maps"
|
||||
|
||||
|
||||
@@ -20,6 +20,12 @@ var (
|
||||
CodeLicenseUnavailable = Code{"license_unavailable"}
|
||||
)
|
||||
|
||||
var (
|
||||
// Used when reverse engineering an error from a response that doesn't have a code.
|
||||
// This should never be used in the codebase, and if it is, it's a bug that should be fixed by using proper error handling and including error codes in responses.
|
||||
CodeUnset = Code{"unset"}
|
||||
)
|
||||
|
||||
var (
|
||||
codeRegex = regexp.MustCompile(`^[a-z_]+$`)
|
||||
)
|
||||
|
||||
@@ -15,14 +15,16 @@ type ServeOpenAPIFunc func(openapi.OperationContext)
|
||||
type Handler interface {
|
||||
http.Handler
|
||||
ServeOpenAPI(openapi.OperationContext)
|
||||
AuditDef() *AuditDef
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
handlerFunc http.HandlerFunc
|
||||
openAPIDef OpenAPIDef
|
||||
auditDef *AuditDef
|
||||
}
|
||||
|
||||
func New(handlerFunc http.HandlerFunc, openAPIDef OpenAPIDef) Handler {
|
||||
func New(handlerFunc http.HandlerFunc, openAPIDef OpenAPIDef, opts ...Option) Handler {
|
||||
// Remove duplicate error status codes
|
||||
openAPIDef.ErrorStatusCodes = slices.DeleteFunc(openAPIDef.ErrorStatusCodes, func(statusCode int) bool {
|
||||
return statusCode == http.StatusUnauthorized || statusCode == http.StatusForbidden || statusCode == http.StatusInternalServerError
|
||||
@@ -36,10 +38,16 @@ func New(handlerFunc http.HandlerFunc, openAPIDef OpenAPIDef) Handler {
|
||||
openAPIDef.ErrorStatusCodes = append(openAPIDef.ErrorStatusCodes, http.StatusUnauthorized, http.StatusForbidden)
|
||||
}
|
||||
|
||||
return &handler{
|
||||
handler := &handler{
|
||||
handlerFunc: handlerFunc,
|
||||
openAPIDef: openAPIDef,
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(handler)
|
||||
}
|
||||
|
||||
return handler
|
||||
}
|
||||
|
||||
func (handler *handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
@@ -120,5 +128,8 @@ func (handler *handler) ServeOpenAPI(opCtx openapi.OperationContext) {
|
||||
openapi.WithHTTPStatus(statusCode),
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (handler *handler) AuditDef() *AuditDef {
|
||||
return handler.auditDef
|
||||
}
|
||||
|
||||
24
pkg/http/handler/option.go
Normal file
24
pkg/http/handler/option.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/SigNoz/signoz/pkg/types/audittypes"
|
||||
)
|
||||
|
||||
// Option configures optional behaviour on a handler created by New.
|
||||
type Option func(*handler)
|
||||
|
||||
type AuditDef struct {
|
||||
ResourceName string // AuthZ Typeable.Name() value, e.g. "dashboard", "user".
|
||||
Action audittypes.Action // create, update, delete, login, etc.
|
||||
Category audittypes.ActionCategory // access_control, configuration_change, etc.
|
||||
ResourceIDParam string // Gorilla mux path param name for the resource ID.
|
||||
}
|
||||
|
||||
// WithAudit attaches an AuditDef to the handler. The actual audit event
|
||||
// emission is handled by the middleware layer, which reads the AuditDef
|
||||
// from the matched route's handler.
|
||||
func WithAuditDef(def AuditDef) Option {
|
||||
return func(h *handler) {
|
||||
h.auditDef = &def
|
||||
}
|
||||
}
|
||||
169
pkg/http/middleware/audit.go
Normal file
169
pkg/http/middleware/audit.go
Normal file
@@ -0,0 +1,169 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/auditor"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/types/audittypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
)
|
||||
|
||||
const (
|
||||
logMessage = "::RECEIVED-REQUEST::"
|
||||
)
|
||||
|
||||
type Audit struct {
|
||||
logger *slog.Logger
|
||||
excludedRoutes map[string]struct{}
|
||||
auditor auditor.Auditor
|
||||
}
|
||||
|
||||
func NewAudit(logger *slog.Logger, excludedRoutes []string, auditor auditor.Auditor) *Audit {
|
||||
excludedRoutesMap := make(map[string]struct{})
|
||||
for _, route := range excludedRoutes {
|
||||
excludedRoutesMap[route] = struct{}{}
|
||||
}
|
||||
|
||||
return &Audit{
|
||||
logger: logger.With(slog.String("pkg", pkgname)),
|
||||
excludedRoutes: excludedRoutesMap,
|
||||
auditor: auditor,
|
||||
}
|
||||
}
|
||||
|
||||
func (middleware *Audit) Wrap(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
start := time.Now()
|
||||
host, port, _ := net.SplitHostPort(req.Host)
|
||||
path, err := mux.CurrentRoute(req).GetPathTemplate()
|
||||
if err != nil {
|
||||
path = req.URL.Path
|
||||
}
|
||||
|
||||
fields := []any{
|
||||
string(semconv.ClientAddressKey), req.RemoteAddr,
|
||||
string(semconv.UserAgentOriginalKey), req.UserAgent(),
|
||||
string(semconv.ServerAddressKey), host,
|
||||
string(semconv.ServerPortKey), port,
|
||||
string(semconv.HTTPRequestSizeKey), req.ContentLength,
|
||||
string(semconv.HTTPRouteKey), path,
|
||||
}
|
||||
|
||||
responseBuffer := &byteBuffer{}
|
||||
writer := newResponseCapture(rw, responseBuffer)
|
||||
next.ServeHTTP(writer, req)
|
||||
|
||||
statusCode, writeErr := writer.StatusCode(), writer.WriteError()
|
||||
|
||||
// Logging or Audit: skip if the matched route is in the excluded list. This allows us to exclude noisy routes (e.g. health checks) from both logging and audit.
|
||||
if _, ok := middleware.excludedRoutes[path]; ok {
|
||||
return
|
||||
}
|
||||
|
||||
middleware.emitAuditEvent(req, writer, path)
|
||||
|
||||
fields = append(fields,
|
||||
string(semconv.HTTPResponseStatusCodeKey), statusCode,
|
||||
string(semconv.HTTPServerRequestDurationName), time.Since(start),
|
||||
)
|
||||
if writeErr != nil {
|
||||
fields = append(fields, errors.Attr(writeErr))
|
||||
middleware.logger.ErrorContext(req.Context(), logMessage, fields...)
|
||||
} else {
|
||||
if responseBuffer.Len() != 0 {
|
||||
fields = append(fields, "response.body", responseBuffer.String())
|
||||
}
|
||||
|
||||
middleware.logger.InfoContext(req.Context(), logMessage, fields...)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (middleware *Audit) emitAuditEvent(req *http.Request, writer responseCapture, routeTemplate string) {
|
||||
if middleware.auditor == nil {
|
||||
return
|
||||
}
|
||||
|
||||
def := auditDefFromRequest(req)
|
||||
if def == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// extract claims
|
||||
claims, _ := authtypes.ClaimsFromContext(req.Context())
|
||||
|
||||
// extract status code
|
||||
statusCode := writer.StatusCode()
|
||||
|
||||
// extract traces.
|
||||
span := trace.SpanFromContext(req.Context())
|
||||
|
||||
// extract error details.
|
||||
var errorType, errorCode string
|
||||
if statusCode >= 400 {
|
||||
errorType = render.ErrorTypeFromStatusCode(statusCode)
|
||||
errorCode = render.ErrorCodeFromBody(writer.BodyBytes())
|
||||
}
|
||||
|
||||
event := audittypes.NewAuditEventFromHTTPRequest(
|
||||
req,
|
||||
routeTemplate,
|
||||
statusCode,
|
||||
span.SpanContext().TraceID(),
|
||||
span.SpanContext().SpanID(),
|
||||
def.Action,
|
||||
def.Category,
|
||||
claims,
|
||||
resourceIDFromRequest(req, def.ResourceIDParam),
|
||||
def.ResourceName,
|
||||
errorType,
|
||||
errorCode,
|
||||
)
|
||||
|
||||
middleware.auditor.Audit(req.Context(), event)
|
||||
}
|
||||
|
||||
func auditDefFromRequest(req *http.Request) *handler.AuditDef {
|
||||
route := mux.CurrentRoute(req)
|
||||
if route == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
actualHandler := route.GetHandler()
|
||||
if actualHandler == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// The type assertion is necessary because route.GetHandler() returns
|
||||
// http.Handler, and not every http.Handler on the mux is a handler.Handler
|
||||
// (e.g. middleware wrappers, raw http.HandlerFunc registrations).
|
||||
provider, ok := actualHandler.(handler.Handler)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return provider.AuditDef()
|
||||
}
|
||||
|
||||
func resourceIDFromRequest(req *http.Request, param string) string {
|
||||
if param == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
vars := mux.Vars(req)
|
||||
if vars == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return vars[param]
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
logMessage string = "::RECEIVED-REQUEST::"
|
||||
)
|
||||
|
||||
type Logging struct {
|
||||
logger *slog.Logger
|
||||
excludedRoutes map[string]struct{}
|
||||
}
|
||||
|
||||
func NewLogging(logger *slog.Logger, excludedRoutes []string) *Logging {
|
||||
excludedRoutesMap := make(map[string]struct{})
|
||||
for _, route := range excludedRoutes {
|
||||
excludedRoutesMap[route] = struct{}{}
|
||||
}
|
||||
|
||||
return &Logging{
|
||||
logger: logger.With(slog.String("pkg", pkgname)),
|
||||
excludedRoutes: excludedRoutesMap,
|
||||
}
|
||||
}
|
||||
|
||||
func (middleware *Logging) Wrap(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
start := time.Now()
|
||||
host, port, _ := net.SplitHostPort(req.Host)
|
||||
path, err := mux.CurrentRoute(req).GetPathTemplate()
|
||||
if err != nil {
|
||||
path = req.URL.Path
|
||||
}
|
||||
|
||||
fields := []any{
|
||||
string(semconv.ClientAddressKey), req.RemoteAddr,
|
||||
string(semconv.UserAgentOriginalKey), req.UserAgent(),
|
||||
string(semconv.ServerAddressKey), host,
|
||||
string(semconv.ServerPortKey), port,
|
||||
string(semconv.HTTPRequestSizeKey), req.ContentLength,
|
||||
string(semconv.HTTPRouteKey), path,
|
||||
}
|
||||
|
||||
badResponseBuffer := new(bytes.Buffer)
|
||||
writer := newBadResponseLoggingWriter(rw, badResponseBuffer)
|
||||
next.ServeHTTP(writer, req)
|
||||
|
||||
// if the path is in the excludedRoutes map, don't log
|
||||
if _, ok := middleware.excludedRoutes[path]; ok {
|
||||
return
|
||||
}
|
||||
|
||||
statusCode, err := writer.StatusCode(), writer.WriteError()
|
||||
fields = append(fields,
|
||||
string(semconv.HTTPResponseStatusCodeKey), statusCode,
|
||||
string(semconv.HTTPServerRequestDurationName), time.Since(start),
|
||||
)
|
||||
if err != nil {
|
||||
fields = append(fields, errors.Attr(err))
|
||||
middleware.logger.ErrorContext(req.Context(), logMessage, fields...)
|
||||
} else {
|
||||
// when the status code is 400 or >=500, and the response body is not empty.
|
||||
if badResponseBuffer.Len() != 0 {
|
||||
fields = append(fields, "response.body", badResponseBuffer.String())
|
||||
}
|
||||
|
||||
middleware.logger.InfoContext(req.Context(), logMessage, fields...)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package middleware
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
@@ -10,118 +9,156 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
maxResponseBodyInLogs = 4096 // At most 4k bytes from response bodies in our logs.
|
||||
maxResponseBodyCapture int = 4096 // At most 4k bytes from response bodies.
|
||||
)
|
||||
|
||||
type badResponseLoggingWriter interface {
|
||||
// Wraps an http.ResponseWriter to capture the status code,
|
||||
// write errors, and (for error responses) a bounded slice of the body.
|
||||
type responseCapture interface {
|
||||
http.ResponseWriter
|
||||
// Get the status code.
|
||||
|
||||
// StatusCode returns the HTTP status code written to the response.
|
||||
StatusCode() int
|
||||
// Get the error while writing.
|
||||
|
||||
// WriteError returns the error (if any) from the downstream Write call.
|
||||
WriteError() error
|
||||
|
||||
// BodyBytes returns the captured response body bytes. Only populated
|
||||
// for error responses (status >= 400).
|
||||
BodyBytes() []byte
|
||||
}
|
||||
|
||||
func newBadResponseLoggingWriter(rw http.ResponseWriter, buffer io.Writer) badResponseLoggingWriter {
|
||||
b := nonFlushingBadResponseLoggingWriter{
|
||||
func newResponseCapture(rw http.ResponseWriter, buffer *byteBuffer) responseCapture {
|
||||
b := nonFlushingResponseCapture{
|
||||
rw: rw,
|
||||
buffer: buffer,
|
||||
logBody: false,
|
||||
bodyBytesLeft: maxResponseBodyInLogs,
|
||||
captureBody: false,
|
||||
bodyBytesLeft: maxResponseBodyCapture,
|
||||
statusCode: http.StatusOK,
|
||||
}
|
||||
|
||||
if f, ok := rw.(http.Flusher); ok {
|
||||
return &flushingBadResponseLoggingWriter{b, f}
|
||||
return &flushingResponseCapture{nonFlushingResponseCapture: b, f: f}
|
||||
}
|
||||
|
||||
return &b
|
||||
}
|
||||
|
||||
type nonFlushingBadResponseLoggingWriter struct {
|
||||
rw http.ResponseWriter
|
||||
buffer io.Writer
|
||||
logBody bool
|
||||
bodyBytesLeft int
|
||||
statusCode int
|
||||
writeError error // The error returned when downstream Write() fails.
|
||||
// byteBuffer is a minimal write-only buffer used to capture response bodies.
|
||||
type byteBuffer struct {
|
||||
buf []byte
|
||||
}
|
||||
|
||||
// Extends nonFlushingBadResponseLoggingWriter that implements http.Flusher.
|
||||
type flushingBadResponseLoggingWriter struct {
|
||||
nonFlushingBadResponseLoggingWriter
|
||||
func (b *byteBuffer) Write(p []byte) (int, error) {
|
||||
b.buf = append(b.buf, p...)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (b *byteBuffer) WriteString(s string) (int, error) {
|
||||
b.buf = append(b.buf, s...)
|
||||
return len(s), nil
|
||||
}
|
||||
|
||||
func (b *byteBuffer) Bytes() []byte {
|
||||
return b.buf
|
||||
}
|
||||
|
||||
func (b *byteBuffer) Len() int {
|
||||
return len(b.buf)
|
||||
}
|
||||
|
||||
func (b *byteBuffer) String() string {
|
||||
return string(b.buf)
|
||||
}
|
||||
|
||||
type nonFlushingResponseCapture struct {
|
||||
rw http.ResponseWriter
|
||||
buffer *byteBuffer
|
||||
captureBody bool
|
||||
bodyBytesLeft int
|
||||
statusCode int
|
||||
writeError error
|
||||
}
|
||||
|
||||
type flushingResponseCapture struct {
|
||||
nonFlushingResponseCapture
|
||||
f http.Flusher
|
||||
}
|
||||
|
||||
// Unwrap method is used by http.ResponseController to get access to original http.ResponseWriter.
|
||||
func (writer *nonFlushingBadResponseLoggingWriter) Unwrap() http.ResponseWriter {
|
||||
// Unwrap is used by http.ResponseController to get access to original http.ResponseWriter.
|
||||
func (writer *nonFlushingResponseCapture) Unwrap() http.ResponseWriter {
|
||||
return writer.rw
|
||||
}
|
||||
|
||||
// Header returns the header map that will be sent by WriteHeader.
|
||||
// Implements ResponseWriter.
|
||||
func (writer *nonFlushingBadResponseLoggingWriter) Header() http.Header {
|
||||
func (writer *nonFlushingResponseCapture) Header() http.Header {
|
||||
return writer.rw.Header()
|
||||
}
|
||||
|
||||
// WriteHeader writes the HTTP response header.
|
||||
func (writer *nonFlushingBadResponseLoggingWriter) WriteHeader(statusCode int) {
|
||||
func (writer *nonFlushingResponseCapture) WriteHeader(statusCode int) {
|
||||
writer.statusCode = statusCode
|
||||
if statusCode >= 500 || statusCode == 400 {
|
||||
writer.logBody = true
|
||||
if statusCode >= 400 {
|
||||
writer.captureBody = true
|
||||
}
|
||||
|
||||
writer.rw.WriteHeader(statusCode)
|
||||
}
|
||||
|
||||
// Writes HTTP response data.
|
||||
func (writer *nonFlushingBadResponseLoggingWriter) Write(data []byte) (int, error) {
|
||||
// Write writes HTTP response data.
|
||||
func (writer *nonFlushingResponseCapture) Write(data []byte) (int, error) {
|
||||
if writer.statusCode == 0 {
|
||||
// WriteHeader has (probably) not been called, so we need to call it with StatusOK to fulfill the interface contract.
|
||||
// https://godoc.org/net/http#ResponseWriter
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
// 204 No Content is a success response that indicates that the request has been successfully processed and that the response body is intentionally empty.
|
||||
if writer.statusCode == 204 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
n, err := writer.rw.Write(data)
|
||||
if writer.logBody {
|
||||
if writer.captureBody {
|
||||
writer.captureResponseBody(data)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
writer.writeError = err
|
||||
}
|
||||
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Hijack hijacks the first response writer that is a Hijacker.
|
||||
func (writer *nonFlushingBadResponseLoggingWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
func (writer *nonFlushingResponseCapture) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
hj, ok := writer.rw.(http.Hijacker)
|
||||
if ok {
|
||||
return hj.Hijack()
|
||||
}
|
||||
|
||||
return nil, nil, errors.NewInternalf(errors.CodeInternal, "cannot cast underlying response writer to Hijacker")
|
||||
}
|
||||
|
||||
func (writer *nonFlushingBadResponseLoggingWriter) StatusCode() int {
|
||||
func (writer *nonFlushingResponseCapture) StatusCode() int {
|
||||
return writer.statusCode
|
||||
}
|
||||
|
||||
func (writer *nonFlushingBadResponseLoggingWriter) WriteError() error {
|
||||
func (writer *nonFlushingResponseCapture) WriteError() error {
|
||||
return writer.writeError
|
||||
}
|
||||
|
||||
func (writer *flushingBadResponseLoggingWriter) Flush() {
|
||||
func (writer *nonFlushingResponseCapture) BodyBytes() []byte {
|
||||
return writer.buffer.Bytes()
|
||||
}
|
||||
|
||||
func (writer *flushingResponseCapture) Flush() {
|
||||
writer.f.Flush()
|
||||
}
|
||||
|
||||
func (writer *nonFlushingBadResponseLoggingWriter) captureResponseBody(data []byte) {
|
||||
func (writer *nonFlushingResponseCapture) captureResponseBody(data []byte) {
|
||||
if len(data) > writer.bodyBytesLeft {
|
||||
_, _ = writer.buffer.Write(data[:writer.bodyBytesLeft])
|
||||
_, _ = io.WriteString(writer.buffer, "...")
|
||||
_, _ = writer.buffer.WriteString("...")
|
||||
writer.bodyBytesLeft = 0
|
||||
writer.logBody = false
|
||||
writer.captureBody = false
|
||||
} else {
|
||||
_, _ = writer.buffer.Write(data)
|
||||
writer.bodyBytesLeft -= len(data)
|
||||
|
||||
88
pkg/http/middleware/response_test.go
Normal file
88
pkg/http/middleware/response_test.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestResponseCapture(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
handler http.HandlerFunc
|
||||
expectedStatus int
|
||||
expectedBodyBytes string
|
||||
expectedClientBody string
|
||||
}{
|
||||
{
|
||||
name: "Success_DoesNotCaptureBody",
|
||||
handler: func(rw http.ResponseWriter, req *http.Request) {
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
_, _ = rw.Write([]byte(`{"status":"success","data":{"id":"123"}}`))
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBodyBytes: "",
|
||||
expectedClientBody: `{"status":"success","data":{"id":"123"}}`,
|
||||
},
|
||||
{
|
||||
name: "Error_CapturesBody",
|
||||
handler: func(rw http.ResponseWriter, req *http.Request) {
|
||||
rw.WriteHeader(http.StatusForbidden)
|
||||
_, _ = rw.Write([]byte(`{"status":"error","error":{"code":"authz_forbidden","message":"forbidden"}}`))
|
||||
},
|
||||
expectedStatus: http.StatusForbidden,
|
||||
expectedBodyBytes: `{"status":"error","error":{"code":"authz_forbidden","message":"forbidden"}}`,
|
||||
expectedClientBody: `{"status":"error","error":{"code":"authz_forbidden","message":"forbidden"}}`,
|
||||
},
|
||||
{
|
||||
name: "Error_TruncatesAtMaxCapture",
|
||||
handler: func(rw http.ResponseWriter, req *http.Request) {
|
||||
rw.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = rw.Write([]byte(strings.Repeat("x", maxResponseBodyCapture+100)))
|
||||
},
|
||||
expectedStatus: http.StatusInternalServerError,
|
||||
expectedBodyBytes: strings.Repeat("x", maxResponseBodyCapture) + "...",
|
||||
expectedClientBody: strings.Repeat("x", maxResponseBodyCapture+100),
|
||||
},
|
||||
{
|
||||
name: "NoContent_SuppressesWrite",
|
||||
handler: func(rw http.ResponseWriter, req *http.Request) {
|
||||
rw.WriteHeader(http.StatusNoContent)
|
||||
_, _ = rw.Write([]byte("should be suppressed"))
|
||||
},
|
||||
expectedStatus: http.StatusNoContent,
|
||||
expectedBodyBytes: "",
|
||||
expectedClientBody: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var captured responseCapture
|
||||
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
buf := &byteBuffer{}
|
||||
captured = newResponseCapture(rw, buf)
|
||||
testCase.handler(captured, req)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
resp, err := http.Get(server.URL)
|
||||
assert.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
clientBody, _ := io.ReadAll(resp.Body)
|
||||
|
||||
assert.Equal(t, testCase.expectedStatus, captured.StatusCode())
|
||||
assert.Equal(t, testCase.expectedBodyBytes, string(captured.BodyBytes()))
|
||||
assert.Equal(t, testCase.expectedClientBody, string(clientBody))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -42,6 +43,45 @@ func Success(rw http.ResponseWriter, httpCode int, data interface{}) {
|
||||
_, _ = rw.Write(body)
|
||||
}
|
||||
|
||||
func ErrorCodeFromBody(body []byte) string {
|
||||
code := gjson.GetBytes(body, "error.code").String()
|
||||
|
||||
// This should never return empty since we only call this function on responses that were generated by us.
|
||||
// If it does return empty, the codebase has failed to use render package for error responses somewhere, and we should fix that instead of trying to handle it here.
|
||||
if code == "" {
|
||||
return errors.CodeUnset.String()
|
||||
}
|
||||
|
||||
return code
|
||||
}
|
||||
|
||||
func ErrorTypeFromStatusCode(statusCode int) string {
|
||||
// We are losing the exact type information here, but we can at least capture the error code and message for better observability.
|
||||
// To get the exact type, we would need some changes in the render package to include the error type in the response, which we can consider in the future if there is a need for it.
|
||||
switch statusCode {
|
||||
case http.StatusBadRequest:
|
||||
return errors.TypeInvalidInput.String()
|
||||
case http.StatusNotFound:
|
||||
return errors.TypeNotFound.String()
|
||||
case http.StatusConflict:
|
||||
return errors.TypeAlreadyExists.String()
|
||||
case http.StatusUnauthorized:
|
||||
return errors.TypeUnauthenticated.String()
|
||||
case http.StatusNotImplemented:
|
||||
return errors.TypeUnsupported.String()
|
||||
case http.StatusForbidden:
|
||||
return errors.TypeForbidden.String()
|
||||
case statusClientClosedConnection:
|
||||
return errors.TypeCanceled.String()
|
||||
case http.StatusGatewayTimeout:
|
||||
return errors.TypeTimeout.String()
|
||||
case http.StatusUnavailableForLegalReasons:
|
||||
return errors.TypeLicenseUnavailable.String()
|
||||
default:
|
||||
return errors.TypeInternal.String()
|
||||
}
|
||||
}
|
||||
|
||||
func Error(rw http.ResponseWriter, cause error) {
|
||||
// Derive the http code from the error type
|
||||
t, _, _, _, _, _ := errors.Unwrapb(cause)
|
||||
|
||||
@@ -58,6 +58,31 @@ func TestSuccess(t *testing.T) {
|
||||
assert.Equal(t, expected, actual)
|
||||
}
|
||||
|
||||
func TestErrorCodeFromBody(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
body []byte
|
||||
wantCode string
|
||||
}{
|
||||
{
|
||||
name: "ValidErrorResponse",
|
||||
body: []byte(`{"status":"error","error":{"code":"authz_forbidden","message":"only admins can access this resource"}}`),
|
||||
wantCode: "authz_forbidden",
|
||||
},
|
||||
{
|
||||
name: "InvalidJSON",
|
||||
body: []byte(`not json`),
|
||||
wantCode: "unset",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
assert.Equal(t, testCase.wantCode, ErrorCodeFromBody(testCase.body))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestError(t *testing.T) {
|
||||
listener, err := net.Listen("tcp", "localhost:0")
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package implcloudintegration
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
citypes "github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
|
||||
)
|
||||
|
||||
type definitionStore struct{}
|
||||
|
||||
func NewDefinitionStore() citypes.ServiceDefinitionStore {
|
||||
return &definitionStore{}
|
||||
}
|
||||
|
||||
func (d *definitionStore) Get(ctx context.Context, provider citypes.CloudProviderType, serviceID citypes.ServiceID) (*citypes.ServiceDefinition, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
func (d *definitionStore) List(ctx context.Context, provider citypes.CloudProviderType) ([]*citypes.ServiceDefinition, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 85 85" fill="#fff" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round"><use xlink:href="#A" x="2.5" y="2.5"/><symbol id="A" overflow="visible"><g stroke="none"><path d="M0 41.579C0 20.293 17.84 3.157 40 3.157s40 17.136 40 38.422S62.16 80 40 80 0 62.864 0 41.579z" fill="#9d5025"/><path d="M0 38.422C0 17.136 17.84 0 40 0s40 17.136 40 38.422-17.84 38.422-40 38.422S0 59.707 0 38.422z" fill="#f58536"/><path d="M51.672 7.387v13.952H28.327V7.387zm18.061 40.378v11.364h-11.83V47.765zm-14.958 0v11.364h-11.83V47.765zm-18.206 0v11.364h-11.83V47.765zm-14.959 0v11.364H9.78V47.765z"/><path d="M14.63 37.929h2.13v11.149h-2.13z"/><path d="M14.63 37.929h17.088v2.045H14.63z"/><path d="M29.589 37.929h2.13v11.149H29.59zm18.206 0h2.13v11.149h-2.13z"/><path d="M47.795 37.929h17.088v2.045H47.795z"/><path d="M62.754 37.929h2.13v11.149h-2.129zm-40.631-7.954h2.13v8.977h-2.13zM38.935 19.28h2.13v10.859h-2.129z"/><path d="M22.123 29.116h35.32v2.045h-35.32z"/><path d="M55.314 29.975h2.13v8.977h-2.129z"/></g></symbol></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,468 @@
|
||||
{
|
||||
"id": "alb",
|
||||
"title": "ALB",
|
||||
"icon": "file://icon.svg",
|
||||
"overview": "file://overview.md",
|
||||
"supportedSignals": {
|
||||
"metrics": true,
|
||||
"logs": false
|
||||
},
|
||||
"dataCollected": {
|
||||
"metrics": [
|
||||
{
|
||||
"name": "aws_ApplicationELB_ActiveConnectionCount_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_ActiveConnectionCount_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_ActiveConnectionCount_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_ActiveConnectionCount_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_AnomalousHostCount_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_AnomalousHostCount_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_AnomalousHostCount_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_AnomalousHostCount_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_ConsumedLCUs_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_ConsumedLCUs_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_ConsumedLCUs_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_ConsumedLCUs_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_HTTPCode_Target_2XX_Count_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_HTTPCode_Target_2XX_Count_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_HTTPCode_Target_2XX_Count_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_HTTPCode_Target_2XX_Count_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_HTTPCode_Target_4XX_Count_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_HTTPCode_Target_4XX_Count_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_HTTPCode_Target_4XX_Count_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_HTTPCode_Target_4XX_Count_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_HealthyHostCount_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_HealthyHostCount_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_HealthyHostCount_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_HealthyHostCount_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_HealthyStateDNS_count",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_HealthyStateDNS_max",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_HealthyStateDNS_min",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_HealthyStateDNS_sum",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_HealthyStateRouting_count",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_HealthyStateRouting_max",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_HealthyStateRouting_min",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_HealthyStateRouting_sum",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_MitigatedHostCount_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_MitigatedHostCount_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_MitigatedHostCount_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_MitigatedHostCount_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_NewConnectionCount_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_NewConnectionCount_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_NewConnectionCount_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_NewConnectionCount_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_PeakLCUs_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_PeakLCUs_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_PeakLCUs_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_PeakLCUs_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_ProcessedBytes_count",
|
||||
"unit": "Bytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_ProcessedBytes_max",
|
||||
"unit": "Bytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_ProcessedBytes_min",
|
||||
"unit": "Bytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_ProcessedBytes_sum",
|
||||
"unit": "Bytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_RequestCountPerTarget_count",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_RequestCountPerTarget_max",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_RequestCountPerTarget_min",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_RequestCountPerTarget_sum",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_RequestCount_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_RequestCount_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_RequestCount_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_RequestCount_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_TargetResponseTime_count",
|
||||
"unit": "Seconds",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_TargetResponseTime_max",
|
||||
"unit": "Seconds",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_TargetResponseTime_min",
|
||||
"unit": "Seconds",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_TargetResponseTime_sum",
|
||||
"unit": "Seconds",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_UnHealthyHostCount_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_UnHealthyHostCount_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_UnHealthyHostCount_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_UnHealthyHostCount_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_UnhealthyStateDNS_count",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_UnhealthyStateDNS_max",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_UnhealthyStateDNS_min",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_UnhealthyStateDNS_sum",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_UnhealthyStateRouting_count",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_UnhealthyStateRouting_max",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_UnhealthyStateRouting_min",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_UnhealthyStateRouting_sum",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
}
|
||||
],
|
||||
"logs": []
|
||||
},
|
||||
"telemetryCollectionStrategy": {
|
||||
"aws": {
|
||||
"metrics": {
|
||||
"cloudwatchMetricStreamFilters": [
|
||||
{
|
||||
"Namespace": "AWS/ApplicationELB"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"assets": {
|
||||
"dashboards": [
|
||||
{
|
||||
"id": "overview",
|
||||
"title": "ALB Overview",
|
||||
"description": "Overview of Application Load Balancer",
|
||||
"definition": "file://assets/dashboards/overview.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
### Monitor Application Load Balancers with SigNoz
|
||||
|
||||
Collect key ALB metrics and view them with an out of the box dashboard.
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,14 @@
|
||||
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs>
|
||||
<linearGradient x1="0%" y1="100%" x2="100%" y2="0%" id="linearGradient-1">
|
||||
<stop stop-color="#4D27A8" offset="0%"></stop>
|
||||
<stop stop-color="#A166FF" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g fill="url(#linearGradient-1)">
|
||||
<rect id="Rectangle" x="0" y="0" width="24" height="24"></rect>
|
||||
</g>
|
||||
<path d="M6,6.76751613 L8,5.43446738 L8,18.5659476 L6,17.2328988 L6,6.76751613 Z M5,6.49950633 L5,17.4999086 C5,17.6669147 5.084,17.8239204 5.223,17.9159238 L8.223,19.9159969 C8.307,19.971999 8.403,20 8.5,20 C8.581,20 8.662,19.9809993 8.736,19.9409978 C8.898,19.8539947 9,19.6849885 9,19.4999817 L9,16.9998903 L10,16.9998903 L10,15.9998537 L9,15.9998537 L9,7.99956118 L10,7.99956118 L10,6.99952461 L9,6.99952461 L9,4.49943319 C9,4.31542646 8.898,4.14542025 8.736,4.0594171 C8.574,3.97241392 8.377,3.98141425 8.223,4.08341798 L5.223,6.08349112 C5.084,6.17649452 5,6.33250022 5,6.49950633 L5,6.49950633 Z M19,17.2328988 L17,18.5659476 L17,5.43446738 L19,6.76751613 L19,17.2328988 Z M19.777,6.08349112 L16.777,4.08341798 C16.623,3.98141425 16.426,3.97241392 16.264,4.0594171 C16.102,4.14542025 16,4.31542646 16,4.49943319 L16,6.99952461 L15,6.99952461 L15,7.99956118 L16,7.99956118 L16,15.9998537 L15,15.9998537 L15,16.9998903 L16,16.9998903 L16,19.4999817 C16,19.6849885 16.102,19.8539947 16.264,19.9409978 C16.338,19.9809993 16.419,20 16.5,20 C16.597,20 16.693,19.971999 16.777,19.9159969 L19.777,17.9159238 C19.916,17.8239204 20,17.6669147 20,17.4999086 L20,6.49950633 C20,6.33250022 19.916,6.17649452 19.777,6.08349112 L19.777,6.08349112 Z M13,7.99956118 L14,7.99956118 L14,6.99952461 L13,6.99952461 L13,7.99956118 Z M11,7.99956118 L12,7.99956118 L12,6.99952461 L11,6.99952461 L11,7.99956118 Z M13,16.9998903 L14,16.9998903 L14,15.9998537 L13,15.9998537 L13,16.9998903 Z M11,16.9998903 L12,16.9998903 L12,15.9998537 L11,15.9998537 L11,16.9998903 Z M13.18,14.884813 L10.18,12.3847215 C10.065,12.288718 10,12.1487129 10,11.9997075 C10,11.851702 10.065,11.7106969 10.18,11.6156934 L13.18,9.11560199 L13.82,9.88463011 L11.281,11.9997075 L13.82,14.1157848 L13.18,14.884813 Z" id="Amazon-API-Gateway_Icon_16_Squid" fill="#FFFFFF"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -0,0 +1,200 @@
|
||||
{
|
||||
"id": "api-gateway",
|
||||
"title": "API Gateway",
|
||||
"icon": "file://icon.svg",
|
||||
"overview": "file://overview.md",
|
||||
"supportedSignals": {
|
||||
"metrics": true,
|
||||
"logs": true
|
||||
},
|
||||
"dataCollected": {
|
||||
"metrics": [
|
||||
{
|
||||
"name": "aws_ApiGateway_4XXError_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_4XXError_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_4XXError_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_4XXError_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_5XXError_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_5XXError_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_5XXError_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_5XXError_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_CacheHitCount_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_CacheHitCount_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_CacheHitCount_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_CacheHitCount_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_CacheMissCount_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_CacheMissCount_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_CacheMissCount_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_CacheMissCount_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_Count_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_Count_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_Count_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_Count_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_IntegrationLatency_count",
|
||||
"unit": "Milliseconds",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_IntegrationLatency_max",
|
||||
"unit": "Milliseconds",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_IntegrationLatency_min",
|
||||
"unit": "Milliseconds",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_IntegrationLatency_sum",
|
||||
"unit": "Milliseconds",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_Latency_count",
|
||||
"unit": "Milliseconds",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_Latency_max",
|
||||
"unit": "Milliseconds",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_Latency_min",
|
||||
"unit": "Milliseconds",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_Latency_sum",
|
||||
"unit": "Milliseconds",
|
||||
"type": "Gauge"
|
||||
}
|
||||
],
|
||||
"logs": [
|
||||
{
|
||||
"name": "Account Id",
|
||||
"path": "resources.cloud.account.id",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "Log Group Name",
|
||||
"path": "resources.aws.cloudwatch.log_group_name",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "Log Stream Name",
|
||||
"path": "resources.aws.cloudwatch.log_stream_name",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"telemetryCollectionStrategy": {
|
||||
"aws": {
|
||||
"metrics": {
|
||||
"cloudwatchMetricStreamFilters": [
|
||||
{
|
||||
"Namespace": "AWS/ApiGateway"
|
||||
}
|
||||
]
|
||||
},
|
||||
"logs": {
|
||||
"cloudwatchLogsSubscriptions": [
|
||||
{
|
||||
"logGroupNamePrefix": "API-Gateway",
|
||||
"filterPattern": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"assets": {
|
||||
"dashboards": [
|
||||
{
|
||||
"id": "overview",
|
||||
"title": "API Gateway Overview",
|
||||
"description": "Overview of API Gateway",
|
||||
"definition": "file://assets/dashboards/overview.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
### Monitor API Gateway with SigNoz
|
||||
|
||||
Collect key API Gateway metrics and view them with an out of the box dashboard.
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 6.2 KiB |
@@ -0,0 +1,395 @@
|
||||
{
|
||||
"id": "dynamodb",
|
||||
"title": "DynamoDB",
|
||||
"icon": "file://icon.svg",
|
||||
"overview": "file://overview.md",
|
||||
"supportedSignals": {
|
||||
"metrics": true,
|
||||
"logs": false
|
||||
},
|
||||
"dataCollected": {
|
||||
"metrics": [
|
||||
{
|
||||
"name": "aws_DynamoDB_AccountMaxReads_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_AccountMaxReads_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_AccountMaxReads_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_AccountMaxReads_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_AccountMaxTableLevelReads_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_AccountMaxTableLevelReads_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_AccountMaxTableLevelReads_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_AccountMaxTableLevelReads_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_AccountMaxTableLevelWrites_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_AccountMaxTableLevelWrites_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_AccountMaxTableLevelWrites_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_AccountMaxTableLevelWrites_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_AccountMaxWrites_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_AccountMaxWrites_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_AccountMaxWrites_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_AccountMaxWrites_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_AccountProvisionedReadCapacityUtilization_count",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_AccountProvisionedReadCapacityUtilization_max",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_AccountProvisionedReadCapacityUtilization_min",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_AccountProvisionedReadCapacityUtilization_sum",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_AccountProvisionedWriteCapacityUtilization_count",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_AccountProvisionedWriteCapacityUtilization_max",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_AccountProvisionedWriteCapacityUtilization_min",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_AccountProvisionedWriteCapacityUtilization_sum",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_ConsumedReadCapacityUnits_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_ConsumedReadCapacityUnits_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_ConsumedReadCapacityUnits_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_ConsumedReadCapacityUnits_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_ConsumedWriteCapacityUnits_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_ConsumedWriteCapacityUnits_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_ConsumedWriteCapacityUnits_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_ConsumedWriteCapacityUnits_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_MaxProvisionedTableReadCapacityUtilization_count",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_MaxProvisionedTableReadCapacityUtilization_max",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_MaxProvisionedTableReadCapacityUtilization_min",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_MaxProvisionedTableReadCapacityUtilization_sum",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_MaxProvisionedTableWriteCapacityUtilization_count",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_MaxProvisionedTableWriteCapacityUtilization_max",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_MaxProvisionedTableWriteCapacityUtilization_min",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_MaxProvisionedTableWriteCapacityUtilization_sum",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_ReturnedItemCount_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_ReturnedItemCount_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_ReturnedItemCount_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_ReturnedItemCount_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_SuccessfulRequestLatency_count",
|
||||
"unit": "Milliseconds",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_SuccessfulRequestLatency_max",
|
||||
"unit": "Milliseconds",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_SuccessfulRequestLatency_min",
|
||||
"unit": "Milliseconds",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_SuccessfulRequestLatency_sum",
|
||||
"unit": "Milliseconds",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_ThrottledRequests_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_ThrottledRequests_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_ThrottledRequests_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_ThrottledRequests_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_UserErrors_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_UserErrors_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_UserErrors_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_UserErrors_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_WriteThrottleEvents_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_WriteThrottleEvents_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_WriteThrottleEvents_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_DynamoDB_WriteThrottleEvents_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
"telemetryCollectionStrategy": {
|
||||
"aws": {
|
||||
"metrics": {
|
||||
"cloudwatchMetricStreamFilters": [
|
||||
{
|
||||
"Namespace": "AWS/DynamoDB"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"assets": {
|
||||
"dashboards": [
|
||||
{
|
||||
"id": "overview",
|
||||
"title": "DynamoDB Overview",
|
||||
"description": "Overview of DynamoDB",
|
||||
"definition": "file://assets/dashboards/overview.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
### Monitor DynamoDB with SigNoz
|
||||
|
||||
Collect DynamoDB Key Metrics and view them with an out of the box dashboard.
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none">
|
||||
<path fill="#9D5025" d="M1.702 2.98L1 3.312v9.376l.702.332 2.842-4.777L1.702 2.98z" />
|
||||
<path fill="#F58536" d="M3.339 12.657l-1.637.363V2.98l1.637.353v9.324z" />
|
||||
<path fill="#9D5025" d="M2.476 2.612l.863-.406 4.096 6.216-4.096 5.372-.863-.406V2.612z" />
|
||||
<path fill="#F58536" d="M5.38 13.248l-2.041.546V2.206l2.04.548v10.494z" />
|
||||
<path fill="#9D5025" d="M4.3 1.75l1.08-.512 6.043 7.864-6.043 5.66-1.08-.511V1.749z" />
|
||||
<path fill="#F58536" d="M7.998 13.856l-2.618.906V1.238l2.618.908v11.71z" />
|
||||
<path fill="#9D5025" d="M6.602.66L7.998 0l6.538 8.453L7.998 16l-1.396-.66V.66z" />
|
||||
<path fill="#F58536" d="M15 12.686L7.998 16V0L15 3.314v9.372z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 805 B |
@@ -0,0 +1,519 @@
|
||||
{
|
||||
"id": "ec2",
|
||||
"title": "EC2",
|
||||
"icon": "file://icon.svg",
|
||||
"overview": "file://overview.md",
|
||||
"supportedSignals": {
|
||||
"metrics": true,
|
||||
"logs": false
|
||||
},
|
||||
"dataCollected": {
|
||||
"metrics": [
|
||||
{
|
||||
"name": "aws_EC2_CPUCreditBalance_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_CPUCreditBalance_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_CPUCreditBalance_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_CPUCreditBalance_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_CPUCreditUsage_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_CPUCreditUsage_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_CPUCreditUsage_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_CPUCreditUsage_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_CPUSurplusCreditBalance_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_CPUSurplusCreditBalance_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_CPUSurplusCreditBalance_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_CPUSurplusCreditBalance_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_CPUSurplusCreditsCharged_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_CPUSurplusCreditsCharged_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_CPUSurplusCreditsCharged_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_CPUSurplusCreditsCharged_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_CPUUtilization_count",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_CPUUtilization_max",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_CPUUtilization_min",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_CPUUtilization_sum",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_EBSByteBalance__count",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_EBSByteBalance__max",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_EBSByteBalance__min",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_EBSByteBalance__sum",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_EBSIOBalance__count",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_EBSIOBalance__max",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_EBSIOBalance__min",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_EBSIOBalance__sum",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_EBSReadBytes_count",
|
||||
"unit": "Bytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_EBSReadBytes_max",
|
||||
"unit": "Bytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_EBSReadBytes_min",
|
||||
"unit": "Bytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_EBSReadBytes_sum",
|
||||
"unit": "Bytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_EBSReadOps_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_EBSReadOps_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_EBSReadOps_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_EBSReadOps_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_EBSWriteBytes_count",
|
||||
"unit": "Bytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_EBSWriteBytes_max",
|
||||
"unit": "Bytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_EBSWriteBytes_min",
|
||||
"unit": "Bytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_EBSWriteBytes_sum",
|
||||
"unit": "Bytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_EBSWriteOps_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_EBSWriteOps_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_EBSWriteOps_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_EBSWriteOps_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_MetadataNoToken_count",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_MetadataNoToken_max",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_MetadataNoToken_min",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_MetadataNoToken_sum",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_NetworkIn_count",
|
||||
"unit": "Bytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_NetworkIn_max",
|
||||
"unit": "Bytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_NetworkIn_min",
|
||||
"unit": "Bytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_NetworkIn_sum",
|
||||
"unit": "Bytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_NetworkOut_count",
|
||||
"unit": "Bytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_NetworkOut_max",
|
||||
"unit": "Bytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_NetworkOut_min",
|
||||
"unit": "Bytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_NetworkOut_sum",
|
||||
"unit": "Bytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_NetworkPacketsIn_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_NetworkPacketsIn_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_NetworkPacketsIn_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_NetworkPacketsIn_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_NetworkPacketsOut_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_NetworkPacketsOut_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_NetworkPacketsOut_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_NetworkPacketsOut_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_StatusCheckFailed_AttachedEBS_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_StatusCheckFailed_AttachedEBS_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_StatusCheckFailed_AttachedEBS_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_StatusCheckFailed_AttachedEBS_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_StatusCheckFailed_Instance_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_StatusCheckFailed_Instance_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_StatusCheckFailed_Instance_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_StatusCheckFailed_Instance_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_StatusCheckFailed_System_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_StatusCheckFailed_System_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_StatusCheckFailed_System_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_StatusCheckFailed_System_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_StatusCheckFailed_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_StatusCheckFailed_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_StatusCheckFailed_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_EC2_StatusCheckFailed_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
}
|
||||
],
|
||||
"logs": []
|
||||
},
|
||||
"telemetryCollectionStrategy": {
|
||||
"aws": {
|
||||
"metrics": {
|
||||
"cloudwatchMetricStreamFilters": [
|
||||
{
|
||||
"Namespace": "AWS/EC2"
|
||||
},
|
||||
{
|
||||
"Namespace": "CWAgent"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"assets": {
|
||||
"dashboards": [
|
||||
{
|
||||
"id": "overview",
|
||||
"title": "EC2 Overview",
|
||||
"description": "Overview of EC2",
|
||||
"definition": "file://assets/dashboards/overview.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
### Monitor EC2 with SigNoz
|
||||
|
||||
Collect key EC2 metrics and view them with an out of the box dashboard.
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,851 @@
|
||||
{
|
||||
"description": "View key AWS ECS metrics with an out of the box dashboard.\n",
|
||||
"image":"data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22UTF-8%22%3F%3E%3Csvg%20width%3D%2280px%22%20height%3D%2280px%22%20viewBox%3D%220%200%2080%2080%22%20version%3D%221.1%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%3E%3C!--%20Generator%3A%20Sketch%2064%20(93537)%20-%20https%3A%2F%2Fsketch.com%20--%3E%3Ctitle%3EIcon-Architecture%2F64%2FArch_Amazon-Elastic-Container-Service_64%3C%2Ftitle%3E%3Cdesc%3ECreated%20with%20Sketch.%3C%2Fdesc%3E%3Cdefs%3E%3ClinearGradient%20x1%3D%220%25%22%20y1%3D%22100%25%22%20x2%3D%22100%25%22%20y2%3D%220%25%22%20id%3D%22linearGradient-1%22%3E%3Cstop%20stop-color%3D%22%23C8511B%22%20offset%3D%220%25%22%3E%3C%2Fstop%3E%3Cstop%20stop-color%3D%22%23FF9900%22%20offset%3D%22100%25%22%3E%3C%2Fstop%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E%3Cg%20id%3D%22Icon-Architecture%2F64%2FArch_Amazon-Elastic-Container-Service_64%22%20stroke%3D%22none%22%20stroke-width%3D%221%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Cg%20id%3D%22Icon-Architecture-BG%2F64%2FContainers%22%20fill%3D%22url(%23linearGradient-1)%22%3E%3Crect%20id%3D%22Rectangle%22%20x%3D%220%22%20y%3D%220%22%20width%3D%2280%22%20height%3D%2280%22%3E%3C%2Frect%3E%3C%2Fg%3E%3Cpath%20d%3D%22M64%2C48.2340095%20L56%2C43.4330117%20L56%2C32.0000169%20C56%2C31.6440171%2055.812%2C31.3150172%2055.504%2C31.1360173%20L44%2C24.4260204%20L44%2C14.7520248%20L64%2C26.5710194%20L64%2C48.2340095%20Z%20M65.509%2C25.13902%20L43.509%2C12.139026%20C43.199%2C11.9560261%2042.818%2C11.9540261%2042.504%2C12.131026%20C42.193%2C12.3090259%2042%2C12.6410257%2042%2C13.0000256%20L42%2C25.0000201%20C42%2C25.3550199%2042.189%2C25.6840198%2042.496%2C25.8640197%20L54%2C32.5740166%20L54%2C44.0000114%20C54%2C44.3510113%2054.185%2C44.6770111%2054.486%2C44.857011%20L64.486%2C50.8570083%20C64.644%2C50.9520082%2064.822%2C51%2065%2C51%20C65.17%2C51%2065.34%2C50.9570082%2065.493%2C50.8700083%20C65.807%2C50.6930084%2066%2C50.3600085%2066%2C50%20L66%2C26.0000196%20C66%2C25.6460198%2065.814%2C25.31902%2065.509%2C25.13902%20L65.509%2C25.13902%20Z%20M40.445%2C66.863001%20L17%2C54.3990067%20L17%2C26.5710194%20L37%2C14.7520248%20L37%2C24.4510204%20L26.463%2C31.1560173%20C26.175%2C31.3400172%2026%2C31.6580171%2026%2C32.0000169%20L26%2C49.0000091%20C26%2C49.373009%2026.208%2C49.7150088%2026.538%2C49.8870087%20L39.991%2C56.8870055%20C40.28%2C57.0370055%2040.624%2C57.0380055%2040.912%2C56.8880055%20L53.964%2C50.1440086%20L61.996%2C54.9640064%20L40.445%2C66.863001%20Z%20M64.515%2C54.1420068%20L54.515%2C48.1420095%20C54.217%2C47.9640096%2053.849%2C47.9520096%2053.541%2C48.1120095%20L40.455%2C54.8730065%20L28%2C48.3930094%20L28%2C32.5490167%20L38.537%2C25.8440197%20C38.825%2C25.6600198%2039%2C25.3420199%2039%2C25.0000201%20L39%2C13.0000256%20C39%2C12.6410257%2038.808%2C12.3090259%2038.496%2C12.131026%20C38.184%2C11.9540261%2037.802%2C11.9560261%2037.491%2C12.139026%20L15.491%2C25.13902%20C15.187%2C25.31902%2015%2C25.6460198%2015%2C26.0000196%20L15%2C55%20C15%2C55.3690062%2015.204%2C55.7090061%2015.53%2C55.883006%20L39.984%2C68.8830001%20C40.131%2C68.961%2040.292%2C69%2040.453%2C69%20C40.62%2C69%2040.786%2C68.958%2040.937%2C68.8750001%20L64.484%2C55.875006%20C64.797%2C55.7020061%2064.993%2C55.3750062%2065.0001416%2C55.0180064%20C65.006%2C54.6600066%2064.821%2C54.3260067%2064.515%2C54.1420068%20L64.515%2C54.1420068%20Z%22%20id%3D%22Amazon-Elastic-Container-Service_Icon_64_Squid%22%20fill%3D%22%23FFFFFF%22%3E%3C%2Fpath%3E%3C%2Fg%3E%3C%2Fsvg%3E",
|
||||
"layout": [
|
||||
{
|
||||
"h": 6,
|
||||
"i": "f78becf8-0328-48b4-84b6-ff4dac325940",
|
||||
"moved": false,
|
||||
"static": false,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
{
|
||||
"h": 6,
|
||||
"i": "2b4eac06-b426-4f78-b874-2e1734c4104b",
|
||||
"moved": false,
|
||||
"static": false,
|
||||
"w": 6,
|
||||
"x": 6,
|
||||
"y": 0
|
||||
},
|
||||
{
|
||||
"h": 6,
|
||||
"i": "5bea2bc0-13a2-4937-bccb-60ffe8a43ad5",
|
||||
"moved": false,
|
||||
"static": false,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 6
|
||||
},
|
||||
{
|
||||
"h": 6,
|
||||
"i": "6fac67b0-50ec-4b43-ac4b-320a303d0369",
|
||||
"moved": false,
|
||||
"static": false,
|
||||
"w": 6,
|
||||
"x": 6,
|
||||
"y": 6
|
||||
}
|
||||
],
|
||||
"panelMap": {},
|
||||
"tags": [],
|
||||
"title": "AWS ECS Overview",
|
||||
"uploadedGrafana": false,
|
||||
"variables": {
|
||||
"51f4fa2b-89c7-47c2-9795-f32cffaab985": {
|
||||
"allSelected": false,
|
||||
"customValue": "",
|
||||
"description": "AWS Account ID",
|
||||
"id": "51f4fa2b-89c7-47c2-9795-f32cffaab985",
|
||||
"key": "51f4fa2b-89c7-47c2-9795-f32cffaab985",
|
||||
"modificationUUID": "7b814d17-8fff-4ed6-a4ea-90e3b1a97584",
|
||||
"multiSelect": false,
|
||||
"name": "Account",
|
||||
"order": 0,
|
||||
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud_account_id') AS cloud_account_id\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_ECS_MemoryUtilization_max' GROUP BY cloud_account_id",
|
||||
"showALLOption": false,
|
||||
"sort": "DISABLED",
|
||||
"textboxValue": "",
|
||||
"type": "QUERY"
|
||||
},
|
||||
"9faf0f4b-b245-4b3c-83a3-60cfa76dfeb0": {
|
||||
"allSelected": false,
|
||||
"customValue": "",
|
||||
"description": "Account Region",
|
||||
"id": "9faf0f4b-b245-4b3c-83a3-60cfa76dfeb0",
|
||||
"key": "9faf0f4b-b245-4b3c-83a3-60cfa76dfeb0",
|
||||
"modificationUUID": "3b5f499b-22a3-4c8a-847c-8d3811c9e6b2",
|
||||
"multiSelect": false,
|
||||
"name": "Region",
|
||||
"order": 1,
|
||||
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud_region') AS region\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_ECS_MemoryUtilization_max' AND JSONExtractString(labels, 'cloud_account_id') IN {{.Account}} GROUP BY region",
|
||||
"showALLOption": false,
|
||||
"sort": "ASC",
|
||||
"textboxValue": "",
|
||||
"type": "QUERY"
|
||||
},
|
||||
"bfbdbcbe-a168-4d81-b108-36339e249116": {
|
||||
"allSelected": true,
|
||||
"customValue": "",
|
||||
"description": "ECS Cluster Name",
|
||||
"id": "bfbdbcbe-a168-4d81-b108-36339e249116",
|
||||
"key": "bfbdbcbe-a168-4d81-b108-36339e249116",
|
||||
"modificationUUID": "9fb0d63c-ac6c-497d-82b3-17d95944e245",
|
||||
"multiSelect": true,
|
||||
"name": "Cluster",
|
||||
"order": 2,
|
||||
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'ClusterName') AS cluster\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_ECS_MemoryUtilization_max' AND JSONExtractString(labels, 'cloud_account_id') IN {{.Account}} AND JSONExtractString(labels, 'cloud_region') IN {{.Region}}\nGROUP BY cluster",
|
||||
"showALLOption": true,
|
||||
"sort": "ASC",
|
||||
"textboxValue": "",
|
||||
"type": "QUERY"
|
||||
}
|
||||
},
|
||||
"version": "v4",
|
||||
"widgets": [
|
||||
{
|
||||
"bucketCount": 30,
|
||||
"bucketWidth": 0,
|
||||
"columnUnits": {},
|
||||
"description": "",
|
||||
"fillSpans": false,
|
||||
"id": "f78becf8-0328-48b4-84b6-ff4dac325940",
|
||||
"isLogScale": false,
|
||||
"isStacked": false,
|
||||
"mergeAllActiveQueries": false,
|
||||
"nullZeroValues": "zero",
|
||||
"opacity": "1",
|
||||
"panelTypes": "graph",
|
||||
"query": {
|
||||
"builder": {
|
||||
"queryData": [
|
||||
{
|
||||
"aggregateAttribute": {
|
||||
"dataType": "float64",
|
||||
"id": "aws_ECS_MemoryUtilization_max--float64--Gauge--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "aws_ECS_MemoryUtilization_max",
|
||||
"type": "Gauge"
|
||||
},
|
||||
"aggregateOperator": "max",
|
||||
"dataSource": "metrics",
|
||||
"disabled": false,
|
||||
"expression": "A",
|
||||
"filters": {
|
||||
"items": [
|
||||
{
|
||||
"id": "26ac617d",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "cloud_region--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "cloud_region",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "=",
|
||||
"value": "$Region"
|
||||
},
|
||||
{
|
||||
"id": "57172ed9",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "cloud_account_id--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "cloud_account_id",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "=",
|
||||
"value": "$Account"
|
||||
},
|
||||
{
|
||||
"id": "49b9f85e",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "ClusterName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "ClusterName",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "in",
|
||||
"value": [
|
||||
"$Cluster"
|
||||
]
|
||||
}
|
||||
],
|
||||
"op": "AND"
|
||||
},
|
||||
"functions": [],
|
||||
"groupBy": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "ServiceName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "ServiceName",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "ClusterName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "ClusterName",
|
||||
"type": "tag"
|
||||
}
|
||||
],
|
||||
"having": [],
|
||||
"legend": "{{ServiceName}} ({{ClusterName}})",
|
||||
"limit": null,
|
||||
"orderBy": [],
|
||||
"queryName": "A",
|
||||
"reduceTo": "avg",
|
||||
"spaceAggregation": "max",
|
||||
"stepInterval": 60,
|
||||
"timeAggregation": "max"
|
||||
}
|
||||
],
|
||||
"queryFormulas": []
|
||||
},
|
||||
"clickhouse_sql": [
|
||||
{
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": ""
|
||||
}
|
||||
],
|
||||
"id": "56068fdd-d523-4117-92fa-87c6518ad07c",
|
||||
"promql": [
|
||||
{
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": ""
|
||||
}
|
||||
],
|
||||
"queryType": "builder"
|
||||
},
|
||||
"selectedLogFields": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"name": "body",
|
||||
"type": ""
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"name": "timestamp",
|
||||
"type": ""
|
||||
}
|
||||
],
|
||||
"selectedTracesFields": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "serviceName--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "serviceName",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "name--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "name",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "float64",
|
||||
"id": "durationNano--float64--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "durationNano",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "httpMethod--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "httpMethod",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "responseStatusCode--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "responseStatusCode",
|
||||
"type": "tag"
|
||||
}
|
||||
],
|
||||
"softMax": 0,
|
||||
"softMin": 0,
|
||||
"stackedBarChart": false,
|
||||
"thresholds": [],
|
||||
"timePreferance": "GLOBAL_TIME",
|
||||
"title": "Maximum Memory Utilization",
|
||||
"yAxisUnit": "none"
|
||||
},
|
||||
{
|
||||
"bucketCount": 30,
|
||||
"bucketWidth": 0,
|
||||
"columnUnits": {},
|
||||
"description": "",
|
||||
"fillSpans": false,
|
||||
"id": "2b4eac06-b426-4f78-b874-2e1734c4104b",
|
||||
"isLogScale": false,
|
||||
"isStacked": false,
|
||||
"mergeAllActiveQueries": false,
|
||||
"nullZeroValues": "zero",
|
||||
"opacity": "1",
|
||||
"panelTypes": "graph",
|
||||
"query": {
|
||||
"builder": {
|
||||
"queryData": [
|
||||
{
|
||||
"aggregateAttribute": {
|
||||
"dataType": "float64",
|
||||
"id": "aws_ECS_MemoryUtilization_min--float64--Gauge--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "aws_ECS_MemoryUtilization_min",
|
||||
"type": "Gauge"
|
||||
},
|
||||
"aggregateOperator": "min",
|
||||
"dataSource": "metrics",
|
||||
"disabled": false,
|
||||
"expression": "A",
|
||||
"filters": {
|
||||
"items": [
|
||||
{
|
||||
"id": "cd4b8848",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "cloud_region--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "cloud_region",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "=",
|
||||
"value": "$Region"
|
||||
},
|
||||
{
|
||||
"id": "aa5115c6",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "cloud_account_id--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "cloud_account_id",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "=",
|
||||
"value": "$Account"
|
||||
},
|
||||
{
|
||||
"id": "f60677b6",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "ClusterName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "ClusterName",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "in",
|
||||
"value": [
|
||||
"$Cluster"
|
||||
]
|
||||
}
|
||||
],
|
||||
"op": "AND"
|
||||
},
|
||||
"functions": [],
|
||||
"groupBy": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "ServiceName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "ServiceName",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "ClusterName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "ClusterName",
|
||||
"type": "tag"
|
||||
}
|
||||
],
|
||||
"having": [],
|
||||
"legend": "{{ServiceName}} ({{ClusterName}})",
|
||||
"limit": null,
|
||||
"orderBy": [],
|
||||
"queryName": "A",
|
||||
"reduceTo": "avg",
|
||||
"spaceAggregation": "min",
|
||||
"stepInterval": 60,
|
||||
"timeAggregation": "min"
|
||||
}
|
||||
],
|
||||
"queryFormulas": []
|
||||
},
|
||||
"clickhouse_sql": [
|
||||
{
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": ""
|
||||
}
|
||||
],
|
||||
"id": "fb19342e-cbde-40d8-b12f-ad108698356b",
|
||||
"promql": [
|
||||
{
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": ""
|
||||
}
|
||||
],
|
||||
"queryType": "builder"
|
||||
},
|
||||
"selectedLogFields": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"name": "body",
|
||||
"type": ""
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"name": "timestamp",
|
||||
"type": ""
|
||||
}
|
||||
],
|
||||
"selectedTracesFields": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "serviceName--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "serviceName",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "name--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "name",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "float64",
|
||||
"id": "durationNano--float64--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "durationNano",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "httpMethod--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "httpMethod",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "responseStatusCode--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "responseStatusCode",
|
||||
"type": "tag"
|
||||
}
|
||||
],
|
||||
"softMax": 0,
|
||||
"softMin": 0,
|
||||
"stackedBarChart": false,
|
||||
"thresholds": [],
|
||||
"timePreferance": "GLOBAL_TIME",
|
||||
"title": "Minimum Memory Utilization",
|
||||
"yAxisUnit": "none"
|
||||
},
|
||||
{
|
||||
"bucketCount": 30,
|
||||
"bucketWidth": 0,
|
||||
"columnUnits": {},
|
||||
"description": "",
|
||||
"fillSpans": false,
|
||||
"id": "5bea2bc0-13a2-4937-bccb-60ffe8a43ad5",
|
||||
"isLogScale": false,
|
||||
"isStacked": false,
|
||||
"mergeAllActiveQueries": false,
|
||||
"nullZeroValues": "zero",
|
||||
"opacity": "1",
|
||||
"panelTypes": "graph",
|
||||
"query": {
|
||||
"builder": {
|
||||
"queryData": [
|
||||
{
|
||||
"aggregateAttribute": {
|
||||
"dataType": "float64",
|
||||
"id": "aws_ECS_CPUUtilization_max--float64--Gauge--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "aws_ECS_CPUUtilization_max",
|
||||
"type": "Gauge"
|
||||
},
|
||||
"aggregateOperator": "max",
|
||||
"dataSource": "metrics",
|
||||
"disabled": false,
|
||||
"expression": "A",
|
||||
"filters": {
|
||||
"items": [
|
||||
{
|
||||
"id": "2c13c8ee",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "cloud_region--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "cloud_region",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "=",
|
||||
"value": "$Region"
|
||||
},
|
||||
{
|
||||
"id": "f489f6a8",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "cloud_account_id--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "cloud_account_id",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "=",
|
||||
"value": "$Account"
|
||||
},
|
||||
{
|
||||
"id": "94012320",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "ClusterName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "ClusterName",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "in",
|
||||
"value": [
|
||||
"$Cluster"
|
||||
]
|
||||
}
|
||||
],
|
||||
"op": "AND"
|
||||
},
|
||||
"functions": [],
|
||||
"groupBy": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "ServiceName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "ServiceName",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "ClusterName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "ClusterName",
|
||||
"type": "tag"
|
||||
}
|
||||
],
|
||||
"having": [],
|
||||
"legend": "{{ServiceName}} ({{ClusterName}})",
|
||||
"limit": null,
|
||||
"orderBy": [],
|
||||
"queryName": "A",
|
||||
"reduceTo": "avg",
|
||||
"spaceAggregation": "max",
|
||||
"stepInterval": 60,
|
||||
"timeAggregation": "max"
|
||||
}
|
||||
],
|
||||
"queryFormulas": []
|
||||
},
|
||||
"clickhouse_sql": [
|
||||
{
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": ""
|
||||
}
|
||||
],
|
||||
"id": "273e0a76-c780-4b9a-9b03-2649d4227173",
|
||||
"promql": [
|
||||
{
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": ""
|
||||
}
|
||||
],
|
||||
"queryType": "builder"
|
||||
},
|
||||
"selectedLogFields": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"name": "body",
|
||||
"type": ""
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"name": "timestamp",
|
||||
"type": ""
|
||||
}
|
||||
],
|
||||
"selectedTracesFields": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "serviceName--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "serviceName",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "name--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "name",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "float64",
|
||||
"id": "durationNano--float64--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "durationNano",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "httpMethod--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "httpMethod",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "responseStatusCode--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "responseStatusCode",
|
||||
"type": "tag"
|
||||
}
|
||||
],
|
||||
"softMax": 0,
|
||||
"softMin": 0,
|
||||
"stackedBarChart": false,
|
||||
"thresholds": [],
|
||||
"timePreferance": "GLOBAL_TIME",
|
||||
"title": "Maximum CPU Utilization",
|
||||
"yAxisUnit": "none"
|
||||
},
|
||||
{
|
||||
"bucketCount": 30,
|
||||
"bucketWidth": 0,
|
||||
"columnUnits": {},
|
||||
"description": "",
|
||||
"fillSpans": false,
|
||||
"id": "6fac67b0-50ec-4b43-ac4b-320a303d0369",
|
||||
"isLogScale": false,
|
||||
"isStacked": false,
|
||||
"mergeAllActiveQueries": false,
|
||||
"nullZeroValues": "zero",
|
||||
"opacity": "1",
|
||||
"panelTypes": "graph",
|
||||
"query": {
|
||||
"builder": {
|
||||
"queryData": [
|
||||
{
|
||||
"aggregateAttribute": {
|
||||
"dataType": "float64",
|
||||
"id": "aws_ECS_CPUUtilization_min--float64--Gauge--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "aws_ECS_CPUUtilization_min",
|
||||
"type": "Gauge"
|
||||
},
|
||||
"aggregateOperator": "min",
|
||||
"dataSource": "metrics",
|
||||
"disabled": false,
|
||||
"expression": "A",
|
||||
"filters": {
|
||||
"items": [
|
||||
{
|
||||
"id": "758ba906",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "cloud_region--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "cloud_region",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "=",
|
||||
"value": "$Region"
|
||||
},
|
||||
{
|
||||
"id": "4ffe6bf7",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "cloud_account_id--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "cloud_account_id",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "=",
|
||||
"value": "$Account"
|
||||
},
|
||||
{
|
||||
"id": "53d98059",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "ClusterName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "ClusterName",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "in",
|
||||
"value": [
|
||||
"$Cluster"
|
||||
]
|
||||
}
|
||||
],
|
||||
"op": "AND"
|
||||
},
|
||||
"functions": [],
|
||||
"groupBy": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "ServiceName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "ServiceName",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "ClusterName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "ClusterName",
|
||||
"type": "tag"
|
||||
}
|
||||
],
|
||||
"having": [],
|
||||
"legend": "{{ServiceName}} ({{ClusterName}})",
|
||||
"limit": null,
|
||||
"orderBy": [],
|
||||
"queryName": "A",
|
||||
"reduceTo": "avg",
|
||||
"spaceAggregation": "min",
|
||||
"stepInterval": 60,
|
||||
"timeAggregation": "min"
|
||||
}
|
||||
],
|
||||
"queryFormulas": []
|
||||
},
|
||||
"clickhouse_sql": [
|
||||
{
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": ""
|
||||
}
|
||||
],
|
||||
"id": "c89482b3-5a98-4e2c-be0d-ef036d7dac05",
|
||||
"promql": [
|
||||
{
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": ""
|
||||
}
|
||||
],
|
||||
"queryType": "builder"
|
||||
},
|
||||
"selectedLogFields": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"name": "body",
|
||||
"type": ""
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"name": "timestamp",
|
||||
"type": ""
|
||||
}
|
||||
],
|
||||
"selectedTracesFields": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "serviceName--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "serviceName",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "name--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "name",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "float64",
|
||||
"id": "durationNano--float64--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "durationNano",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "httpMethod--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "httpMethod",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "responseStatusCode--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "responseStatusCode",
|
||||
"type": "tag"
|
||||
}
|
||||
],
|
||||
"softMax": 0,
|
||||
"softMin": 0,
|
||||
"stackedBarChart": false,
|
||||
"thresholds": [],
|
||||
"timePreferance": "GLOBAL_TIME",
|
||||
"title": "Minimum CPU Utilization",
|
||||
"yAxisUnit": "none"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,851 @@
|
||||
{
|
||||
"description": "View key AWS ECS metrics with an out of the box dashboard.\n",
|
||||
"image":"data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22UTF-8%22%3F%3E%3Csvg%20width%3D%2280px%22%20height%3D%2280px%22%20viewBox%3D%220%200%2080%2080%22%20version%3D%221.1%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%3E%3C!--%20Generator%3A%20Sketch%2064%20(93537)%20-%20https%3A%2F%2Fsketch.com%20--%3E%3Ctitle%3EIcon-Architecture%2F64%2FArch_Amazon-Elastic-Container-Service_64%3C%2Ftitle%3E%3Cdesc%3ECreated%20with%20Sketch.%3C%2Fdesc%3E%3Cdefs%3E%3ClinearGradient%20x1%3D%220%25%22%20y1%3D%22100%25%22%20x2%3D%22100%25%22%20y2%3D%220%25%22%20id%3D%22linearGradient-1%22%3E%3Cstop%20stop-color%3D%22%23C8511B%22%20offset%3D%220%25%22%3E%3C%2Fstop%3E%3Cstop%20stop-color%3D%22%23FF9900%22%20offset%3D%22100%25%22%3E%3C%2Fstop%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E%3Cg%20id%3D%22Icon-Architecture%2F64%2FArch_Amazon-Elastic-Container-Service_64%22%20stroke%3D%22none%22%20stroke-width%3D%221%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Cg%20id%3D%22Icon-Architecture-BG%2F64%2FContainers%22%20fill%3D%22url(%23linearGradient-1)%22%3E%3Crect%20id%3D%22Rectangle%22%20x%3D%220%22%20y%3D%220%22%20width%3D%2280%22%20height%3D%2280%22%3E%3C%2Frect%3E%3C%2Fg%3E%3Cpath%20d%3D%22M64%2C48.2340095%20L56%2C43.4330117%20L56%2C32.0000169%20C56%2C31.6440171%2055.812%2C31.3150172%2055.504%2C31.1360173%20L44%2C24.4260204%20L44%2C14.7520248%20L64%2C26.5710194%20L64%2C48.2340095%20Z%20M65.509%2C25.13902%20L43.509%2C12.139026%20C43.199%2C11.9560261%2042.818%2C11.9540261%2042.504%2C12.131026%20C42.193%2C12.3090259%2042%2C12.6410257%2042%2C13.0000256%20L42%2C25.0000201%20C42%2C25.3550199%2042.189%2C25.6840198%2042.496%2C25.8640197%20L54%2C32.5740166%20L54%2C44.0000114%20C54%2C44.3510113%2054.185%2C44.6770111%2054.486%2C44.857011%20L64.486%2C50.8570083%20C64.644%2C50.9520082%2064.822%2C51%2065%2C51%20C65.17%2C51%2065.34%2C50.9570082%2065.493%2C50.8700083%20C65.807%2C50.6930084%2066%2C50.3600085%2066%2C50%20L66%2C26.0000196%20C66%2C25.6460198%2065.814%2C25.31902%2065.509%2C25.13902%20L65.509%2C25.13902%20Z%20M40.445%2C66.863001%20L17%2C54.3990067%20L17%2C26.5710194%20L37%2C14.7520248%20L37%2C24.4510204%20L26.463%2C31.1560173%20C26.175%2C31.3400172%2026%2C31.6580171%2026%2C32.0000169%20L26%2C49.0000091%20C26%2C49.373009%2026.208%2C49.7150088%2026.538%2C49.8870087%20L39.991%2C56.8870055%20C40.28%2C57.0370055%2040.624%2C57.0380055%2040.912%2C56.8880055%20L53.964%2C50.1440086%20L61.996%2C54.9640064%20L40.445%2C66.863001%20Z%20M64.515%2C54.1420068%20L54.515%2C48.1420095%20C54.217%2C47.9640096%2053.849%2C47.9520096%2053.541%2C48.1120095%20L40.455%2C54.8730065%20L28%2C48.3930094%20L28%2C32.5490167%20L38.537%2C25.8440197%20C38.825%2C25.6600198%2039%2C25.3420199%2039%2C25.0000201%20L39%2C13.0000256%20C39%2C12.6410257%2038.808%2C12.3090259%2038.496%2C12.131026%20C38.184%2C11.9540261%2037.802%2C11.9560261%2037.491%2C12.139026%20L15.491%2C25.13902%20C15.187%2C25.31902%2015%2C25.6460198%2015%2C26.0000196%20L15%2C55%20C15%2C55.3690062%2015.204%2C55.7090061%2015.53%2C55.883006%20L39.984%2C68.8830001%20C40.131%2C68.961%2040.292%2C69%2040.453%2C69%20C40.62%2C69%2040.786%2C68.958%2040.937%2C68.8750001%20L64.484%2C55.875006%20C64.797%2C55.7020061%2064.993%2C55.3750062%2065.0001416%2C55.0180064%20C65.006%2C54.6600066%2064.821%2C54.3260067%2064.515%2C54.1420068%20L64.515%2C54.1420068%20Z%22%20id%3D%22Amazon-Elastic-Container-Service_Icon_64_Squid%22%20fill%3D%22%23FFFFFF%22%3E%3C%2Fpath%3E%3C%2Fg%3E%3C%2Fsvg%3E",
|
||||
"layout": [
|
||||
{
|
||||
"h": 6,
|
||||
"i": "f78becf8-0328-48b4-84b6-ff4dac325940",
|
||||
"moved": false,
|
||||
"static": false,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
{
|
||||
"h": 6,
|
||||
"i": "2b4eac06-b426-4f78-b874-2e1734c4104b",
|
||||
"moved": false,
|
||||
"static": false,
|
||||
"w": 6,
|
||||
"x": 6,
|
||||
"y": 0
|
||||
},
|
||||
{
|
||||
"h": 6,
|
||||
"i": "5bea2bc0-13a2-4937-bccb-60ffe8a43ad5",
|
||||
"moved": false,
|
||||
"static": false,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 6
|
||||
},
|
||||
{
|
||||
"h": 6,
|
||||
"i": "6fac67b0-50ec-4b43-ac4b-320a303d0369",
|
||||
"moved": false,
|
||||
"static": false,
|
||||
"w": 6,
|
||||
"x": 6,
|
||||
"y": 6
|
||||
}
|
||||
],
|
||||
"panelMap": {},
|
||||
"tags": [],
|
||||
"title": "AWS ECS Overview",
|
||||
"uploadedGrafana": false,
|
||||
"variables": {
|
||||
"51f4fa2b-89c7-47c2-9795-f32cffaab985": {
|
||||
"allSelected": false,
|
||||
"customValue": "",
|
||||
"description": "AWS Account ID",
|
||||
"id": "51f4fa2b-89c7-47c2-9795-f32cffaab985",
|
||||
"key": "51f4fa2b-89c7-47c2-9795-f32cffaab985",
|
||||
"modificationUUID": "7b814d17-8fff-4ed6-a4ea-90e3b1a97584",
|
||||
"multiSelect": false,
|
||||
"name": "Account",
|
||||
"order": 0,
|
||||
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud.account.id') AS `cloud.account.id`\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_ECS_MemoryUtilization_max' GROUP BY `cloud.account.id`",
|
||||
"showALLOption": false,
|
||||
"sort": "DISABLED",
|
||||
"textboxValue": "",
|
||||
"type": "QUERY"
|
||||
},
|
||||
"9faf0f4b-b245-4b3c-83a3-60cfa76dfeb0": {
|
||||
"allSelected": false,
|
||||
"customValue": "",
|
||||
"description": "Account Region",
|
||||
"id": "9faf0f4b-b245-4b3c-83a3-60cfa76dfeb0",
|
||||
"key": "9faf0f4b-b245-4b3c-83a3-60cfa76dfeb0",
|
||||
"modificationUUID": "3b5f499b-22a3-4c8a-847c-8d3811c9e6b2",
|
||||
"multiSelect": false,
|
||||
"name": "Region",
|
||||
"order": 1,
|
||||
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud.region') AS region\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_ECS_MemoryUtilization_max' AND JSONExtractString(labels, 'cloud.account.id') IN {{.Account}} GROUP BY region",
|
||||
"showALLOption": false,
|
||||
"sort": "ASC",
|
||||
"textboxValue": "",
|
||||
"type": "QUERY"
|
||||
},
|
||||
"bfbdbcbe-a168-4d81-b108-36339e249116": {
|
||||
"allSelected": true,
|
||||
"customValue": "",
|
||||
"description": "ECS Cluster Name",
|
||||
"id": "bfbdbcbe-a168-4d81-b108-36339e249116",
|
||||
"key": "bfbdbcbe-a168-4d81-b108-36339e249116",
|
||||
"modificationUUID": "9fb0d63c-ac6c-497d-82b3-17d95944e245",
|
||||
"multiSelect": true,
|
||||
"name": "Cluster",
|
||||
"order": 2,
|
||||
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'ClusterName') AS cluster\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_ECS_MemoryUtilization_max' AND JSONExtractString(labels, 'cloud.account.id') IN {{.Account}} AND JSONExtractString(labels, 'cloud.region') IN {{.Region}}\nGROUP BY cluster",
|
||||
"showALLOption": true,
|
||||
"sort": "ASC",
|
||||
"textboxValue": "",
|
||||
"type": "QUERY"
|
||||
}
|
||||
},
|
||||
"version": "v4",
|
||||
"widgets": [
|
||||
{
|
||||
"bucketCount": 30,
|
||||
"bucketWidth": 0,
|
||||
"columnUnits": {},
|
||||
"description": "",
|
||||
"fillSpans": false,
|
||||
"id": "f78becf8-0328-48b4-84b6-ff4dac325940",
|
||||
"isLogScale": false,
|
||||
"isStacked": false,
|
||||
"mergeAllActiveQueries": false,
|
||||
"nullZeroValues": "zero",
|
||||
"opacity": "1",
|
||||
"panelTypes": "graph",
|
||||
"query": {
|
||||
"builder": {
|
||||
"queryData": [
|
||||
{
|
||||
"aggregateAttribute": {
|
||||
"dataType": "float64",
|
||||
"id": "aws_ECS_MemoryUtilization_max--float64--Gauge--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "aws_ECS_MemoryUtilization_max",
|
||||
"type": "Gauge"
|
||||
},
|
||||
"aggregateOperator": "max",
|
||||
"dataSource": "metrics",
|
||||
"disabled": false,
|
||||
"expression": "A",
|
||||
"filters": {
|
||||
"items": [
|
||||
{
|
||||
"id": "26ac617d",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "cloud.region--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "cloud.region",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "=",
|
||||
"value": "$Region"
|
||||
},
|
||||
{
|
||||
"id": "57172ed9",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "cloud.account.id--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "cloud.account.id",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "=",
|
||||
"value": "$Account"
|
||||
},
|
||||
{
|
||||
"id": "49b9f85e",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "ClusterName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "ClusterName",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "in",
|
||||
"value": [
|
||||
"$Cluster"
|
||||
]
|
||||
}
|
||||
],
|
||||
"op": "AND"
|
||||
},
|
||||
"functions": [],
|
||||
"groupBy": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "ServiceName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "ServiceName",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "ClusterName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "ClusterName",
|
||||
"type": "tag"
|
||||
}
|
||||
],
|
||||
"having": [],
|
||||
"legend": "{{ServiceName}} ({{ClusterName}})",
|
||||
"limit": null,
|
||||
"orderBy": [],
|
||||
"queryName": "A",
|
||||
"reduceTo": "avg",
|
||||
"spaceAggregation": "max",
|
||||
"stepInterval": 60,
|
||||
"timeAggregation": "max"
|
||||
}
|
||||
],
|
||||
"queryFormulas": []
|
||||
},
|
||||
"clickhouse_sql": [
|
||||
{
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": ""
|
||||
}
|
||||
],
|
||||
"id": "56068fdd-d523-4117-92fa-87c6518ad07c",
|
||||
"promql": [
|
||||
{
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": ""
|
||||
}
|
||||
],
|
||||
"queryType": "builder"
|
||||
},
|
||||
"selectedLogFields": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"name": "body",
|
||||
"type": ""
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"name": "timestamp",
|
||||
"type": ""
|
||||
}
|
||||
],
|
||||
"selectedTracesFields": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "serviceName--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "serviceName",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "name--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "name",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "float64",
|
||||
"id": "durationNano--float64--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "durationNano",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "httpMethod--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "httpMethod",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "responseStatusCode--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "responseStatusCode",
|
||||
"type": "tag"
|
||||
}
|
||||
],
|
||||
"softMax": 0,
|
||||
"softMin": 0,
|
||||
"stackedBarChart": false,
|
||||
"thresholds": [],
|
||||
"timePreferance": "GLOBAL_TIME",
|
||||
"title": "Maximum Memory Utilization",
|
||||
"yAxisUnit": "none"
|
||||
},
|
||||
{
|
||||
"bucketCount": 30,
|
||||
"bucketWidth": 0,
|
||||
"columnUnits": {},
|
||||
"description": "",
|
||||
"fillSpans": false,
|
||||
"id": "2b4eac06-b426-4f78-b874-2e1734c4104b",
|
||||
"isLogScale": false,
|
||||
"isStacked": false,
|
||||
"mergeAllActiveQueries": false,
|
||||
"nullZeroValues": "zero",
|
||||
"opacity": "1",
|
||||
"panelTypes": "graph",
|
||||
"query": {
|
||||
"builder": {
|
||||
"queryData": [
|
||||
{
|
||||
"aggregateAttribute": {
|
||||
"dataType": "float64",
|
||||
"id": "aws_ECS_MemoryUtilization_min--float64--Gauge--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "aws_ECS_MemoryUtilization_min",
|
||||
"type": "Gauge"
|
||||
},
|
||||
"aggregateOperator": "min",
|
||||
"dataSource": "metrics",
|
||||
"disabled": false,
|
||||
"expression": "A",
|
||||
"filters": {
|
||||
"items": [
|
||||
{
|
||||
"id": "cd4b8848",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "cloud.region--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "cloud.region",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "=",
|
||||
"value": "$Region"
|
||||
},
|
||||
{
|
||||
"id": "aa5115c6",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "cloud.account.id--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "cloud.account.id",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "=",
|
||||
"value": "$Account"
|
||||
},
|
||||
{
|
||||
"id": "f60677b6",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "ClusterName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "ClusterName",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "in",
|
||||
"value": [
|
||||
"$Cluster"
|
||||
]
|
||||
}
|
||||
],
|
||||
"op": "AND"
|
||||
},
|
||||
"functions": [],
|
||||
"groupBy": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "ServiceName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "ServiceName",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "ClusterName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "ClusterName",
|
||||
"type": "tag"
|
||||
}
|
||||
],
|
||||
"having": [],
|
||||
"legend": "{{ServiceName}} ({{ClusterName}})",
|
||||
"limit": null,
|
||||
"orderBy": [],
|
||||
"queryName": "A",
|
||||
"reduceTo": "avg",
|
||||
"spaceAggregation": "min",
|
||||
"stepInterval": 60,
|
||||
"timeAggregation": "min"
|
||||
}
|
||||
],
|
||||
"queryFormulas": []
|
||||
},
|
||||
"clickhouse_sql": [
|
||||
{
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": ""
|
||||
}
|
||||
],
|
||||
"id": "fb19342e-cbde-40d8-b12f-ad108698356b",
|
||||
"promql": [
|
||||
{
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": ""
|
||||
}
|
||||
],
|
||||
"queryType": "builder"
|
||||
},
|
||||
"selectedLogFields": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"name": "body",
|
||||
"type": ""
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"name": "timestamp",
|
||||
"type": ""
|
||||
}
|
||||
],
|
||||
"selectedTracesFields": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "serviceName--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "serviceName",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "name--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "name",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "float64",
|
||||
"id": "durationNano--float64--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "durationNano",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "httpMethod--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "httpMethod",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "responseStatusCode--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "responseStatusCode",
|
||||
"type": "tag"
|
||||
}
|
||||
],
|
||||
"softMax": 0,
|
||||
"softMin": 0,
|
||||
"stackedBarChart": false,
|
||||
"thresholds": [],
|
||||
"timePreferance": "GLOBAL_TIME",
|
||||
"title": "Minimum Memory Utilization",
|
||||
"yAxisUnit": "none"
|
||||
},
|
||||
{
|
||||
"bucketCount": 30,
|
||||
"bucketWidth": 0,
|
||||
"columnUnits": {},
|
||||
"description": "",
|
||||
"fillSpans": false,
|
||||
"id": "5bea2bc0-13a2-4937-bccb-60ffe8a43ad5",
|
||||
"isLogScale": false,
|
||||
"isStacked": false,
|
||||
"mergeAllActiveQueries": false,
|
||||
"nullZeroValues": "zero",
|
||||
"opacity": "1",
|
||||
"panelTypes": "graph",
|
||||
"query": {
|
||||
"builder": {
|
||||
"queryData": [
|
||||
{
|
||||
"aggregateAttribute": {
|
||||
"dataType": "float64",
|
||||
"id": "aws_ECS_CPUUtilization_max--float64--Gauge--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "aws_ECS_CPUUtilization_max",
|
||||
"type": "Gauge"
|
||||
},
|
||||
"aggregateOperator": "max",
|
||||
"dataSource": "metrics",
|
||||
"disabled": false,
|
||||
"expression": "A",
|
||||
"filters": {
|
||||
"items": [
|
||||
{
|
||||
"id": "2c13c8ee",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "cloud.region--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "cloud.region",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "=",
|
||||
"value": "$Region"
|
||||
},
|
||||
{
|
||||
"id": "f489f6a8",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "cloud.account.id--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "cloud.account.id",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "=",
|
||||
"value": "$Account"
|
||||
},
|
||||
{
|
||||
"id": "94012320",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "ClusterName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "ClusterName",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "in",
|
||||
"value": [
|
||||
"$Cluster"
|
||||
]
|
||||
}
|
||||
],
|
||||
"op": "AND"
|
||||
},
|
||||
"functions": [],
|
||||
"groupBy": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "ServiceName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "ServiceName",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "ClusterName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "ClusterName",
|
||||
"type": "tag"
|
||||
}
|
||||
],
|
||||
"having": [],
|
||||
"legend": "{{ServiceName}} ({{ClusterName}})",
|
||||
"limit": null,
|
||||
"orderBy": [],
|
||||
"queryName": "A",
|
||||
"reduceTo": "avg",
|
||||
"spaceAggregation": "max",
|
||||
"stepInterval": 60,
|
||||
"timeAggregation": "max"
|
||||
}
|
||||
],
|
||||
"queryFormulas": []
|
||||
},
|
||||
"clickhouse_sql": [
|
||||
{
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": ""
|
||||
}
|
||||
],
|
||||
"id": "273e0a76-c780-4b9a-9b03-2649d4227173",
|
||||
"promql": [
|
||||
{
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": ""
|
||||
}
|
||||
],
|
||||
"queryType": "builder"
|
||||
},
|
||||
"selectedLogFields": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"name": "body",
|
||||
"type": ""
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"name": "timestamp",
|
||||
"type": ""
|
||||
}
|
||||
],
|
||||
"selectedTracesFields": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "serviceName--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "serviceName",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "name--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "name",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "float64",
|
||||
"id": "durationNano--float64--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "durationNano",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "httpMethod--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "httpMethod",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "responseStatusCode--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "responseStatusCode",
|
||||
"type": "tag"
|
||||
}
|
||||
],
|
||||
"softMax": 0,
|
||||
"softMin": 0,
|
||||
"stackedBarChart": false,
|
||||
"thresholds": [],
|
||||
"timePreferance": "GLOBAL_TIME",
|
||||
"title": "Maximum CPU Utilization",
|
||||
"yAxisUnit": "none"
|
||||
},
|
||||
{
|
||||
"bucketCount": 30,
|
||||
"bucketWidth": 0,
|
||||
"columnUnits": {},
|
||||
"description": "",
|
||||
"fillSpans": false,
|
||||
"id": "6fac67b0-50ec-4b43-ac4b-320a303d0369",
|
||||
"isLogScale": false,
|
||||
"isStacked": false,
|
||||
"mergeAllActiveQueries": false,
|
||||
"nullZeroValues": "zero",
|
||||
"opacity": "1",
|
||||
"panelTypes": "graph",
|
||||
"query": {
|
||||
"builder": {
|
||||
"queryData": [
|
||||
{
|
||||
"aggregateAttribute": {
|
||||
"dataType": "float64",
|
||||
"id": "aws_ECS_CPUUtilization_min--float64--Gauge--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "aws_ECS_CPUUtilization_min",
|
||||
"type": "Gauge"
|
||||
},
|
||||
"aggregateOperator": "min",
|
||||
"dataSource": "metrics",
|
||||
"disabled": false,
|
||||
"expression": "A",
|
||||
"filters": {
|
||||
"items": [
|
||||
{
|
||||
"id": "758ba906",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "cloud.region--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "cloud.region",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "=",
|
||||
"value": "$Region"
|
||||
},
|
||||
{
|
||||
"id": "4ffe6bf7",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "cloud.account.id--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "cloud.account.id",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "=",
|
||||
"value": "$Account"
|
||||
},
|
||||
{
|
||||
"id": "53d98059",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "ClusterName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "ClusterName",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "in",
|
||||
"value": [
|
||||
"$Cluster"
|
||||
]
|
||||
}
|
||||
],
|
||||
"op": "AND"
|
||||
},
|
||||
"functions": [],
|
||||
"groupBy": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "ServiceName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "ServiceName",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "ClusterName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "ClusterName",
|
||||
"type": "tag"
|
||||
}
|
||||
],
|
||||
"having": [],
|
||||
"legend": "{{ServiceName}} ({{ClusterName}})",
|
||||
"limit": null,
|
||||
"orderBy": [],
|
||||
"queryName": "A",
|
||||
"reduceTo": "avg",
|
||||
"spaceAggregation": "min",
|
||||
"stepInterval": 60,
|
||||
"timeAggregation": "min"
|
||||
}
|
||||
],
|
||||
"queryFormulas": []
|
||||
},
|
||||
"clickhouse_sql": [
|
||||
{
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": ""
|
||||
}
|
||||
],
|
||||
"id": "c89482b3-5a98-4e2c-be0d-ef036d7dac05",
|
||||
"promql": [
|
||||
{
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": ""
|
||||
}
|
||||
],
|
||||
"queryType": "builder"
|
||||
},
|
||||
"selectedLogFields": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"name": "body",
|
||||
"type": ""
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"name": "timestamp",
|
||||
"type": ""
|
||||
}
|
||||
],
|
||||
"selectedTracesFields": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "serviceName--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "serviceName",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "name--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "name",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "float64",
|
||||
"id": "durationNano--float64--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "durationNano",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "httpMethod--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "httpMethod",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "responseStatusCode--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "responseStatusCode",
|
||||
"type": "tag"
|
||||
}
|
||||
],
|
||||
"softMax": 0,
|
||||
"softMin": 0,
|
||||
"stackedBarChart": false,
|
||||
"thresholds": [],
|
||||
"timePreferance": "GLOBAL_TIME",
|
||||
"title": "Minimum CPU Utilization",
|
||||
"yAxisUnit": "none"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="80px" height="80px" viewBox="0 0 80 80" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 64 (93537) - https://sketch.com -->
|
||||
<title>Icon-Architecture/64/Arch_Amazon-Elastic-Container-Service_64</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
<linearGradient x1="0%" y1="100%" x2="100%" y2="0%" id="linearGradient-1">
|
||||
<stop stop-color="#C8511B" offset="0%"></stop>
|
||||
<stop stop-color="#FF9900" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g id="Icon-Architecture/64/Arch_Amazon-Elastic-Container-Service_64" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Icon-Architecture-BG/64/Containers" fill="url(#linearGradient-1)">
|
||||
<rect id="Rectangle" x="0" y="0" width="80" height="80"></rect>
|
||||
</g>
|
||||
<path d="M64,48.2340095 L56,43.4330117 L56,32.0000169 C56,31.6440171 55.812,31.3150172 55.504,31.1360173 L44,24.4260204 L44,14.7520248 L64,26.5710194 L64,48.2340095 Z M65.509,25.13902 L43.509,12.139026 C43.199,11.9560261 42.818,11.9540261 42.504,12.131026 C42.193,12.3090259 42,12.6410257 42,13.0000256 L42,25.0000201 C42,25.3550199 42.189,25.6840198 42.496,25.8640197 L54,32.5740166 L54,44.0000114 C54,44.3510113 54.185,44.6770111 54.486,44.857011 L64.486,50.8570083 C64.644,50.9520082 64.822,51 65,51 C65.17,51 65.34,50.9570082 65.493,50.8700083 C65.807,50.6930084 66,50.3600085 66,50 L66,26.0000196 C66,25.6460198 65.814,25.31902 65.509,25.13902 L65.509,25.13902 Z M40.445,66.863001 L17,54.3990067 L17,26.5710194 L37,14.7520248 L37,24.4510204 L26.463,31.1560173 C26.175,31.3400172 26,31.6580171 26,32.0000169 L26,49.0000091 C26,49.373009 26.208,49.7150088 26.538,49.8870087 L39.991,56.8870055 C40.28,57.0370055 40.624,57.0380055 40.912,56.8880055 L53.964,50.1440086 L61.996,54.9640064 L40.445,66.863001 Z M64.515,54.1420068 L54.515,48.1420095 C54.217,47.9640096 53.849,47.9520096 53.541,48.1120095 L40.455,54.8730065 L28,48.3930094 L28,32.5490167 L38.537,25.8440197 C38.825,25.6600198 39,25.3420199 39,25.0000201 L39,13.0000256 C39,12.6410257 38.808,12.3090259 38.496,12.131026 C38.184,11.9540261 37.802,11.9560261 37.491,12.139026 L15.491,25.13902 C15.187,25.31902 15,25.6460198 15,26.0000196 L15,55 C15,55.3690062 15.204,55.7090061 15.53,55.883006 L39.984,68.8830001 C40.131,68.961 40.292,69 40.453,69 C40.62,69 40.786,68.958 40.937,68.8750001 L64.484,55.875006 C64.797,55.7020061 64.993,55.3750062 65.0001416,55.0180064 C65.006,54.6600066 64.821,54.3260067 64.515,54.1420068 L64.515,54.1420068 Z" id="Amazon-Elastic-Container-Service_Icon_64_Squid" fill="#FFFFFF"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
@@ -0,0 +1,871 @@
|
||||
{
|
||||
"id": "ecs",
|
||||
"title": "ECS",
|
||||
"icon": "file://icon.svg",
|
||||
"overview": "file://overview.md",
|
||||
"supportedSignals": {
|
||||
"metrics": true,
|
||||
"logs": true
|
||||
},
|
||||
"dataCollected": {
|
||||
"metrics": [
|
||||
{
|
||||
"name": "aws_ECS_CPUUtilization_count",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_CPUUtilization_max",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_CPUUtilization_min",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_CPUUtilization_sum",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_ContainerCpuReserved_count",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_ContainerCpuReserved_max",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_ContainerCpuReserved_min",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_ContainerCpuReserved_sum",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_ContainerCpuUtilization_count",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_ContainerCpuUtilization_max",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_ContainerCpuUtilization_min",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_ContainerCpuUtilization_sum",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_ContainerCpuUtilized_count",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_ContainerCpuUtilized_max",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_ContainerCpuUtilized_min",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_ContainerCpuUtilized_sum",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_ContainerInstanceCount_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_ContainerInstanceCount_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_ContainerInstanceCount_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_ContainerInstanceCount_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_ContainerMemoryReserved_count",
|
||||
"unit": "Megabytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_ContainerMemoryReserved_max",
|
||||
"unit": "Megabytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_ContainerMemoryReserved_min",
|
||||
"unit": "Megabytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_ContainerMemoryReserved_sum",
|
||||
"unit": "Megabytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_ContainerMemoryUtilization_count",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_ContainerMemoryUtilization_max",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_ContainerMemoryUtilization_min",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_ContainerMemoryUtilization_sum",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_ContainerMemoryUtilized_count",
|
||||
"unit": "Megabytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_ContainerMemoryUtilized_max",
|
||||
"unit": "Megabytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_ContainerMemoryUtilized_min",
|
||||
"unit": "Megabytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_ContainerMemoryUtilized_sum",
|
||||
"unit": "Megabytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_ContainerNetworkRxBytes_count",
|
||||
"unit": "Bytes/Second",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_ContainerNetworkRxBytes_max",
|
||||
"unit": "Bytes/Second",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_ContainerNetworkRxBytes_min",
|
||||
"unit": "Bytes/Second",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_ContainerNetworkRxBytes_sum",
|
||||
"unit": "Bytes/Second",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_ContainerNetworkTxBytes_count",
|
||||
"unit": "Bytes/Second",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_ContainerNetworkTxBytes_max",
|
||||
"unit": "Bytes/Second",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_ContainerNetworkTxBytes_min",
|
||||
"unit": "Bytes/Second",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_ContainerNetworkTxBytes_sum",
|
||||
"unit": "Bytes/Second",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_ContainerStorageReadBytes_count",
|
||||
"unit": "Bytes/Second",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_ContainerStorageReadBytes_max",
|
||||
"unit": "Bytes/Second",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_ContainerStorageReadBytes_min",
|
||||
"unit": "Bytes/Second",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_ContainerStorageReadBytes_sum",
|
||||
"unit": "Bytes/Second",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_ContainerStorageWriteBytes_count",
|
||||
"unit": "Bytes/Second",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_ContainerStorageWriteBytes_max",
|
||||
"unit": "Bytes/Second",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_ContainerStorageWriteBytes_min",
|
||||
"unit": "Bytes/Second",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_ContainerStorageWriteBytes_sum",
|
||||
"unit": "Bytes/Second",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_CpuReserved_count",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_CpuReserved_max",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_CpuReserved_min",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_CpuReserved_sum",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_CpuUtilized_count",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_CpuUtilized_max",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_CpuUtilized_min",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_CpuUtilized_sum",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_DeploymentCount_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_DeploymentCount_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_DeploymentCount_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_DeploymentCount_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_DesiredTaskCount_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_DesiredTaskCount_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_DesiredTaskCount_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_DesiredTaskCount_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_EphemeralStorageReserved_count",
|
||||
"unit": "Gigabytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_EphemeralStorageReserved_max",
|
||||
"unit": "Gigabytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_EphemeralStorageReserved_min",
|
||||
"unit": "Gigabytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_EphemeralStorageReserved_sum",
|
||||
"unit": "Gigabytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_EphemeralStorageUtilized_count",
|
||||
"unit": "Gigabytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_EphemeralStorageUtilized_max",
|
||||
"unit": "Gigabytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_EphemeralStorageUtilized_min",
|
||||
"unit": "Gigabytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_EphemeralStorageUtilized_sum",
|
||||
"unit": "Gigabytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_MemoryReserved_count",
|
||||
"unit": "Megabytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_MemoryReserved_max",
|
||||
"unit": "Megabytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_MemoryReserved_min",
|
||||
"unit": "Megabytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_MemoryReserved_sum",
|
||||
"unit": "Megabytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_MemoryUtilized_count",
|
||||
"unit": "Megabytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_MemoryUtilized_max",
|
||||
"unit": "Megabytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_MemoryUtilized_min",
|
||||
"unit": "Megabytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_MemoryUtilized_sum",
|
||||
"unit": "Megabytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_NetworkRxBytes_count",
|
||||
"unit": "Bytes/Second",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_NetworkRxBytes_max",
|
||||
"unit": "Bytes/Second",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_NetworkRxBytes_min",
|
||||
"unit": "Bytes/Second",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_NetworkRxBytes_sum",
|
||||
"unit": "Bytes/Second",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_NetworkTxBytes_count",
|
||||
"unit": "Bytes/Second",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_NetworkTxBytes_max",
|
||||
"unit": "Bytes/Second",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_NetworkTxBytes_min",
|
||||
"unit": "Bytes/Second",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_NetworkTxBytes_sum",
|
||||
"unit": "Bytes/Second",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_PendingTaskCount_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_PendingTaskCount_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_PendingTaskCount_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_PendingTaskCount_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_RunningTaskCount_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_RunningTaskCount_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_RunningTaskCount_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_RunningTaskCount_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_ServiceCount_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_ServiceCount_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_ServiceCount_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_ServiceCount_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_StorageReadBytes_count",
|
||||
"unit": "Bytes/Second",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_StorageReadBytes_max",
|
||||
"unit": "Bytes/Second",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_StorageReadBytes_min",
|
||||
"unit": "Bytes/Second",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_StorageReadBytes_sum",
|
||||
"unit": "Bytes/Second",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_StorageWriteBytes_count",
|
||||
"unit": "Bytes/Second",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_StorageWriteBytes_max",
|
||||
"unit": "Bytes/Second",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_StorageWriteBytes_min",
|
||||
"unit": "Bytes/Second",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_StorageWriteBytes_sum",
|
||||
"unit": "Bytes/Second",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_TaskCount_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_TaskCount_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_TaskCount_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_TaskCount_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_TaskCpuUtilization_count",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_TaskCpuUtilization_max",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_TaskCpuUtilization_min",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_TaskCpuUtilization_sum",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_TaskEphemeralStorageUtilization_count",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_TaskEphemeralStorageUtilization_max",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_TaskEphemeralStorageUtilization_min",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_TaskEphemeralStorageUtilization_sum",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_TaskMemoryUtilization_count",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_TaskMemoryUtilization_max",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_TaskMemoryUtilization_min",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_TaskMemoryUtilization_sum",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_TaskSetCount_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_TaskSetCount_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_TaskSetCount_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_ContainerInsights_TaskSetCount_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_MemoryUtilization_count",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_MemoryUtilization_max",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_MemoryUtilization_min",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ECS_MemoryUtilization_sum",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
}
|
||||
],
|
||||
"logs": [
|
||||
{
|
||||
"name": "Account ID",
|
||||
"path": "resources.cloud.account.id",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "Log Group Name",
|
||||
"path": "resources.aws.cloudwatch.log_group_name",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "Log Stream Name",
|
||||
"path": "resources.aws.cloudwatch.log_stream_name",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"telemetryCollectionStrategy": {
|
||||
"aws": {
|
||||
"metrics": {
|
||||
"cloudwatchMetricStreamFilters": [
|
||||
{
|
||||
"Namespace": "AWS/ECS"
|
||||
},
|
||||
{
|
||||
"Namespace": "ECS/ContainerInsights"
|
||||
}
|
||||
]
|
||||
},
|
||||
"logs": {
|
||||
"cloudwatchLogsSubscriptions": [
|
||||
{
|
||||
"logGroupNamePrefix": "/ecs",
|
||||
"filterPattern": ""
|
||||
},
|
||||
{
|
||||
"logGroupNamePrefix": "/aws/ecs/containerinsights",
|
||||
"filterPattern": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"assets": {
|
||||
"dashboards": [
|
||||
{
|
||||
"id": "overview",
|
||||
"title": "AWS ECS Overview",
|
||||
"description": "Overview of ECS",
|
||||
"definition": "file://assets/dashboards/overview.json"
|
||||
},
|
||||
{
|
||||
"id": "containerinsights",
|
||||
"title": "ECS ContainerInsights",
|
||||
"description": "Overview of ECS ContainerInsights",
|
||||
"definition": "file://assets/dashboards/containerinsights.json"
|
||||
},
|
||||
{
|
||||
"id": "enhanced_containerinsights",
|
||||
"title": "ECS Enhanced ContainerInsights",
|
||||
"description": "Enhanced ECS ContainerInsights",
|
||||
"definition": "file://assets/dashboards/enhanced_containerinsights.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
### Monitor Elastic Container Service with SigNoz
|
||||
|
||||
Collect ECS Logs and key Metrics and view them with an out of the box dashboard.
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user