Compare commits

..

36 Commits

Author SHA1 Message Date
Swapnil Nakade
e00dcd8526 refactor: streamline metrics and logs connection status checks 2026-02-18 14:02:03 +05:30
Swapnil Nakade
3d3aa9d977 Merge branch 'main' into feat/azure-integration-ref 2026-02-18 13:37:00 +05:30
Swapnil Nakade
9068cddbe0 feat: add ingestion status check for metrics and logs in Azure integration 2026-02-18 13:25:40 +05:30
Swapnil Nakade
73453ec8c6 fix: fix svc dashboard logic 2026-02-16 17:22:55 +05:30
Swapnil Nakade
b333e44d13 Merge branch 'main' into feat/azure-integration-ref 2026-02-16 16:06:24 +05:30
Swapnil Nakade
5bd51df854 refactor: status check 2026-02-16 15:57:44 +05:30
Swapnil Nakade
989ca522f8 refactor: updating azure telemetry collections strat 2026-02-12 14:54:24 +05:30
Swapnil Nakade
9a2e9d76b5 feat: updating service definitions 2026-02-10 20:11:40 +05:30
Swapnil Nakade
2be42deecd Merge branch 'main' into feat/azure-integration 2026-02-10 18:44:40 +05:30
Swapnil Nakade
95cad880cc feat: adding default values for azure service config 2026-02-10 18:21:26 +05:30
Swapnil Nakade
cfef1091b3 Merge branch 'main' into feat/azure-integration 2026-02-09 15:38:17 +05:30
Swapnil Nakade
4504c364f2 feat: adding comments 2026-02-09 15:37:10 +05:30
Swapnil Nakade
1a006870e1 refactor: removing wrong deps 2026-02-09 15:24:02 +05:30
Swapnil Nakade
e7a27a1cfb Merge branch 'main' into feat/azure-integration 2026-02-09 15:19:36 +05:30
Swapnil Nakade
1e7323ead2 refactor: updating response for conn status 2026-02-08 22:56:04 +05:30
Swapnil Nakade
af4c6c5b52 Merge branch 'main' into feat/azure-integration 2026-02-08 18:24:47 +05:30
Swapnil Nakade
02262ba245 refactor: wip 2026-02-08 17:28:04 +05:30
Swapnil Nakade
df7c9e1339 chore: wip 2026-02-08 00:09:29 +05:30
Swapnil Nakade
ac5e52479f refactor: removing comment 2026-02-06 17:48:49 +05:30
Swapnil Nakade
de56477bbb Merge branch 'main' into feat/azure-integration 2026-02-06 17:45:40 +05:30
Swapnil Nakade
fddd8a27fa fix: aws connection url generation 2026-02-06 17:44:06 +05:30
Swapnil Nakade
2aa4f8e237 feat: adding logs 2026-02-06 11:57:23 +05:30
Swapnil Nakade
74006a214b ci: fixing lint ci issues 2026-02-06 04:01:39 +05:30
Swapnil Nakade
ed2cbacadc Merge branch 'main' into feat/azure-integration 2026-02-06 03:55:33 +05:30
Swapnil Nakade
3cbd529843 refactor: reverting 2026-02-06 03:51:29 +05:30
Swapnil Nakade
78b481e895 refactor: cloud integration API cleanup 2026-02-06 03:42:25 +05:30
Swapnil Nakade
215098ec0d refactor: sorting services list for consistency 2026-02-03 13:59:26 +05:30
Swapnil Nakade
5a4ef2e4ce refactor: code beautification 2026-02-03 01:16:58 +05:30
Swapnil Nakade
b1f33c4f7f refactor: updating service details api 2026-02-03 00:52:27 +05:30
Swapnil Nakade
713c84b1e4 refactor: updating cloud provider type 2026-02-03 00:10:18 +05:30
Swapnil Nakade
c3daf9e428 Merge branch 'main' into feat/azure-integration 2026-02-02 23:37:45 +05:30
Swapnil Nakade
70a908deb1 refactor: updating cloud-integration controller 2026-02-02 23:36:46 +05:30
Swapnil Nakade
cc9cdded3c refactor: updating cloud integration apis 2026-02-02 21:50:15 +05:30
Swapnil Nakade
77067cd614 feat: extending cloud integration apis 2026-01-29 12:20:19 +05:30
Swapnil Nakade
ab703d9a65 Merge branch 'main' into feat/azure-integration 2026-01-28 18:28:16 +05:30
Swapnil Nakade
611e8fbf9e refactor: updating azure cloud integrations api 2026-01-28 11:18:58 +05:30
141 changed files with 14696 additions and 13379 deletions

7
.gitignore vendored
View File

@@ -1,11 +1,8 @@
node_modules
# editor
.vscode
!.vscode/settings.json
.zed
.idea
deploy/docker/environment_tiny/common_test
frontend/node_modules
@@ -34,6 +31,8 @@ frontend/yarn-debug.log*
frontend/yarn-error.log*
frontend/src/constants/env.ts
.idea
**/build
**/storage
**/locust-scripts/__pycache__/
@@ -230,3 +229,5 @@ cython_debug/
pyrightconfig.json
# cursor files
frontend/.cursor/

View File

@@ -176,7 +176,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.112.0
image: signoz/signoz:v0.111.0
command:
- --config=/root/config/prometheus.yml
ports:

View File

@@ -117,7 +117,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.112.0
image: signoz/signoz:v0.111.0
command:
- --config=/root/config/prometheus.yml
ports:

View File

@@ -179,7 +179,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.112.0}
image: signoz/signoz:${VERSION:-v0.111.0}
container_name: signoz
command:
- --config=/root/config/prometheus.yml

View File

@@ -111,7 +111,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.112.0}
image: signoz/signoz:${VERSION:-v0.111.0}
container_name: signoz
command:
- --config=/root/config/prometheus.yml

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
package api
import (
"log/slog"
"net/http"
"net/http/httputil"
"time"
@@ -10,11 +11,9 @@ import (
"github.com/SigNoz/signoz/ee/query-service/usage"
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/global"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/http/middleware"
querierAPI "github.com/SigNoz/signoz/pkg/querier"
baseapp "github.com/SigNoz/signoz/pkg/query-service/app"
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations"
"github.com/SigNoz/signoz/pkg/query-service/app/integrations"
"github.com/SigNoz/signoz/pkg/query-service/app/logparsingpipeline"
"github.com/SigNoz/signoz/pkg/query-service/interfaces"
@@ -22,6 +21,7 @@ import (
rules "github.com/SigNoz/signoz/pkg/query-service/rules"
"github.com/SigNoz/signoz/pkg/queryparser"
"github.com/SigNoz/signoz/pkg/signoz"
"github.com/SigNoz/signoz/pkg/types/integrationstypes"
"github.com/SigNoz/signoz/pkg/version"
"github.com/gorilla/mux"
)
@@ -31,13 +31,14 @@ type APIHandlerOptions struct {
RulesManager *rules.Manager
UsageManager *usage.Manager
IntegrationsController *integrations.Controller
CloudIntegrationsController *cloudintegrations.Controller
CloudIntegrationsRegistry map[integrationstypes.CloudProviderType]integrationstypes.CloudProvider
LogsParsingPipelineController *logparsingpipeline.LogParsingPipelineController
Gateway *httputil.ReverseProxy
GatewayUrl string
// Querier Influx Interval
FluxInterval time.Duration
GlobalConfig global.Config
Logger *slog.Logger // this is present in Signoz.Instrumentation but adding for quick access
}
type APIHandler struct {
@@ -51,7 +52,7 @@ func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz, config signoz.
Reader: opts.DataConnector,
RuleManager: opts.RulesManager,
IntegrationsController: opts.IntegrationsController,
CloudIntegrationsController: opts.CloudIntegrationsController,
CloudIntegrationsRegistry: opts.CloudIntegrationsRegistry,
LogsParsingPipelineController: opts.LogsParsingPipelineController,
FluxInterval: opts.FluxInterval,
AlertmanagerAPI: alertmanager.NewAPI(signoz.Alertmanager),
@@ -107,10 +108,7 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
router.HandleFunc("/api/v4/query_range", am.ViewAccess(ah.queryRangeV4)).Methods(http.MethodPost)
// v5
router.Handle("/api/v5/query_range", handler.New(
am.ViewAccess(ah.queryRangeV5),
querierAPI.QueryRangeV5OpenAPIDef,
)).Methods(http.MethodPost)
router.HandleFunc("/api/v5/query_range", am.ViewAccess(ah.queryRangeV5)).Methods(http.MethodPost)
router.HandleFunc("/api/v5/substitute_vars", am.ViewAccess(ah.QuerierAPI.ReplaceVariables)).Methods(http.MethodPost)
@@ -122,14 +120,12 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
}
func (ah *APIHandler) RegisterCloudIntegrationsRoutes(router *mux.Router, am *middleware.AuthZ) {
ah.APIHandler.RegisterCloudIntegrationsRoutes(router, am)
router.HandleFunc(
"/api/v1/cloud-integrations/{cloudProvider}/accounts/generate-connection-params",
am.EditAccess(ah.CloudIntegrationsGenerateConnectionParams),
).Methods(http.MethodGet)
}
func (ah *APIHandler) getVersion(w http.ResponseWriter, r *http.Request) {

View File

@@ -6,6 +6,7 @@ import (
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"strings"
"time"
@@ -13,20 +14,14 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/modules/user"
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/integrationstypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux"
"go.uber.org/zap"
)
type CloudIntegrationConnectionParamsResponse struct {
IngestionUrl string `json:"ingestion_url,omitempty"`
IngestionKey string `json:"ingestion_key,omitempty"`
SigNozAPIUrl string `json:"signoz_api_url,omitempty"`
SigNozAPIKey string `json:"signoz_api_key,omitempty"`
}
// TODO: move this file with other cloud integration related code
func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseWriter, r *http.Request) {
claims, err := authtypes.ClaimsFromContext(r.Context())
@@ -41,23 +36,21 @@ func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseW
return
}
cloudProvider := mux.Vars(r)["cloudProvider"]
if cloudProvider != "aws" {
RespondError(w, basemodel.BadRequest(fmt.Errorf(
"cloud provider not supported: %s", cloudProvider,
)), nil)
cloudProviderString := mux.Vars(r)["cloudProvider"]
cloudProvider, err := integrationstypes.NewCloudProvider(cloudProviderString)
if err != nil {
render.Error(w, err)
return
}
apiKey, apiErr := ah.getOrCreateCloudIntegrationPAT(r.Context(), claims.OrgID, cloudProvider)
if apiErr != nil {
RespondError(w, basemodel.WrapApiError(
apiErr, "couldn't provision PAT for cloud integration:",
), nil)
apiKey, err := ah.getOrCreateCloudIntegrationPAT(r.Context(), claims.OrgID, cloudProvider)
if err != nil {
render.Error(w, err)
return
}
result := CloudIntegrationConnectionParamsResponse{
result := integrationstypes.GettableCloudIntegrationConnectionParams{
SigNozAPIKey: apiKey,
}
@@ -71,16 +64,17 @@ func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseW
// Return the API Key (PAT) even if the rest of the params can not be deduced.
// Params not returned from here will be requested from the user via form inputs.
// This enables gracefully degraded but working experience even for non-cloud deployments.
zap.L().Info("ingestion params and signoz api url can not be deduced since no license was found")
ah.Respond(w, result)
ah.opts.Logger.InfoContext(
r.Context(),
"ingestion params and signoz api url can not be deduced since no license was found",
)
render.Success(w, http.StatusOK, result)
return
}
signozApiUrl, apiErr := ah.getIngestionUrlAndSigNozAPIUrl(r.Context(), license.Key)
if apiErr != nil {
RespondError(w, basemodel.WrapApiError(
apiErr, "couldn't deduce ingestion url and signoz api url",
), nil)
signozApiUrl, err := ah.getIngestionUrlAndSigNozAPIUrl(r.Context(), license.Key)
if err != nil {
render.Error(w, err)
return
}
@@ -89,48 +83,41 @@ func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseW
gatewayUrl := ah.opts.GatewayUrl
if len(gatewayUrl) > 0 {
ingestionKey, apiErr := getOrCreateCloudProviderIngestionKey(
ingestionKeyString, err := ah.getOrCreateCloudProviderIngestionKey(
r.Context(), gatewayUrl, license.Key, cloudProvider,
)
if apiErr != nil {
RespondError(w, basemodel.WrapApiError(
apiErr, "couldn't get or create ingestion key",
), nil)
if err != nil {
render.Error(w, err)
return
}
result.IngestionKey = ingestionKey
result.IngestionKey = ingestionKeyString
} else {
zap.L().Info("ingestion key can't be deduced since no gateway url has been configured")
ah.opts.Logger.InfoContext(
r.Context(),
"ingestion key can't be deduced since no gateway url has been configured",
)
}
ah.Respond(w, result)
render.Success(w, http.StatusOK, result)
}
func (ah *APIHandler) getOrCreateCloudIntegrationPAT(ctx context.Context, orgId string, cloudProvider string) (
string, *basemodel.ApiError,
) {
func (ah *APIHandler) getOrCreateCloudIntegrationPAT(ctx context.Context, orgId string, cloudProvider valuer.String) (string, error) {
integrationPATName := fmt.Sprintf("%s integration", cloudProvider)
integrationUser, apiErr := ah.getOrCreateCloudIntegrationUser(ctx, orgId, cloudProvider)
if apiErr != nil {
return "", apiErr
integrationUser, err := ah.getOrCreateCloudIntegrationUser(ctx, orgId, cloudProvider)
if err != nil {
return "", err
}
orgIdUUID, err := valuer.NewUUID(orgId)
if err != nil {
return "", basemodel.InternalError(fmt.Errorf(
"couldn't parse orgId: %w", err,
))
return "", err
}
allPats, err := ah.Signoz.Modules.User.ListAPIKeys(ctx, orgIdUUID)
if err != nil {
return "", basemodel.InternalError(fmt.Errorf(
"couldn't list PATs: %w", err,
))
return "", err
}
for _, p := range allPats {
if p.UserID == integrationUser.ID && p.Name == integrationPATName {
@@ -138,9 +125,10 @@ func (ah *APIHandler) getOrCreateCloudIntegrationPAT(ctx context.Context, orgId
}
}
zap.L().Info(
ah.opts.Logger.InfoContext(
ctx,
"no PAT found for cloud integration, creating a new one",
zap.String("cloudProvider", cloudProvider),
slog.String("cloudProvider", cloudProvider.String()),
)
newPAT, err := types.NewStorableAPIKey(
@@ -150,68 +138,48 @@ func (ah *APIHandler) getOrCreateCloudIntegrationPAT(ctx context.Context, orgId
0,
)
if err != nil {
return "", basemodel.InternalError(fmt.Errorf(
"couldn't create cloud integration PAT: %w", err,
))
return "", err
}
err = ah.Signoz.Modules.User.CreateAPIKey(ctx, newPAT)
if err != nil {
return "", basemodel.InternalError(fmt.Errorf(
"couldn't create cloud integration PAT: %w", err,
))
return "", err
}
return newPAT.Token, nil
}
func (ah *APIHandler) getOrCreateCloudIntegrationUser(
ctx context.Context, orgId string, cloudProvider string,
) (*types.User, *basemodel.ApiError) {
cloudIntegrationUserName := fmt.Sprintf("%s-integration", cloudProvider)
// TODO: move this function out of handler and use proper module structure
func (ah *APIHandler) getOrCreateCloudIntegrationUser(ctx context.Context, orgId string, cloudProvider valuer.String) (*types.User, error) {
cloudIntegrationUserName := fmt.Sprintf("%s-integration", cloudProvider.String())
email := valuer.MustNewEmail(fmt.Sprintf("%s@signoz.io", cloudIntegrationUserName))
cloudIntegrationUser, err := types.NewUser(cloudIntegrationUserName, email, types.RoleViewer, valuer.MustNewUUID(orgId))
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf("couldn't create cloud integration user: %w", err))
return nil, err
}
password := types.MustGenerateFactorPassword(cloudIntegrationUser.ID.StringValue())
cloudIntegrationUser, err = ah.Signoz.Modules.User.GetOrCreateUser(ctx, cloudIntegrationUser, user.WithFactorPassword(password))
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf("couldn't look for integration user: %w", err))
return nil, err
}
return cloudIntegrationUser, nil
}
func (ah *APIHandler) getIngestionUrlAndSigNozAPIUrl(ctx context.Context, licenseKey string) (
string, *basemodel.ApiError,
) {
// TODO: remove this struct from here
type deploymentResponse struct {
Name string `json:"name"`
ClusterInfo struct {
Region struct {
DNS string `json:"dns"`
} `json:"region"`
} `json:"cluster"`
}
// TODO: move this function out of handler and use proper module structure
func (ah *APIHandler) getIngestionUrlAndSigNozAPIUrl(ctx context.Context, licenseKey string) (string, error) {
respBytes, err := ah.Signoz.Zeus.GetDeployment(ctx, licenseKey)
if err != nil {
return "", basemodel.InternalError(fmt.Errorf(
"couldn't query for deployment info: error: %w", err,
))
return "", errors.WrapInternalf(err, errors.CodeInternal, "couldn't query for deployment info: error")
}
resp := new(deploymentResponse)
resp := new(integrationstypes.GettableDeployment)
err = json.Unmarshal(respBytes, resp)
if err != nil {
return "", basemodel.InternalError(fmt.Errorf(
"couldn't unmarshal deployment info response: error: %w", err,
))
return "", errors.WrapInternalf(err, errors.CodeInternal, "couldn't unmarshal deployment info response")
}
regionDns := resp.ClusterInfo.Region.DNS
@@ -219,9 +187,11 @@ func (ah *APIHandler) getIngestionUrlAndSigNozAPIUrl(ctx context.Context, licens
if len(regionDns) < 1 || len(deploymentName) < 1 {
// Fail early if actual response structure and expectation here ever diverge
return "", basemodel.InternalError(fmt.Errorf(
return "", errors.WrapInternalf(
err,
errors.CodeInternal,
"deployment info response not in expected shape. couldn't determine region dns and deployment name",
))
)
}
signozApiUrl := fmt.Sprintf("https://%s.%s", deploymentName, regionDns)
@@ -229,102 +199,85 @@ func (ah *APIHandler) getIngestionUrlAndSigNozAPIUrl(ctx context.Context, licens
return signozApiUrl, nil
}
type ingestionKey struct {
Name string `json:"name"`
Value string `json:"value"`
// other attributes from gateway response not included here since they are not being used.
}
type ingestionKeysSearchResponse struct {
Status string `json:"status"`
Data []ingestionKey `json:"data"`
Error string `json:"error"`
}
type createIngestionKeyResponse struct {
Status string `json:"status"`
Data ingestionKey `json:"data"`
Error string `json:"error"`
}
func getOrCreateCloudProviderIngestionKey(
ctx context.Context, gatewayUrl string, licenseKey string, cloudProvider string,
) (string, *basemodel.ApiError) {
func (ah *APIHandler) getOrCreateCloudProviderIngestionKey(
ctx context.Context, gatewayUrl string, licenseKey string, cloudProvider valuer.String,
) (string, error) {
cloudProviderKeyName := fmt.Sprintf("%s-integration", cloudProvider)
// see if the key already exists
searchResult, apiErr := requestGateway[ingestionKeysSearchResponse](
searchResult, err := requestGateway[integrationstypes.GettableIngestionKeysSearch](
ctx,
gatewayUrl,
licenseKey,
fmt.Sprintf("/v1/workspaces/me/keys/search?name=%s", cloudProviderKeyName),
nil,
ah.opts.Logger,
)
if apiErr != nil {
return "", basemodel.WrapApiError(
apiErr, "couldn't search for cloudprovider ingestion key",
)
if err != nil {
return "", err
}
if searchResult.Status != "success" {
return "", basemodel.InternalError(fmt.Errorf(
"couldn't search for cloudprovider ingestion key: status: %s, error: %s",
return "", errors.NewInternalf(
errors.CodeInternal,
"couldn't search for cloud provider ingestion key: status: %s, error: %s",
searchResult.Status, searchResult.Error,
))
}
for _, k := range searchResult.Data {
if k.Name == cloudProviderKeyName {
if len(k.Value) < 1 {
// Fail early if actual response structure and expectation here ever diverge
return "", basemodel.InternalError(fmt.Errorf(
"ingestion keys search response not as expected",
))
}
return k.Value, nil
}
}
zap.L().Info(
"no existing ingestion key found for cloud integration, creating a new one",
zap.String("cloudProvider", cloudProvider),
)
createKeyResult, apiErr := requestGateway[createIngestionKeyResponse](
ctx, gatewayUrl, licenseKey, "/v1/workspaces/me/keys",
map[string]any{
"name": cloudProviderKeyName,
"tags": []string{"integration", cloudProvider},
},
)
if apiErr != nil {
return "", basemodel.WrapApiError(
apiErr, "couldn't create cloudprovider ingestion key",
)
}
for _, k := range searchResult.Data {
if k.Name != cloudProviderKeyName {
continue
}
if len(k.Value) < 1 {
// Fail early if actual response structure and expectation here ever diverge
return "", errors.NewInternalf(errors.CodeInternal, "ingestion keys search response not as expected")
}
return k.Value, nil
}
ah.opts.Logger.InfoContext(
ctx,
"no existing ingestion key found for cloud integration, creating a new one",
slog.String("cloudProvider", cloudProvider.String()),
)
createKeyResult, err := requestGateway[integrationstypes.GettableCreateIngestionKey](
ctx, gatewayUrl, licenseKey, "/v1/workspaces/me/keys",
map[string]any{
"name": cloudProviderKeyName,
"tags": []string{"integration", cloudProvider.String()},
},
ah.opts.Logger,
)
if err != nil {
return "", err
}
if createKeyResult.Status != "success" {
return "", basemodel.InternalError(fmt.Errorf(
"couldn't create cloudprovider ingestion key: status: %s, error: %s",
return "", errors.NewInternalf(
errors.CodeInternal,
"couldn't create cloud provider ingestion key: status: %s, error: %s",
createKeyResult.Status, createKeyResult.Error,
))
)
}
ingestionKey := createKeyResult.Data.Value
if len(ingestionKey) < 1 {
ingestionKeyString := createKeyResult.Data.Value
if len(ingestionKeyString) < 1 {
// Fail early if actual response structure and expectation here ever diverge
return "", basemodel.InternalError(fmt.Errorf(
return "", errors.NewInternalf(errors.CodeInternal,
"ingestion key creation response not as expected",
))
)
}
return ingestionKey, nil
return ingestionKeyString, nil
}
func requestGateway[ResponseType any](
ctx context.Context, gatewayUrl string, licenseKey string, path string, payload any,
) (*ResponseType, *basemodel.ApiError) {
ctx context.Context, gatewayUrl, licenseKey, path string, payload any, logger *slog.Logger,
) (*ResponseType, error) {
baseUrl := strings.TrimSuffix(gatewayUrl, "/")
reqUrl := fmt.Sprintf("%s%s", baseUrl, path)
@@ -335,13 +288,12 @@ func requestGateway[ResponseType any](
"X-Consumer-Groups": "ns:default",
}
return requestAndParseResponse[ResponseType](ctx, reqUrl, headers, payload)
return requestAndParseResponse[ResponseType](ctx, reqUrl, headers, payload, logger)
}
func requestAndParseResponse[ResponseType any](
ctx context.Context, url string, headers map[string]string, payload any,
) (*ResponseType, *basemodel.ApiError) {
ctx context.Context, url string, headers map[string]string, payload any, logger *slog.Logger,
) (*ResponseType, error) {
reqMethod := http.MethodGet
var reqBody io.Reader
if payload != nil {
@@ -349,18 +301,14 @@ func requestAndParseResponse[ResponseType any](
bodyJson, err := json.Marshal(payload)
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf(
"couldn't serialize request payload to JSON: %w", err,
))
return nil, errors.WrapInternalf(err, errors.CodeInternal, "couldn't marshal payload")
}
reqBody = bytes.NewBuffer([]byte(bodyJson))
reqBody = bytes.NewBuffer(bodyJson)
}
req, err := http.NewRequestWithContext(ctx, reqMethod, url, reqBody)
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf(
"couldn't prepare request: %w", err,
))
return nil, errors.WrapInternalf(err, errors.CodeInternal, "couldn't create req")
}
for k, v := range headers {
@@ -373,23 +321,26 @@ func requestAndParseResponse[ResponseType any](
response, err := client.Do(req)
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf("couldn't make request: %w", err))
return nil, errors.WrapInternalf(err, errors.CodeInternal, "couldn't make req")
}
defer response.Body.Close()
defer func() {
err = response.Body.Close()
if err != nil {
logger.ErrorContext(ctx, "couldn't close response body", "error", err)
}
}()
respBody, err := io.ReadAll(response.Body)
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf("couldn't read response: %w", err))
return nil, errors.WrapInternalf(err, errors.CodeInternal, "couldn't read response body")
}
var resp ResponseType
err = json.Unmarshal(respBody, &resp)
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf(
"couldn't unmarshal gateway response into %T", resp,
))
return nil, errors.WrapInternalf(err, errors.CodeInternal, "couldn't unmarshal response body")
}
return &resp, nil

View File

@@ -127,12 +127,7 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
)
}
cloudIntegrationsController, err := cloudintegrations.NewController(signoz.SQLStore)
if err != nil {
return nil, fmt.Errorf(
"couldn't create cloud provider integrations controller: %w", err,
)
}
cloudIntegrationsRegistry := cloudintegrations.NewCloudProviderRegistry(signoz.Instrumentation.Logger(), signoz.SQLStore, signoz.Querier)
// ingestion pipelines manager
logParsingPipelineController, err := logparsingpipeline.NewLogParsingPipelinesController(
@@ -167,12 +162,13 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
RulesManager: rm,
UsageManager: usageManager,
IntegrationsController: integrationsController,
CloudIntegrationsController: cloudIntegrationsController,
CloudIntegrationsRegistry: cloudIntegrationsRegistry,
LogsParsingPipelineController: logParsingPipelineController,
FluxInterval: config.Querier.FluxInterval,
Gateway: gatewayProxy,
GatewayUrl: config.Gateway.URL.String(),
GlobalConfig: config.Global,
Logger: signoz.Instrumentation.Logger(),
}
apiHandler, err := api.NewAPIHandler(apiOpts, signoz, config)

View File

@@ -1,74 +0,0 @@
---
description: Core testing conventions - imports, rendering, MSW, interactions, queries
globs: **/*.test.{ts,tsx}
alwaysApply: false
---
# Testing Conventions
## Imports
Always import from the test harness, never directly from `@testing-library/react`:
```ts
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import { server, rest } from 'mocks-server/server';
```
## Router
Use the built-in router in `render`:
```ts
render(<Page />, undefined, { initialRoute: '/traces-explorer' });
```
Only mock `useLocation` / `useParams` if the test depends on their values.
## MSW
Global MSW server runs automatically. Override per-test:
```ts
server.use(
rest.get('*/api/v1/foo', (_req, res, ctx) =>
res(ctx.status(200), ctx.json({ ok: true })))
);
```
Keep large response fixtures in `mocks-server/__mockdata_`.
## Interactions
- Prefer `userEvent` for real user interactions (click, type, select, tab).
- Use `fireEvent` only for low-level events not covered by `userEvent` (e.g., scroll, resize). Wrap in `act(...)` if needed.
- Always `await` interactions:
```ts
const user = userEvent.setup({ pointerEventsCheck: 0 });
await user.click(screen.getByRole('button', { name: /save/i }));
```
## Timers
No global fake timers. Per-test only, for debounce/throttle:
```ts
jest.useFakeTimers();
const user = userEvent.setup({ advanceTimers: (ms) => jest.advanceTimersByTime(ms) });
await user.type(screen.getByRole('textbox'), 'query');
jest.advanceTimersByTime(400);
jest.useRealTimers();
```
## Queries
Prefer accessible queries: `getByRole` > `findByRole` > `getByLabelText` > visible text > `data-testid` (last resort).
## Anti-patterns
- Never import from `@testing-library/react` directly
- Never use global fake timers
- Never wrap `render` in `act(...)`
- Never mock infra dependencies locally (router, react-query)
- Limit to 3-5 focused tests per file

View File

@@ -1,54 +0,0 @@
---
description: When to use global vs local mocks in tests
globs: **/*.test.{ts,tsx}
alwaysApply: false
---
# Mock Strategy
## Use Global Mocks For
High-frequency dependencies (20+ test files):
- Core infrastructure: react-router-dom, react-query, antd
- Browser APIs: ResizeObserver, matchMedia, localStorage
- Utility libraries: date-fns, lodash
Available global mock files (from jest.config.ts):
- `uplot` -> `__mocks__/uplotMock.ts`
## Use Local Mocks For
- Business logic dependencies (API endpoints, custom hooks, domain components)
- Test-specific behavior (different data per test, error scenarios, loading states)
## Decision Tree
```
Used in 20+ test files?
YES -> Global mock
NO -> Business logic or test-specific?
YES -> Local mock
NO -> Consider global if usage grows
```
## Correct Usage
```ts
// Global mocks are already available - just import
import { useLocation } from 'react-router-dom';
// Local mocks for business logic
jest.mock('../api/tracesService', () => ({
getTraces: jest.fn(() => mockTracesData),
}));
```
## Anti-patterns
```ts
// Never re-mock globally mocked dependencies locally
jest.mock('react-router-dom', () => ({ ... }));
// Never put test-specific data in global mocks
jest.mock('../api/tracesService', () => ({ getTraces: jest.fn(() => specificTestData) }));
```

View File

@@ -1,54 +0,0 @@
---
description: TypeScript type safety requirements for Jest tests
globs: **/*.test.{ts,tsx}
alwaysApply: false
---
# TypeScript Type Safety in Tests
All Jest tests must be fully type-safe. Never use `any`.
## Mock Function Typing
```ts
// Use jest.mocked for module mocks
import useFoo from 'hooks/useFoo';
jest.mock('hooks/useFoo');
const mockUseFoo = jest.mocked(useFoo);
// Use jest.MockedFunction for standalone mocks
const mockFetch = jest.fn() as jest.MockedFunction<(id: number) => Promise<User>>;
```
## Mock Data
Define interfaces for all mock data:
```ts
const mockUser: User = { id: 1, name: 'John', email: 'john@example.com' };
const mockProps: ComponentProps = {
title: 'Test',
data: [mockUser],
onSelect: jest.fn() as jest.MockedFunction<(user: User) => void>,
};
```
## Hook Mocking Pattern
```ts
import useFoo from 'hooks/useFoo';
jest.mock('hooks/useFoo');
const mockUseFoo = jest.mocked(useFoo);
mockUseFoo.mockReturnValue(/* minimal shape */);
```
Prefer helpers (`rqSuccess`, `rqLoading`, `rqError`) for React Query results.
## Checklist
- All mock functions use `jest.MockedFunction<T>` or `jest.mocked()`
- All mock data has proper interfaces
- No `any` types in test files
- Component props are typed
- API response types are defined

484
frontend/.cursorrules Normal file
View File

@@ -0,0 +1,484 @@
# Persona
You are an expert developer with deep knowledge of Jest, React Testing Library, MSW, and TypeScript, tasked with creating unit tests for this repository.
# Auto-detect TypeScript Usage
Check for TypeScript in the project through tsconfig.json or package.json dependencies.
Adjust syntax based on this detection.
# TypeScript Type Safety for Jest Tests
**CRITICAL**: All Jest tests MUST be fully type-safe with proper TypeScript types.
**Type Safety Requirements:**
- Use proper TypeScript interfaces for all mock data
- Type all Jest mock functions with `jest.MockedFunction<T>`
- Use generic types for React components and hooks
- Define proper return types for mock functions
- Use `as const` for literal types when needed
- Avoid `any` type use proper typing instead
# Unit Testing Focus
Focus on critical functionality (business logic, utility functions, component behavior)
Mock dependencies (API calls, external modules) before imports
Test multiple data scenarios (valid inputs, invalid inputs, edge cases)
Write maintainable tests with descriptive names grouped in describe blocks
# Global vs Local Mocks
**Use Global Mocks for:**
- High-frequency dependencies (20+ test files)
- Core infrastructure (react-router-dom, react-query, antd)
- Standard implementations across the app
- Browser APIs (ResizeObserver, matchMedia, localStorage)
- Utility libraries (date-fns, lodash)
**Use Local Mocks for:**
- Business logic dependencies (5-15 test files)
- Test-specific behavior (different data per test)
- API endpoints with specific responses
- Domain-specific components
- Error scenarios and edge cases
**Global Mock Files Available (from jest.config.ts):**
- `uplot` → `__mocks__/uplotMock.ts`
# Repo-specific Testing Conventions
## Imports
Always import from our harness:
```ts
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
```
For API mocks:
```ts
import { server, rest } from 'mocks-server/server';
```
Do not import directly from `@testing-library/react`.
## Router
Use the router built into render:
```ts
render(<Page />, undefined, { initialRoute: '/traces-explorer' });
```
Only mock `useLocation` / `useParams` if the test depends on them.
## Hook Mocks
Pattern:
```ts
import useFoo from 'hooks/useFoo';
jest.mock('hooks/useFoo');
const mockUseFoo = jest.mocked(useFoo);
mockUseFoo.mockReturnValue(/* minimal shape */ as any);
```
Prefer helpers (`rqSuccess`, `rqLoading`, `rqError`) for React Query results.
## MSW
Global MSW server runs automatically.
Override per-test:
```ts
server.use(
rest.get('*/api/v1/foo', (_req, res, ctx) => res(ctx.status(200), ctx.json({ ok: true })))
);
```
Keep large responses in `mocks-server/__mockdata_`.
## Interactions
- Prefer `userEvent` for real user interactions (click, type, select, tab).
- Use `fireEvent` only for low-level/programmatic events not covered by `userEvent` (e.g., scroll, resize, setting `element.scrollTop` for virtualization). Wrap in `act(...)` if needed.
- Always await interactions:
```ts
const user = userEvent.setup({ pointerEventsCheck: 0 });
await user.click(screen.getByRole('button', { name: /save/i }));
```
```ts
// Example: virtualized list scroll (no userEvent helper)
const scroller = container.querySelector('[data-test-id="virtuoso-scroller"]') as HTMLElement;
scroller.scrollTop = targetScrollTop;
act(() => { fireEvent.scroll(scroller); });
```
## Timers
❌ No global fake timers.
✅ Per-test only, for debounce/throttle:
```ts
jest.useFakeTimers();
const user = userEvent.setup({ advanceTimers: (ms) => jest.advanceTimersByTime(ms) });
await user.type(screen.getByRole('textbox'), 'query');
jest.advanceTimersByTime(400);
jest.useRealTimers();
```
## Queries
Prefer accessible queries (`getByRole`, `findByRole`, `getByLabelText`).
Fallback: visible text.
Last resort: `data-testid`.
# Example Test (using only configured global mocks)
```ts
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import { server, rest } from 'mocks-server/server';
import MyComponent from '../MyComponent';
describe('MyComponent', () => {
it('renders and interacts', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
server.use(
rest.get('*/api/v1/example', (_req, res, ctx) => res(ctx.status(200), ctx.json({ value: 42 })))
);
render(<MyComponent />, undefined, { initialRoute: '/foo' });
expect(await screen.findByText(/value: 42/i)).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: /refresh/i }));
await waitFor(() => expect(screen.getByText(/loading/i)).toBeInTheDocument());
});
});
```
# Anti-patterns
❌ Importing RTL directly
❌ Using global fake timers
❌ Wrapping render in `act(...)`
❌ Mocking infra dependencies locally (router, react-query)
✅ Use our harness (`tests/test-utils`)
✅ Use MSW for API overrides
✅ Use userEvent + await
✅ Pin time only in tests that assert relative dates
# Best Practices
- **Critical Functionality**: Prioritize testing business logic and utilities
- **Dependency Mocking**: Global mocks for infra, local mocks for business logic
- **Data Scenarios**: Always test valid, invalid, and edge cases
- **Descriptive Names**: Make test intent clear
- **Organization**: Group related tests in describe
- **Consistency**: Match repo conventions
- **Edge Cases**: Test null, undefined, unexpected values
- **Limit Scope**: 35 focused tests per file
- **Use Helpers**: `rqSuccess`, `makeUser`, etc.
- **No Any**: Enforce type safety
# Example Test
```ts
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import { server, rest } from 'mocks-server/server';
import MyComponent from '../MyComponent';
describe('MyComponent', () => {
it('renders and interacts', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
server.use(
rest.get('*/api/v1/example', (_req, res, ctx) => res(ctx.status(200), ctx.json({ value: 42 })))
);
render(<MyComponent />, undefined, { initialRoute: '/foo' });
expect(await screen.findByText(/value: 42/i)).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: /refresh/i }));
await waitFor(() => expect(screen.getByText(/loading/i)).toBeInTheDocument());
});
});
```
# Anti-patterns
❌ Importing RTL directly
❌ Using global fake timers
❌ Wrapping render in `act(...)`
❌ Mocking infra dependencies locally (router, react-query)
✅ Use our harness (`tests/test-utils`)
✅ Use MSW for API overrides
✅ Use userEvent + await
✅ Pin time only in tests that assert relative dates
# TypeScript Type Safety Examples
## Proper Mock Typing
```ts
// ✅ GOOD - Properly typed mocks
interface User {
id: number;
name: string;
email: string;
}
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
// Type the mock functions
const mockFetchUser = jest.fn() as jest.MockedFunction<(id: number) => Promise<ApiResponse<User>>>;
const mockUpdateUser = jest.fn() as jest.MockedFunction<(user: User) => Promise<ApiResponse<User>>>;
// Mock implementation with proper typing
mockFetchUser.mockResolvedValue({
data: { id: 1, name: 'John Doe', email: 'john@example.com' },
status: 200,
message: 'Success'
});
// ❌ BAD - Using any type
const mockFetchUser = jest.fn() as any; // Don't do this
```
## React Component Testing with Types
```ts
// ✅ GOOD - Properly typed component testing
interface ComponentProps {
title: string;
data: User[];
onUserSelect: (user: User) => void;
isLoading?: boolean;
}
const TestComponent: React.FC<ComponentProps> = ({ title, data, onUserSelect, isLoading = false }) => {
// Component implementation
};
describe('TestComponent', () => {
it('should render with proper props', () => {
// Arrange - Type the props properly
const mockProps: ComponentProps = {
title: 'Test Title',
data: [{ id: 1, name: 'John', email: 'john@example.com' }],
onUserSelect: jest.fn() as jest.MockedFunction<(user: User) => void>,
isLoading: false
};
// Act
render(<TestComponent {...mockProps} />);
// Assert
expect(screen.getByText('Test Title')).toBeInTheDocument();
});
});
```
## Hook Testing with Types
```ts
// ✅ GOOD - Properly typed hook testing
interface UseUserDataReturn {
user: User | null;
loading: boolean;
error: string | null;
refetch: () => void;
}
const useUserData = (id: number): UseUserDataReturn => {
// Hook implementation
};
describe('useUserData', () => {
it('should return user data with proper typing', () => {
// Arrange
const mockUser: User = { id: 1, name: 'John', email: 'john@example.com' };
mockFetchUser.mockResolvedValue({
data: mockUser,
status: 200,
message: 'Success'
});
// Act
const { result } = renderHook(() => useUserData(1));
// Assert
expect(result.current.user).toEqual(mockUser);
expect(result.current.loading).toBe(false);
expect(result.current.error).toBeNull();
});
});
```
## Global Mock Type Safety
```ts
// ✅ GOOD - Type-safe global mocks
// In __mocks__/routerMock.ts
export const mockUseLocation = (overrides: Partial<Location> = {}): Location => ({
pathname: '/traces',
search: '',
hash: '',
state: null,
key: 'test-key',
...overrides,
});
// In test files
const location = useLocation(); // Properly typed from global mock
expect(location.pathname).toBe('/traces');
```
# TypeScript Configuration for Jest
## Required Jest Configuration
```json
// jest.config.ts
{
"preset": "ts-jest/presets/js-with-ts-esm",
"globals": {
"ts-jest": {
"useESM": true,
"isolatedModules": true,
"tsconfig": "<rootDir>/tsconfig.jest.json"
}
},
"extensionsToTreatAsEsm": [".ts", ".tsx"],
"moduleFileExtensions": ["ts", "tsx", "js", "json"]
}
```
## TypeScript Jest Configuration
```json
// tsconfig.jest.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"types": ["jest", "@testing-library/jest-dom"],
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"moduleResolution": "node"
},
"include": [
"src/**/*",
"**/*.test.ts",
"**/*.test.tsx",
"__mocks__/**/*"
]
}
```
## Common Type Safety Patterns
### Mock Function Typing
```ts
// ✅ GOOD - Proper mock function typing
const mockApiCall = jest.fn() as jest.MockedFunction<typeof apiCall>;
const mockEventHandler = jest.fn() as jest.MockedFunction<(event: Event) => void>;
// ❌ BAD - Using any
const mockApiCall = jest.fn() as any;
```
### Generic Mock Typing
```ts
// ✅ GOOD - Generic mock typing
interface MockApiResponse<T> {
data: T;
status: number;
}
const mockFetchData = jest.fn() as jest.MockedFunction<
<T>(endpoint: string) => Promise<MockApiResponse<T>>
>;
// Usage
mockFetchData<User>('/users').mockResolvedValue({
data: { id: 1, name: 'John' },
status: 200
});
```
### React Testing Library with Types
```ts
// ✅ GOOD - Typed testing utilities
import { render, screen, RenderResult } from '@testing-library/react';
import { ComponentProps } from 'react';
type TestComponentProps = ComponentProps<typeof TestComponent>;
const renderTestComponent = (props: Partial<TestComponentProps> = {}): RenderResult => {
const defaultProps: TestComponentProps = {
title: 'Test',
data: [],
onSelect: jest.fn(),
...props
};
return render(<TestComponent {...defaultProps} />);
};
```
### Error Handling with Types
```ts
// ✅ GOOD - Typed error handling
interface ApiError {
message: string;
code: number;
details?: Record<string, unknown>;
}
const mockApiError: ApiError = {
message: 'API Error',
code: 500,
details: { endpoint: '/users' }
};
mockFetchUser.mockRejectedValue(new Error(JSON.stringify(mockApiError)));
```
## Type Safety Checklist
- [ ] All mock functions use `jest.MockedFunction<T>`
- [ ] All mock data has proper interfaces
- [ ] No `any` types in test files
- [ ] Generic types are used where appropriate
- [ ] Error types are properly defined
- [ ] Component props are typed
- [ ] Hook return types are defined
- [ ] API response types are defined
- [ ] Global mocks are type-safe
- [ ] Test utilities are properly typed
# Mock Decision Tree
```
Is it used in 20+ test files?
├─ YES → Use Global Mock
│ ├─ react-router-dom
│ ├─ react-query
│ ├─ antd components
│ └─ browser APIs
└─ NO → Is it business logic?
├─ YES → Use Local Mock
│ ├─ API endpoints
│ ├─ Custom hooks
│ └─ Domain components
└─ NO → Is it test-specific?
├─ YES → Use Local Mock
│ ├─ Error scenarios
│ ├─ Loading states
│ └─ Specific data
└─ NO → Consider Global Mock
└─ If it becomes frequently used
```
# Common Anti-Patterns to Avoid
❌ **Don't mock global dependencies locally:**
```js
// BAD - This is already globally mocked
jest.mock('react-router-dom', () => ({ ... }));
```
❌ **Don't create global mocks for test-specific data:**
```js
// BAD - This should be local
jest.mock('../api/tracesService', () => ({
getTraces: jest.fn(() => specificTestData)
}));
```
✅ **Do use global mocks for infrastructure:**
```js
// GOOD - Use global mock
import { useLocation } from 'react-router-dom';
```
✅ **Do create local mocks for business logic:**
```js
// GOOD - Local mock for specific test needs
jest.mock('../api/tracesService', () => ({
getTraces: jest.fn(() => mockTracesData)
}));
```

View File

@@ -20,20 +20,17 @@ import { useMutation, useQuery } from 'react-query';
import { GeneratedAPIInstance } from '../../../index';
import type {
GetMetricAlerts200,
GetMetricAlertsPathParameters,
GetMetricAlertsParams,
GetMetricAttributes200,
GetMetricAttributesParams,
GetMetricAttributesPathParameters,
GetMetricDashboards200,
GetMetricDashboardsPathParameters,
GetMetricDashboardsParams,
GetMetricHighlights200,
GetMetricHighlightsPathParameters,
GetMetricHighlightsParams,
GetMetricMetadata200,
GetMetricMetadataPathParameters,
GetMetricMetadataParams,
GetMetricsStats200,
GetMetricsTreemap200,
ListMetrics200,
ListMetricsParams,
MetricsexplorertypesMetricAttributesRequestDTO,
MetricsexplorertypesStatsRequestDTO,
MetricsexplorertypesTreemapRequestDTO,
MetricsexplorertypesUpdateMetricMetadataRequestDTO,
@@ -46,128 +43,30 @@ type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoint returns a list of distinct metric names within the specified time range
* @summary List metric names
* This endpoint returns associated alerts for a specified metric
* @summary Get metric alerts
*/
export const listMetrics = (
params?: ListMetricsParams,
export const getMetricAlerts = (
params: GetMetricAlertsParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<ListMetrics200>({
url: `/api/v2/metrics`,
return GeneratedAPIInstance<GetMetricAlerts200>({
url: `/api/v2/metric/alerts`,
method: 'GET',
params,
signal,
});
};
export const getListMetricsQueryKey = (params?: ListMetricsParams) => {
return ['listMetrics', ...(params ? [params] : [])] as const;
};
export const getListMetricsQueryOptions = <
TData = Awaited<ReturnType<typeof listMetrics>>,
TError = RenderErrorResponseDTO
>(
params?: ListMetricsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listMetrics>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListMetricsQueryKey(params);
const queryFn: QueryFunction<Awaited<ReturnType<typeof listMetrics>>> = ({
signal,
}) => listMetrics(params, signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof listMetrics>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type ListMetricsQueryResult = NonNullable<
Awaited<ReturnType<typeof listMetrics>>
>;
export type ListMetricsQueryError = RenderErrorResponseDTO;
/**
* @summary List metric names
*/
export function useListMetrics<
TData = Awaited<ReturnType<typeof listMetrics>>,
TError = RenderErrorResponseDTO
>(
params?: ListMetricsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listMetrics>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getListMetricsQueryOptions(params, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary List metric names
*/
export const invalidateListMetrics = async (
queryClient: QueryClient,
params?: ListMetricsParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getListMetricsQueryKey(params) },
options,
);
return queryClient;
};
/**
* This endpoint returns associated alerts for a specified metric
* @summary Get metric alerts
*/
export const getMetricAlerts = (
{ metricName }: GetMetricAlertsPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetMetricAlerts200>({
url: `/api/v2/metrics/${metricName}/alerts`,
method: 'GET',
signal,
});
};
export const getGetMetricAlertsQueryKey = ({
metricName,
}: GetMetricAlertsPathParameters) => {
return ['getMetricAlerts'] as const;
export const getGetMetricAlertsQueryKey = (params?: GetMetricAlertsParams) => {
return ['getMetricAlerts', ...(params ? [params] : [])] as const;
};
export const getGetMetricAlertsQueryOptions = <
TData = Awaited<ReturnType<typeof getMetricAlerts>>,
TError = RenderErrorResponseDTO
>(
{ metricName }: GetMetricAlertsPathParameters,
params: GetMetricAlertsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricAlerts>>,
@@ -178,19 +77,13 @@ export const getGetMetricAlertsQueryOptions = <
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetMetricAlertsQueryKey({ metricName });
const queryKey = queryOptions?.queryKey ?? getGetMetricAlertsQueryKey(params);
const queryFn: QueryFunction<Awaited<ReturnType<typeof getMetricAlerts>>> = ({
signal,
}) => getMetricAlerts({ metricName }, signal);
}) => getMetricAlerts(params, signal);
return {
queryKey,
queryFn,
enabled: !!metricName,
...queryOptions,
} as UseQueryOptions<
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getMetricAlerts>>,
TError,
TData
@@ -210,7 +103,7 @@ export function useGetMetricAlerts<
TData = Awaited<ReturnType<typeof getMetricAlerts>>,
TError = RenderErrorResponseDTO
>(
{ metricName }: GetMetricAlertsPathParameters,
params: GetMetricAlertsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricAlerts>>,
@@ -219,7 +112,7 @@ export function useGetMetricAlerts<
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetMetricAlertsQueryOptions({ metricName }, options);
const queryOptions = getGetMetricAlertsQueryOptions(params, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
@@ -235,126 +128,11 @@ export function useGetMetricAlerts<
*/
export const invalidateGetMetricAlerts = async (
queryClient: QueryClient,
{ metricName }: GetMetricAlertsPathParameters,
params: GetMetricAlertsParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetMetricAlertsQueryKey({ metricName }) },
options,
);
return queryClient;
};
/**
* This endpoint returns attribute keys and their unique values for a specified metric
* @summary Get metric attributes
*/
export const getMetricAttributes = (
{ metricName }: GetMetricAttributesPathParameters,
params?: GetMetricAttributesParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetMetricAttributes200>({
url: `/api/v2/metrics/${metricName}/attributes`,
method: 'GET',
params,
signal,
});
};
export const getGetMetricAttributesQueryKey = (
{ metricName }: GetMetricAttributesPathParameters,
params?: GetMetricAttributesParams,
) => {
return ['getMetricAttributes', ...(params ? [params] : [])] as const;
};
export const getGetMetricAttributesQueryOptions = <
TData = Awaited<ReturnType<typeof getMetricAttributes>>,
TError = RenderErrorResponseDTO
>(
{ metricName }: GetMetricAttributesPathParameters,
params?: GetMetricAttributesParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricAttributes>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ??
getGetMetricAttributesQueryKey({ metricName }, params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getMetricAttributes>>
> = ({ signal }) => getMetricAttributes({ metricName }, params, signal);
return {
queryKey,
queryFn,
enabled: !!metricName,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getMetricAttributes>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetMetricAttributesQueryResult = NonNullable<
Awaited<ReturnType<typeof getMetricAttributes>>
>;
export type GetMetricAttributesQueryError = RenderErrorResponseDTO;
/**
* @summary Get metric attributes
*/
export function useGetMetricAttributes<
TData = Awaited<ReturnType<typeof getMetricAttributes>>,
TError = RenderErrorResponseDTO
>(
{ metricName }: GetMetricAttributesPathParameters,
params?: GetMetricAttributesParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricAttributes>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetMetricAttributesQueryOptions(
{ metricName },
params,
options,
);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Get metric attributes
*/
export const invalidateGetMetricAttributes = async (
queryClient: QueryClient,
{ metricName }: GetMetricAttributesPathParameters,
params?: GetMetricAttributesParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetMetricAttributesQueryKey({ metricName }, params) },
{ queryKey: getGetMetricAlertsQueryKey(params) },
options,
);
@@ -366,27 +144,28 @@ export const invalidateGetMetricAttributes = async (
* @summary Get metric dashboards
*/
export const getMetricDashboards = (
{ metricName }: GetMetricDashboardsPathParameters,
params: GetMetricDashboardsParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetMetricDashboards200>({
url: `/api/v2/metrics/${metricName}/dashboards`,
url: `/api/v2/metric/dashboards`,
method: 'GET',
params,
signal,
});
};
export const getGetMetricDashboardsQueryKey = ({
metricName,
}: GetMetricDashboardsPathParameters) => {
return ['getMetricDashboards'] as const;
export const getGetMetricDashboardsQueryKey = (
params?: GetMetricDashboardsParams,
) => {
return ['getMetricDashboards', ...(params ? [params] : [])] as const;
};
export const getGetMetricDashboardsQueryOptions = <
TData = Awaited<ReturnType<typeof getMetricDashboards>>,
TError = RenderErrorResponseDTO
>(
{ metricName }: GetMetricDashboardsPathParameters,
params: GetMetricDashboardsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricDashboards>>,
@@ -398,18 +177,13 @@ export const getGetMetricDashboardsQueryOptions = <
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetMetricDashboardsQueryKey({ metricName });
queryOptions?.queryKey ?? getGetMetricDashboardsQueryKey(params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getMetricDashboards>>
> = ({ signal }) => getMetricDashboards({ metricName }, signal);
> = ({ signal }) => getMetricDashboards(params, signal);
return {
queryKey,
queryFn,
enabled: !!metricName,
...queryOptions,
} as UseQueryOptions<
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getMetricDashboards>>,
TError,
TData
@@ -429,7 +203,7 @@ export function useGetMetricDashboards<
TData = Awaited<ReturnType<typeof getMetricDashboards>>,
TError = RenderErrorResponseDTO
>(
{ metricName }: GetMetricDashboardsPathParameters,
params: GetMetricDashboardsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricDashboards>>,
@@ -438,10 +212,7 @@ export function useGetMetricDashboards<
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetMetricDashboardsQueryOptions(
{ metricName },
options,
);
const queryOptions = getGetMetricDashboardsQueryOptions(params, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
@@ -457,11 +228,11 @@ export function useGetMetricDashboards<
*/
export const invalidateGetMetricDashboards = async (
queryClient: QueryClient,
{ metricName }: GetMetricDashboardsPathParameters,
params: GetMetricDashboardsParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetMetricDashboardsQueryKey({ metricName }) },
{ queryKey: getGetMetricDashboardsQueryKey(params) },
options,
);
@@ -473,27 +244,28 @@ export const invalidateGetMetricDashboards = async (
* @summary Get metric highlights
*/
export const getMetricHighlights = (
{ metricName }: GetMetricHighlightsPathParameters,
params: GetMetricHighlightsParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetMetricHighlights200>({
url: `/api/v2/metrics/${metricName}/highlights`,
url: `/api/v2/metric/highlights`,
method: 'GET',
params,
signal,
});
};
export const getGetMetricHighlightsQueryKey = ({
metricName,
}: GetMetricHighlightsPathParameters) => {
return ['getMetricHighlights'] as const;
export const getGetMetricHighlightsQueryKey = (
params?: GetMetricHighlightsParams,
) => {
return ['getMetricHighlights', ...(params ? [params] : [])] as const;
};
export const getGetMetricHighlightsQueryOptions = <
TData = Awaited<ReturnType<typeof getMetricHighlights>>,
TError = RenderErrorResponseDTO
>(
{ metricName }: GetMetricHighlightsPathParameters,
params: GetMetricHighlightsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricHighlights>>,
@@ -505,18 +277,13 @@ export const getGetMetricHighlightsQueryOptions = <
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetMetricHighlightsQueryKey({ metricName });
queryOptions?.queryKey ?? getGetMetricHighlightsQueryKey(params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getMetricHighlights>>
> = ({ signal }) => getMetricHighlights({ metricName }, signal);
> = ({ signal }) => getMetricHighlights(params, signal);
return {
queryKey,
queryFn,
enabled: !!metricName,
...queryOptions,
} as UseQueryOptions<
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getMetricHighlights>>,
TError,
TData
@@ -536,7 +303,7 @@ export function useGetMetricHighlights<
TData = Awaited<ReturnType<typeof getMetricHighlights>>,
TError = RenderErrorResponseDTO
>(
{ metricName }: GetMetricHighlightsPathParameters,
params: GetMetricHighlightsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricHighlights>>,
@@ -545,10 +312,7 @@ export function useGetMetricHighlights<
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetMetricHighlightsQueryOptions(
{ metricName },
options,
);
const queryOptions = getGetMetricHighlightsQueryOptions(params, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
@@ -564,115 +328,11 @@ export function useGetMetricHighlights<
*/
export const invalidateGetMetricHighlights = async (
queryClient: QueryClient,
{ metricName }: GetMetricHighlightsPathParameters,
params: GetMetricHighlightsParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetMetricHighlightsQueryKey({ metricName }) },
options,
);
return queryClient;
};
/**
* This endpoint returns metadata information like metric description, unit, type, temporality, monotonicity for a specified metric
* @summary Get metric metadata
*/
export const getMetricMetadata = (
{ metricName }: GetMetricMetadataPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetMetricMetadata200>({
url: `/api/v2/metrics/${metricName}/metadata`,
method: 'GET',
signal,
});
};
export const getGetMetricMetadataQueryKey = ({
metricName,
}: GetMetricMetadataPathParameters) => {
return ['getMetricMetadata'] as const;
};
export const getGetMetricMetadataQueryOptions = <
TData = Awaited<ReturnType<typeof getMetricMetadata>>,
TError = RenderErrorResponseDTO
>(
{ metricName }: GetMetricMetadataPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricMetadata>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetMetricMetadataQueryKey({ metricName });
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getMetricMetadata>>
> = ({ signal }) => getMetricMetadata({ metricName }, signal);
return {
queryKey,
queryFn,
enabled: !!metricName,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getMetricMetadata>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetMetricMetadataQueryResult = NonNullable<
Awaited<ReturnType<typeof getMetricMetadata>>
>;
export type GetMetricMetadataQueryError = RenderErrorResponseDTO;
/**
* @summary Get metric metadata
*/
export function useGetMetricMetadata<
TData = Awaited<ReturnType<typeof getMetricMetadata>>,
TError = RenderErrorResponseDTO
>(
{ metricName }: GetMetricMetadataPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricMetadata>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetMetricMetadataQueryOptions({ metricName }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Get metric metadata
*/
export const invalidateGetMetricMetadata = async (
queryClient: QueryClient,
{ metricName }: GetMetricMetadataPathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetMetricMetadataQueryKey({ metricName }) },
{ queryKey: getGetMetricHighlightsQueryKey(params) },
options,
);
@@ -778,6 +438,189 @@ export const useUpdateMetricMetadata = <
return useMutation(mutationOptions);
};
/**
* This endpoint returns attribute keys and their unique values for a specified metric
* @summary Get metric attributes
*/
export const getMetricAttributes = (
metricsexplorertypesMetricAttributesRequestDTO: MetricsexplorertypesMetricAttributesRequestDTO,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetMetricAttributes200>({
url: `/api/v2/metrics/attributes`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: metricsexplorertypesMetricAttributesRequestDTO,
signal,
});
};
export const getGetMetricAttributesMutationOptions = <
TError = RenderErrorResponseDTO,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof getMetricAttributes>>,
TError,
{ data: MetricsexplorertypesMetricAttributesRequestDTO },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof getMetricAttributes>>,
TError,
{ data: MetricsexplorertypesMetricAttributesRequestDTO },
TContext
> => {
const mutationKey = ['getMetricAttributes'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof getMetricAttributes>>,
{ data: MetricsexplorertypesMetricAttributesRequestDTO }
> = (props) => {
const { data } = props ?? {};
return getMetricAttributes(data);
};
return { mutationFn, ...mutationOptions };
};
export type GetMetricAttributesMutationResult = NonNullable<
Awaited<ReturnType<typeof getMetricAttributes>>
>;
export type GetMetricAttributesMutationBody = MetricsexplorertypesMetricAttributesRequestDTO;
export type GetMetricAttributesMutationError = RenderErrorResponseDTO;
/**
* @summary Get metric attributes
*/
export const useGetMetricAttributes = <
TError = RenderErrorResponseDTO,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof getMetricAttributes>>,
TError,
{ data: MetricsexplorertypesMetricAttributesRequestDTO },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof getMetricAttributes>>,
TError,
{ data: MetricsexplorertypesMetricAttributesRequestDTO },
TContext
> => {
const mutationOptions = getGetMetricAttributesMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* This endpoint returns metadata information like metric description, unit, type, temporality, monotonicity for a specified metric
* @summary Get metric metadata
*/
export const getMetricMetadata = (
params: GetMetricMetadataParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetMetricMetadata200>({
url: `/api/v2/metrics/metadata`,
method: 'GET',
params,
signal,
});
};
export const getGetMetricMetadataQueryKey = (
params?: GetMetricMetadataParams,
) => {
return ['getMetricMetadata', ...(params ? [params] : [])] as const;
};
export const getGetMetricMetadataQueryOptions = <
TData = Awaited<ReturnType<typeof getMetricMetadata>>,
TError = RenderErrorResponseDTO
>(
params: GetMetricMetadataParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricMetadata>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetMetricMetadataQueryKey(params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getMetricMetadata>>
> = ({ signal }) => getMetricMetadata(params, signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getMetricMetadata>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetMetricMetadataQueryResult = NonNullable<
Awaited<ReturnType<typeof getMetricMetadata>>
>;
export type GetMetricMetadataQueryError = RenderErrorResponseDTO;
/**
* @summary Get metric metadata
*/
export function useGetMetricMetadata<
TData = Awaited<ReturnType<typeof getMetricMetadata>>,
TError = RenderErrorResponseDTO
>(
params: GetMetricMetadataParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricMetadata>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetMetricMetadataQueryOptions(params, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Get metric metadata
*/
export const invalidateGetMetricMetadata = async (
queryClient: QueryClient,
params: GetMetricMetadataParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetMetricMetadataQueryKey(params) },
options,
);
return queryClient;
};
/**
* This endpoint provides list of metrics with their number of samples and timeseries for the given time range
* @summary Get metrics statistics

View File

@@ -1,107 +0,0 @@
/**
* ! Do not edit manually
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'yarn generate:api'
* SigNoz
*/
import type {
MutationFunction,
UseMutationOptions,
UseMutationResult,
} from 'react-query';
import { useMutation } from 'react-query';
import { GeneratedAPIInstance } from '../../../index';
import type {
Querybuildertypesv5QueryRangeRequestDTO,
QueryRangeV5200,
RenderErrorResponseDTO,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* Execute a composite query over a time range. Supports builder queries (traces, logs, metrics), formulas, trace operators, PromQL, and ClickHouse SQL.
* @summary Query range
*/
export const queryRangeV5 = (
querybuildertypesv5QueryRangeRequestDTO: Querybuildertypesv5QueryRangeRequestDTO,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<QueryRangeV5200>({
url: `/api/v5/query_range`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: querybuildertypesv5QueryRangeRequestDTO,
signal,
});
};
export const getQueryRangeV5MutationOptions = <
TError = RenderErrorResponseDTO,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof queryRangeV5>>,
TError,
{ data: Querybuildertypesv5QueryRangeRequestDTO },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof queryRangeV5>>,
TError,
{ data: Querybuildertypesv5QueryRangeRequestDTO },
TContext
> => {
const mutationKey = ['queryRangeV5'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof queryRangeV5>>,
{ data: Querybuildertypesv5QueryRangeRequestDTO }
> = (props) => {
const { data } = props ?? {};
return queryRangeV5(data);
};
return { mutationFn, ...mutationOptions };
};
export type QueryRangeV5MutationResult = NonNullable<
Awaited<ReturnType<typeof queryRangeV5>>
>;
export type QueryRangeV5MutationBody = Querybuildertypesv5QueryRangeRequestDTO;
export type QueryRangeV5MutationError = RenderErrorResponseDTO;
/**
* @summary Query range
*/
export const useQueryRangeV5 = <
TError = RenderErrorResponseDTO,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof queryRangeV5>>,
TError,
{ data: Querybuildertypesv5QueryRangeRequestDTO },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof queryRangeV5>>,
TError,
{ data: Querybuildertypesv5QueryRangeRequestDTO },
TContext
> => {
const mutationOptions = getQueryRangeV5MutationOptions(options);
return useMutation(mutationOptions);
};

File diff suppressed because it is too large Load Diff

View File

@@ -11,10 +11,13 @@ export const getMetricMetadata = async (
): Promise<SuccessResponseV2<MetricMetadataResponse> | ErrorResponseV2> => {
try {
const encodedMetricName = encodeURIComponent(metricName);
const response = await axios.get(`/metrics/${encodedMetricName}/metadata`, {
signal,
headers,
});
const response = await axios.get(
`/metrics/metadata?metricName=${encodedMetricName}`,
{
signal,
headers,
},
);
return {
httpStatusCode: response.status,

View File

@@ -1,332 +0,0 @@
import { ReactElement } from 'react-markdown/lib/react-markdown';
import { ENVIRONMENT } from 'constants/env';
import { BrandedPermission, buildPermission } from 'hooks/useAuthZ/utils';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { render, screen, waitFor } from 'tests/test-utils';
import { GuardAuthZ } from './GuardAuthZ';
const BASE_URL = ENVIRONMENT.baseURL || '';
describe('GuardAuthZ', () => {
const TestChild = (): ReactElement => <div>Protected Content</div>;
const LoadingFallback = (): ReactElement => <div>Loading...</div>;
const ErrorFallback = (error: Error): ReactElement => (
<div>Error occurred: {error.message}</div>
);
const NoPermissionFallback = (_response: {
requiredPermissionName: BrandedPermission;
}): ReactElement => <div>Access denied</div>;
const NoPermissionFallbackWithSuggestions = (response: {
requiredPermissionName: BrandedPermission;
}): ReactElement => (
<div>
Access denied. Required permission: {response.requiredPermissionName}
</div>
);
it('should render children when permission is granted', async () => {
const permission = buildPermission('read', 'dashboard:*');
server.use(
rest.post(`${BASE_URL}/api/v1/permissions/check`, (_req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
[permission]: true,
}),
);
}),
);
render(
<GuardAuthZ relation="read" object="dashboard:*">
<TestChild />
</GuardAuthZ>,
);
await waitFor(() => {
expect(screen.getByText('Protected Content')).toBeInTheDocument();
});
});
it('should render fallbackOnLoading when loading', () => {
server.use(
rest.post(`${BASE_URL}/api/v1/permissions/check`, (_req, res, ctx) => {
return res(ctx.status(200), ctx.json({}));
}),
);
render(
<GuardAuthZ
relation="read"
object="dashboard:*"
fallbackOnLoading={<LoadingFallback />}
>
<TestChild />
</GuardAuthZ>,
);
expect(screen.getByText('Loading...')).toBeInTheDocument();
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
});
it('should render null when loading and no fallbackOnLoading provided', () => {
server.use(
rest.post(`${BASE_URL}/api/v1/permissions/check`, (_req, res, ctx) => {
return res(ctx.status(200), ctx.json({}));
}),
);
const { container } = render(
<GuardAuthZ relation="read" object="dashboard:*">
<TestChild />
</GuardAuthZ>,
);
expect(container.firstChild).toBeNull();
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
});
it('should render fallbackOnError when API error occurs', async () => {
const errorMessage = 'Internal Server Error';
server.use(
rest.post(`${BASE_URL}/api/v1/permissions/check`, (_req, res, ctx) => {
return res(ctx.status(500), ctx.json({ error: errorMessage }));
}),
);
render(
<GuardAuthZ
relation="read"
object="dashboard:*"
fallbackOnError={ErrorFallback}
>
<TestChild />
</GuardAuthZ>,
);
await waitFor(() => {
expect(screen.getByText(/Error occurred:/)).toBeInTheDocument();
});
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
});
it('should pass error object to fallbackOnError function', async () => {
const errorMessage = 'Network request failed';
let receivedError: Error | null = null;
const errorFallbackWithCapture = (error: Error): ReactElement => {
receivedError = error;
return <div>Captured error: {error.message}</div>;
};
server.use(
rest.post(`${BASE_URL}/api/v1/permissions/check`, (_req, res, ctx) => {
return res(ctx.status(500), ctx.json({ error: errorMessage }));
}),
);
render(
<GuardAuthZ
relation="read"
object="dashboard:*"
fallbackOnError={errorFallbackWithCapture}
>
<TestChild />
</GuardAuthZ>,
);
await waitFor(() => {
expect(receivedError).not.toBeNull();
});
expect(receivedError).toBeInstanceOf(Error);
expect(screen.getByText(/Captured error:/)).toBeInTheDocument();
});
it('should render null when error occurs and no fallbackOnError provided', async () => {
server.use(
rest.post(`${BASE_URL}/api/v1/permissions/check`, (_req, res, ctx) => {
return res(ctx.status(500), ctx.json({ error: 'Internal Server Error' }));
}),
);
const { container } = render(
<GuardAuthZ relation="read" object="dashboard:*">
<TestChild />
</GuardAuthZ>,
);
await waitFor(() => {
expect(container.firstChild).toBeNull();
});
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
});
it('should render fallbackOnNoPermissions when permission is denied', async () => {
const permission = buildPermission('update', 'dashboard:123');
server.use(
rest.post(`${BASE_URL}/api/v1/permissions/check`, (_req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
[permission]: false,
}),
);
}),
);
render(
<GuardAuthZ
relation="update"
object="dashboard:123"
fallbackOnNoPermissions={NoPermissionFallback}
>
<TestChild />
</GuardAuthZ>,
);
await waitFor(() => {
expect(screen.getByText('Access denied')).toBeInTheDocument();
});
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
});
it('should render null when permission is denied and no fallbackOnNoPermissions provided', async () => {
const permission = buildPermission('update', 'dashboard:123');
server.use(
rest.post(`${BASE_URL}/api/v1/permissions/check`, (_req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
[permission]: false,
}),
);
}),
);
const { container } = render(
<GuardAuthZ relation="update" object="dashboard:123">
<TestChild />
</GuardAuthZ>,
);
await waitFor(() => {
expect(container.firstChild).toBeNull();
});
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
});
it('should render null when permissions object is null', async () => {
server.use(
rest.post(`${BASE_URL}/api/v1/permissions/check`, (_req, res, ctx) => {
return res(ctx.status(200), ctx.json(null));
}),
);
const { container } = render(
<GuardAuthZ relation="read" object="dashboard:*">
<TestChild />
</GuardAuthZ>,
);
await waitFor(() => {
expect(container.firstChild).toBeNull();
});
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
});
it('should pass requiredPermissionName to fallbackOnNoPermissions', async () => {
const permission = buildPermission('update', 'dashboard:123');
server.use(
rest.post(`${BASE_URL}/api/v1/permissions/check`, (_req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
[permission]: false,
}),
);
}),
);
render(
<GuardAuthZ
relation="update"
object="dashboard:123"
fallbackOnNoPermissions={NoPermissionFallbackWithSuggestions}
>
<TestChild />
</GuardAuthZ>,
);
await waitFor(() => {
expect(
screen.getByText(/Access denied. Required permission:/),
).toBeInTheDocument();
});
expect(screen.getByText(new RegExp(permission))).toBeInTheDocument();
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
});
it('should handle different relation and object combinations', async () => {
const permission1 = buildPermission('read', 'dashboard:*');
const permission2 = buildPermission('delete', 'dashboard:456');
server.use(
rest.post(`${BASE_URL}/api/v1/permissions/check`, (req, res, ctx) => {
const body = req.body as {
permissions: Array<{ relation: string; object: string }>;
};
const perm = body.permissions[0];
const permKey = `${perm.relation}/${perm.object}`;
if (permKey === permission1) {
return res(
ctx.status(200),
ctx.json({
[permission1]: true,
}),
);
}
return res(
ctx.status(200),
ctx.json({
[permission2]: true,
}),
);
}),
);
const { rerender } = render(
<GuardAuthZ relation="read" object="dashboard:*">
<TestChild />
</GuardAuthZ>,
);
await waitFor(() => {
expect(screen.getByText('Protected Content')).toBeInTheDocument();
});
rerender(
<GuardAuthZ relation="delete" object="dashboard:456">
<TestChild />
</GuardAuthZ>,
);
await waitFor(() => {
expect(screen.getByText('Protected Content')).toBeInTheDocument();
});
});
});

View File

@@ -1,50 +0,0 @@
import { ReactElement } from 'react';
import { useAuthZ } from 'hooks/useAuthZ/useAuthZ';
import {
AuthZObject,
AuthZRelation,
BrandedPermission,
buildPermission,
} from 'hooks/useAuthZ/utils';
export type GuardAuthZProps<R extends AuthZRelation> = {
children: ReactElement;
relation: R;
object: AuthZObject<R>;
fallbackOnLoading?: JSX.Element;
fallbackOnError?: (error: Error) => JSX.Element;
fallbackOnNoPermissions?: (response: {
requiredPermissionName: BrandedPermission;
}) => JSX.Element;
};
export function GuardAuthZ<R extends AuthZRelation>({
children,
relation,
object,
fallbackOnLoading,
fallbackOnError,
fallbackOnNoPermissions,
}: GuardAuthZProps<R>): JSX.Element | null {
const permission = buildPermission<R>(relation, object);
const { permissions, isLoading, error } = useAuthZ([permission]);
if (isLoading) {
return fallbackOnLoading ?? null;
}
if (error) {
return fallbackOnError?.(error) ?? null;
}
if (!permissions?.[permission]?.isGranted) {
return (
fallbackOnNoPermissions?.({
requiredPermissionName: permission,
}) ?? null
);
}
return children;
}

View File

@@ -1,46 +0,0 @@
.guard-authz-error-no-authz {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
padding: 24px;
.guard-authz-error-no-authz-content {
display: flex;
flex-direction: column;
justify-content: flex-start;
gap: 8px;
max-width: 500px;
}
img {
width: 32px;
height: 32px;
}
h3 {
font-size: 18px;
color: var(--bg-vanilla-100);
line-height: 18px;
}
p {
font-size: 14px;
color: var(--bg-vanilla-400);
line-height: 18px;
pre {
display: flex;
align-items: center;
background-color: var(--bg-slate-400);
cursor: pointer;
svg {
margin-left: 2px;
}
}
}
}

View File

@@ -1,489 +0,0 @@
import { ReactElement } from 'react';
import type { RouteComponentProps } from 'react-router-dom';
import { ENVIRONMENT } from 'constants/env';
import { buildPermission } from 'hooks/useAuthZ/utils';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { render, screen, waitFor } from 'tests/test-utils';
import { createGuardedRoute } from './createGuardedRoute';
const BASE_URL = ENVIRONMENT.baseURL || '';
describe('createGuardedRoute', () => {
const TestComponent = ({ testProp }: { testProp: string }): ReactElement => (
<div>Test Component: {testProp}</div>
);
it('should render component when permission is granted', async () => {
const permission = buildPermission('read', 'dashboard:*');
server.use(
rest.post(`${BASE_URL}/api/v1/permissions/check`, (_req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
[permission]: true,
}),
);
}),
);
const GuardedComponent = createGuardedRoute(
TestComponent,
'read',
'dashboard:*',
);
const mockMatch = {
params: {},
isExact: true,
path: '/dashboard',
url: '/dashboard',
};
const props = {
testProp: 'test-value',
match: mockMatch,
location: ({} as unknown) as RouteComponentProps['location'],
history: ({} as unknown) as RouteComponentProps['history'],
};
render(<GuardedComponent {...props} />);
await waitFor(() => {
expect(screen.getByText('Test Component: test-value')).toBeInTheDocument();
});
});
it('should substitute route parameters in object string', async () => {
const permission = buildPermission('read', 'dashboard:123');
server.use(
rest.post(`${BASE_URL}/api/v1/permissions/check`, (_req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
[permission]: true,
}),
);
}),
);
const GuardedComponent = createGuardedRoute(
TestComponent,
'read',
'dashboard:{id}',
);
const mockMatch = {
params: { id: '123' },
isExact: true,
path: '/dashboard/:id',
url: '/dashboard/123',
};
const props = {
testProp: 'test-value',
match: mockMatch,
location: ({} as unknown) as RouteComponentProps['location'],
history: ({} as unknown) as RouteComponentProps['history'],
};
render(<GuardedComponent {...props} />);
await waitFor(() => {
expect(screen.getByText('Test Component: test-value')).toBeInTheDocument();
});
});
it('should handle multiple route parameters', async () => {
const permission = buildPermission('update', 'dashboard:123:456');
server.use(
rest.post(`${BASE_URL}/api/v1/permissions/check`, (_req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
[permission]: true,
}),
);
}),
);
const GuardedComponent = createGuardedRoute(
TestComponent,
'update',
'dashboard:{id}:{version}',
);
const mockMatch = {
params: { id: '123', version: '456' },
isExact: true,
path: '/dashboard/:id/:version',
url: '/dashboard/123/456',
};
const props = {
testProp: 'test-value',
match: mockMatch,
location: ({} as unknown) as RouteComponentProps['location'],
history: ({} as unknown) as RouteComponentProps['history'],
};
render(<GuardedComponent {...props} />);
await waitFor(() => {
expect(screen.getByText('Test Component: test-value')).toBeInTheDocument();
});
});
it('should keep placeholder when route parameter is missing', async () => {
const permission = buildPermission('read', 'dashboard:{id}');
server.use(
rest.post(`${BASE_URL}/api/v1/permissions/check`, (_req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
[permission]: true,
}),
);
}),
);
const GuardedComponent = createGuardedRoute(
TestComponent,
'read',
'dashboard:{id}',
);
const mockMatch = {
params: {},
isExact: true,
path: '/dashboard',
url: '/dashboard',
};
const props = {
testProp: 'test-value',
match: mockMatch,
location: ({} as unknown) as RouteComponentProps['location'],
history: ({} as unknown) as RouteComponentProps['history'],
};
render(<GuardedComponent {...props} />);
await waitFor(() => {
expect(screen.getByText('Test Component: test-value')).toBeInTheDocument();
});
});
it('should render loading fallback when loading', () => {
server.use(
rest.post(`${BASE_URL}/api/v1/permissions/check`, (_req, res, ctx) => {
return res(ctx.status(200), ctx.json({}));
}),
);
const GuardedComponent = createGuardedRoute(
TestComponent,
'read',
'dashboard:*',
);
const mockMatch = {
params: {},
isExact: true,
path: '/dashboard',
url: '/dashboard',
};
const props = {
testProp: 'test-value',
match: mockMatch,
location: ({} as unknown) as RouteComponentProps['location'],
history: ({} as unknown) as RouteComponentProps['history'],
};
render(<GuardedComponent {...props} />);
expect(screen.getByText('SigNoz')).toBeInTheDocument();
expect(
screen.queryByText('Test Component: test-value'),
).not.toBeInTheDocument();
});
it('should render error fallback when API error occurs', async () => {
server.use(
rest.post(`${BASE_URL}/api/v1/permissions/check`, (_req, res, ctx) => {
return res(ctx.status(500), ctx.json({ error: 'Internal Server Error' }));
}),
);
const GuardedComponent = createGuardedRoute(
TestComponent,
'read',
'dashboard:*',
);
const mockMatch = {
params: {},
isExact: true,
path: '/dashboard',
url: '/dashboard',
};
const props = {
testProp: 'test-value',
match: mockMatch,
location: ({} as unknown) as RouteComponentProps['location'],
history: ({} as unknown) as RouteComponentProps['history'],
};
render(<GuardedComponent {...props} />);
await waitFor(() => {
expect(screen.getByText(/Something went wrong/i)).toBeInTheDocument();
});
expect(
screen.queryByText('Test Component: test-value'),
).not.toBeInTheDocument();
});
it('should render no permissions fallback when permission is denied', async () => {
const permission = buildPermission('update', 'dashboard:123');
server.use(
rest.post(`${BASE_URL}/api/v1/permissions/check`, (_req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
[permission]: false,
}),
);
}),
);
const GuardedComponent = createGuardedRoute(
TestComponent,
'update',
'dashboard:{id}',
);
const mockMatch = {
params: { id: '123' },
isExact: true,
path: '/dashboard/:id',
url: '/dashboard/123',
};
const props = {
testProp: 'test-value',
match: mockMatch,
location: ({} as unknown) as RouteComponentProps['location'],
history: ({} as unknown) as RouteComponentProps['history'],
};
render(<GuardedComponent {...props} />);
await waitFor(() => {
const heading = document.querySelector('h3');
expect(heading).toBeInTheDocument();
expect(heading?.textContent).toMatch(/permission to view/i);
});
expect(screen.getByText(permission, { exact: false })).toBeInTheDocument();
expect(
screen.queryByText('Test Component: test-value'),
).not.toBeInTheDocument();
});
it('should pass all props to wrapped component', async () => {
const permission = buildPermission('read', 'dashboard:*');
server.use(
rest.post(`${BASE_URL}/api/v1/permissions/check`, (_req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
[permission]: true,
}),
);
}),
);
const ComponentWithMultipleProps = ({
prop1,
prop2,
prop3,
}: {
prop1: string;
prop2: number;
prop3: boolean;
}): ReactElement => (
<div>
{prop1} - {prop2} - {prop3.toString()}
</div>
);
const GuardedComponent = createGuardedRoute(
ComponentWithMultipleProps,
'read',
'dashboard:*',
);
const mockMatch = {
params: {},
isExact: true,
path: '/dashboard',
url: '/dashboard',
};
const props = {
prop1: 'value1',
prop2: 42,
prop3: true,
match: mockMatch,
location: ({} as unknown) as RouteComponentProps['location'],
history: ({} as unknown) as RouteComponentProps['history'],
};
render(<GuardedComponent {...props} />);
await waitFor(() => {
expect(screen.getByText('value1 - 42 - true')).toBeInTheDocument();
});
});
it('should memoize resolved object based on route params', async () => {
const permission1 = buildPermission('read', 'dashboard:123');
const permission2 = buildPermission('read', 'dashboard:456');
let requestCount = 0;
const requestedPermissions: string[] = [];
server.use(
rest.post(`${BASE_URL}/api/v1/permissions/check`, (req, res, ctx) => {
requestCount++;
const body = req.body as {
permissions: Array<{ relation: string; object: string }>;
};
const perm = body.permissions[0];
requestedPermissions.push(`${perm.relation}/${perm.object}`);
if (perm.object === 'dashboard:123') {
return res(
ctx.status(200),
ctx.json({
[permission1]: true,
}),
);
}
return res(
ctx.status(200),
ctx.json({
[permission2]: true,
}),
);
}),
);
const GuardedComponent = createGuardedRoute(
TestComponent,
'read',
'dashboard:{id}',
);
const mockMatch1 = {
params: { id: '123' },
isExact: true,
path: '/dashboard/:id',
url: '/dashboard/123',
};
const props1 = {
testProp: 'test-value-1',
match: mockMatch1,
location: ({} as unknown) as RouteComponentProps['location'],
history: ({} as unknown) as RouteComponentProps['history'],
};
const { unmount } = render(<GuardedComponent {...props1} />);
await waitFor(() => {
expect(screen.getByText('Test Component: test-value-1')).toBeInTheDocument();
});
expect(requestCount).toBe(1);
expect(requestedPermissions).toContain('read/dashboard:123');
unmount();
const mockMatch2 = {
params: { id: '456' },
isExact: true,
path: '/dashboard/:id',
url: '/dashboard/456',
};
const props2 = {
testProp: 'test-value-2',
match: mockMatch2,
location: ({} as unknown) as RouteComponentProps['location'],
history: ({} as unknown) as RouteComponentProps['history'],
};
render(<GuardedComponent {...props2} />);
await waitFor(() => {
expect(screen.getByText('Test Component: test-value-2')).toBeInTheDocument();
});
expect(requestCount).toBe(2);
expect(requestedPermissions).toContain('read/dashboard:456');
});
it('should handle different relation types', async () => {
const permission = buildPermission('delete', 'dashboard:789');
server.use(
rest.post(`${BASE_URL}/api/v1/permissions/check`, (_req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
[permission]: true,
}),
);
}),
);
const GuardedComponent = createGuardedRoute(
TestComponent,
'delete',
'dashboard:{id}',
);
const mockMatch = {
params: { id: '789' },
isExact: true,
path: '/dashboard/:id',
url: '/dashboard/789',
};
const props = {
testProp: 'test-value',
match: mockMatch,
location: ({} as unknown) as RouteComponentProps['location'],
history: ({} as unknown) as RouteComponentProps['history'],
};
render(<GuardedComponent {...props} />);
await waitFor(() => {
expect(screen.getByText('Test Component: test-value')).toBeInTheDocument();
});
});
});

View File

@@ -1,79 +0,0 @@
import { ComponentType, ReactElement, useMemo } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { toast } from '@signozhq/sonner';
import {
AuthZObject,
AuthZRelation,
BrandedPermission,
} from 'hooks/useAuthZ/utils';
import { Copy } from 'lucide-react';
import { useCopyToClipboard } from '../../hooks/useCopyToClipboard';
import ErrorBoundaryFallback from '../../pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import AppLoading from '../AppLoading/AppLoading';
import { GuardAuthZ } from './GuardAuthZ';
import './createGuardedRoute.styles.scss';
const onErrorFallback = (): JSX.Element => <ErrorBoundaryFallback />;
function OnNoPermissionsFallback(response: {
requiredPermissionName: BrandedPermission;
}): ReactElement {
const { copyToClipboard } = useCopyToClipboard();
const onClickToCopy = (): void => {
copyToClipboard(response.requiredPermissionName, 'required-permission');
toast.success('Permission copied to clipboard');
};
return (
<div className="guard-authz-error-no-authz">
<div className="guard-authz-error-no-authz-content">
<img src="/Icons/no-data.svg" alt="No permission" />
<h3>Uh-oh! You dont have permission to view this page.</h3>
<p>
You need the following permission to view this page:
<br />
<pre onClick={onClickToCopy}>
{response.requiredPermissionName} <Copy size={12} />
</pre>
Ask your SigNoz administrator to grant access.
</p>
</div>
</div>
);
}
// eslint-disable-next-line @typescript-eslint/ban-types
export function createGuardedRoute<P extends object, R extends AuthZRelation>(
Component: ComponentType<P>,
relation: R,
object: AuthZObject<R>,
): ComponentType<P & RouteComponentProps<Record<string, string>>> {
return function GuardedRouteComponent(
props: P & RouteComponentProps<Record<string, string>>,
): ReactElement {
const resolvedObject = useMemo(() => {
const paramPattern = /\{([^}]+)\}/g;
return object.replace(paramPattern, (match, paramName) => {
const paramValue = props.match?.params?.[paramName];
return paramValue !== undefined ? paramValue : match;
}) as AuthZObject<R>;
}, [props.match?.params]);
return (
<GuardAuthZ
relation={relation}
object={resolvedObject}
fallbackOnLoading={<AppLoading />}
fallbackOnError={onErrorFallback}
fallbackOnNoPermissions={(response): ReactElement => (
<OnNoPermissionsFallback {...response} />
)}
>
<Component {...props} />
</GuardAuthZ>
);
};
}

View File

@@ -1,3 +0,0 @@
export { createGuardedRoute } from './createGuardedRoute';
export type { GuardAuthZProps } from './GuardAuthZ';
export { GuardAuthZ } from './GuardAuthZ';

View File

@@ -1,9 +1,8 @@
/* eslint-disable no-nested-ternary */
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { useCallback, useEffect, useMemo } from 'react';
import { useQuery } from 'react-query';
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
import { Virtuoso } from 'react-virtuoso';
import { Card } from 'antd';
import LogDetail from 'components/LogDetail';
import RawLogView from 'components/Logs/RawLogView';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
@@ -11,8 +10,6 @@ import LogsError from 'container/LogsError/LogsError';
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
import { FontSize } from 'container/OptionsMenu/types';
import { useHandleLogsPagination } from 'hooks/infraMonitoring/useHandleLogsPagination';
import useLogDetailHandlers from 'hooks/logs/useLogDetailHandlers';
import useScrollToLog from 'hooks/logs/useScrollToLog';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { ILog } from 'types/api/logs/log';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
@@ -31,15 +28,6 @@ interface Props {
}
function HostMetricsLogs({ timeRange, filters }: Props): JSX.Element {
const virtuosoRef = useRef<VirtuosoHandle>(null);
const {
activeLog,
onAddToQuery,
selectedTab,
handleSetActiveLog,
handleCloseLogDetail,
} = useLogDetailHandlers();
const basePayload = getHostLogsQueryPayload(
timeRange.startTime,
timeRange.endTime,
@@ -84,40 +72,29 @@ function HostMetricsLogs({ timeRange, filters }: Props): JSX.Element {
setIsPaginating(false);
}, [data, setIsPaginating]);
const handleScrollToLog = useScrollToLog({
logs,
virtuosoRef,
});
const getItemContent = useCallback(
(_: number, logToRender: ILog): JSX.Element => {
return (
<div key={logToRender.id}>
<RawLogView
isTextOverflowEllipsisDisabled
data={logToRender}
linesPerRow={5}
fontSize={FontSize.MEDIUM}
selectedFields={[
{
dataType: 'string',
type: '',
name: 'body',
},
{
dataType: 'string',
type: '',
name: 'timestamp',
},
]}
onSetActiveLog={handleSetActiveLog}
onClearActiveLog={handleCloseLogDetail}
isActiveLog={activeLog?.id === logToRender.id}
/>
</div>
);
},
[activeLog, handleSetActiveLog, handleCloseLogDetail],
(_: number, logToRender: ILog): JSX.Element => (
<RawLogView
isTextOverflowEllipsisDisabled
key={logToRender.id}
data={logToRender}
linesPerRow={5}
fontSize={FontSize.MEDIUM}
selectedFields={[
{
dataType: 'string',
type: '',
name: 'body',
},
{
dataType: 'string',
type: '',
name: 'timestamp',
},
]}
/>
),
[],
);
const renderFooter = useCallback(
@@ -141,7 +118,6 @@ function HostMetricsLogs({ timeRange, filters }: Props): JSX.Element {
<Virtuoso
className="host-metrics-logs-virtuoso"
key="host-metrics-logs-virtuoso"
ref={virtuosoRef}
data={logs}
endReached={loadMoreLogs}
totalCount={logs.length}
@@ -163,24 +139,7 @@ function HostMetricsLogs({ timeRange, filters }: Props): JSX.Element {
{!isLoading && !isError && logs.length === 0 && <NoLogsContainer />}
{isError && !isLoading && <LogsError />}
{!isLoading && !isError && logs.length > 0 && (
<div
className="host-metrics-logs-list-container"
data-log-detail-ignore="true"
>
{renderContent}
</div>
)}
{selectedTab && activeLog && (
<LogDetail
log={activeLog}
onClose={handleCloseLogDetail}
logs={logs}
onNavigateLog={handleSetActiveLog}
selectedTab={selectedTab}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
onScrollToLog={handleScrollToLog}
/>
<div className="host-metrics-logs-list-container">{renderContent}</div>
)}
</div>
);

View File

@@ -13,9 +13,6 @@ export type LogDetailProps = {
handleChangeSelectedView?: ChangeViewFunctionType;
isListViewPanel?: boolean;
listViewPanelSelectedFields?: IField[] | null;
logs?: ILog[];
onNavigateLog?: (log: ILog) => void;
onScrollToLog?: (logId: string) => void;
} & Pick<AddToQueryHOCProps, 'onAddToQuery'> &
Partial<Pick<ActionItemProps, 'onClickActionItem'>> &
Pick<DrawerProps, 'onClose'>;

View File

@@ -15,8 +15,6 @@
}
.log-detail-drawer__title-right {
display: flex;
align-items: center;
.ant-btn {
display: flex;
align-items: center;
@@ -68,10 +66,6 @@
margin-bottom: 16px;
}
.log-detail-drawer__content {
height: 100%;
}
.log-detail-drawer__log {
width: 100%;
display: flex;
@@ -189,115 +183,9 @@
.ant-drawer-close {
padding: 0px;
}
.log-detail-drawer__footer-hint {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 8px 16px;
text-align: left;
color: var(--text-vanilla-200);
background: var(--bg-ink-400);
z-index: 10;
.log-detail-drawer__footer-hint-content {
display: flex;
align-items: center;
gap: 4px;
}
.log-detail-drawer__footer-hint-icon {
display: inline;
vertical-align: middle;
color: var(--text-vanilla-200);
}
.log-detail-drawer__footer-hint-text {
font-size: 13px;
margin: 0;
}
}
.log-arrows {
display: flex;
box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.1);
border-radius: 6px;
padding: 2px 6px;
align-items: center;
margin-left: 8px;
}
.log-arrow-btn {
padding: 0;
min-width: 28px;
height: 28px;
border-radius: 4px;
background: var(--bg-ink-400);
color: var(--text-vanilla-400);
border: 1px solid var(--bg-ink-300);
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.08);
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s ease-in-out;
}
.log-arrow-btn-up,
.log-arrow-btn-down {
background: var(--bg-ink-400);
}
.log-arrow-btn:active,
.log-arrow-btn:focus {
background: var(--bg-ink-300);
color: var(--text-vanilla-100);
}
.log-arrow-btn[disabled] {
opacity: 0.5;
cursor: not-allowed;
background: var(--bg-ink-500);
color: var(--text-vanilla-200);
.log-arrow-btn:hover:not([disabled]) {
background: var(--bg-ink-300);
color: var(--text-vanilla-100);
}
}
}
.lightMode {
.log-arrows {
background: var(--bg-vanilla-100);
box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.04);
}
.log-arrow-btn {
background: var(--bg-vanilla-100);
color: var(--text-ink-400);
border: 1px solid var(--bg-vanilla-300);
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.04);
}
.log-arrow-btn-up,
.log-arrow-btn-down {
background: var(--bg-vanilla-100);
}
.log-arrow-btn:active,
.log-arrow-btn:focus {
background: var(--bg-vanilla-200);
color: var(--text-ink-500);
}
.log-arrow-btn:hover:not([disabled]) {
background: var(--bg-vanilla-200);
color: var(--text-ink-500);
}
.log-arrow-btn[disabled] {
background: var(--bg-vanilla-100);
color: var(--text-ink-200);
}
.ant-drawer-header {
border-bottom: 1px solid var(--bg-vanilla-400);
background: var(--bg-vanilla-100);
@@ -364,33 +252,4 @@
color: var(--text-ink-300);
}
}
.log-detail-drawer__footer-hint {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 8px 16px;
text-align: left;
color: var(--text-vanilla-700);
background: var(--bg-vanilla-100);
z-index: 10;
.log-detail-drawer__footer-hint-content {
display: flex;
align-items: center;
gap: 4px;
}
.log-detail-drawer__footer-hint-icon {
display: inline;
vertical-align: middle;
color: var(--text-vanilla-700);
}
.log-detail-drawer__footer-hint-text {
font-size: 13px;
margin: 0;
}
}
}

View File

@@ -1,5 +1,5 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useCopyToClipboard, useLocation } from 'react-use';
import { Color, Spacing } from '@signozhq/design-tokens';
@@ -32,12 +32,8 @@ import { useSafeNavigate } from 'hooks/useSafeNavigate';
import createQueryParams from 'lib/createQueryParams';
import { cloneDeep } from 'lodash-es';
import {
ArrowDown,
ArrowUp,
BarChart2,
Braces,
ChevronDown,
ChevronUp,
Compass,
Copy,
Filter,
@@ -64,9 +60,6 @@ function LogDetailInner({
isListViewPanel = false,
listViewPanelSelectedFields,
handleChangeSelectedView,
logs,
onNavigateLog,
onScrollToLog,
}: LogDetailInnerProps): JSX.Element {
const initialContextQuery = useInitialQuery(log);
const [contextQuery, setContextQuery] = useState<Query | undefined>(
@@ -81,78 +74,6 @@ function LogDetailInner({
const [isEdit, setIsEdit] = useState<boolean>(false);
const { stagedQuery, updateAllQueriesOperators } = useQueryBuilder();
// Handle clicks outside to close drawer, except on explicitly ignored regions
useEffect(() => {
const handleClickOutside = (e: MouseEvent): void => {
const target = e.target as HTMLElement;
// Don't close if clicking on explicitly ignored regions
if (target.closest('[data-log-detail-ignore="true"]')) {
return;
}
// Close the drawer for any other outside click
onClose?.(e as any);
};
document.addEventListener('mousedown', handleClickOutside);
return (): void => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [onClose]);
// Keyboard navigation - handle up/down arrow keys
// Only listen when in OVERVIEW tab
useEffect(() => {
if (
!logs ||
!onNavigateLog ||
logs.length === 0 ||
selectedView !== VIEW_TYPES.OVERVIEW
) {
return;
}
const handleKeyDown = (e: KeyboardEvent): void => {
const currentIndex = logs.findIndex((l) => l.id === log.id);
if (currentIndex === -1) {
return;
}
if (e.key === 'ArrowUp') {
e.preventDefault();
e.stopPropagation();
// Navigate to previous log
if (currentIndex > 0) {
const prevLog = logs[currentIndex - 1];
onNavigateLog(prevLog);
// Trigger scroll to the log element
if (onScrollToLog) {
onScrollToLog(prevLog.id);
}
}
} else if (e.key === 'ArrowDown') {
e.preventDefault();
e.stopPropagation();
// Navigate to next log
if (currentIndex < logs.length - 1) {
const nextLog = logs[currentIndex + 1];
onNavigateLog(nextLog);
// Trigger scroll to the log element
if (onScrollToLog) {
onScrollToLog(nextLog.id);
}
}
}
};
document.addEventListener('keydown', handleKeyDown);
return (): void => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [log.id, logs, onNavigateLog, onScrollToLog, selectedView]);
const listQuery = useMemo(() => {
if (!stagedQuery || stagedQuery.builder.queryData.length < 1) {
return null;
@@ -306,87 +227,32 @@ function LogDetailInner({
);
const logType = log?.attributes_string?.log_level || LogType.INFO;
const currentLogIndex = logs ? logs.findIndex((l) => l.id === log.id) : -1;
const isPrevDisabled =
!logs || !onNavigateLog || logs.length === 0 || currentLogIndex <= 0;
const isNextDisabled =
!logs ||
!onNavigateLog ||
logs.length === 0 ||
currentLogIndex === logs.length - 1;
type HandleNavigateLogParams = {
direction: 'next' | 'previous';
};
const handleNavigateLog = ({ direction }: HandleNavigateLogParams): void => {
if (!logs || !onNavigateLog || currentLogIndex === -1) {
return;
}
if (direction === 'previous' && !isPrevDisabled) {
const prevLog = logs[currentLogIndex - 1];
onNavigateLog(prevLog);
onScrollToLog?.(prevLog.id);
} else if (direction === 'next' && !isNextDisabled) {
const nextLog = logs[currentLogIndex + 1];
onNavigateLog(nextLog);
onScrollToLog?.(nextLog.id);
}
};
return (
<Drawer
width="60%"
mask={false}
maskClosable={false}
maskStyle={{ background: 'none' }}
title={
<div className="log-detail-drawer__title" data-log-detail-ignore="true">
<div className="log-detail-drawer__title">
<div className="log-detail-drawer__title-left">
<Divider type="vertical" className={cx('log-type-indicator', LogType)} />
<Typography.Text className="title">Log details</Typography.Text>
</div>
<div className="log-detail-drawer__title-right">
<div className="log-arrows">
<Tooltip
title={isPrevDisabled ? '' : 'Move to previous log'}
placement="top"
mouseLeaveDelay={0}
{showOpenInExplorerBtn && (
<div className="log-detail-drawer__title-right">
<Button
className="open-in-explorer-btn"
icon={<Compass size={16} />}
onClick={handleOpenInExplorer}
>
<Button
icon={<ChevronUp size={14} />}
className="log-arrow-btn log-arrow-btn-up"
disabled={isPrevDisabled}
onClick={(): void => handleNavigateLog({ direction: 'previous' })}
/>
</Tooltip>
<Tooltip
title={isNextDisabled ? '' : 'Move to next log'}
placement="top"
mouseLeaveDelay={0}
>
<Button
icon={<ChevronDown size={14} />}
className="log-arrow-btn log-arrow-btn-down"
disabled={isNextDisabled}
onClick={(): void => handleNavigateLog({ direction: 'next' })}
/>
</Tooltip>
Open in Explorer
</Button>
</div>
{showOpenInExplorerBtn && (
<div>
<Button
className="open-in-explorer-btn"
icon={<Compass size={16} />}
onClick={handleOpenInExplorer}
>
Open in Explorer
</Button>
</div>
)}
</div>
)}
</div>
}
placement="right"
// closable
onClose={drawerCloseHandler}
open={log !== null}
style={{
@@ -397,164 +263,138 @@ function LogDetailInner({
destroyOnClose
closeIcon={<X size={16} style={{ marginTop: Spacing.MARGIN_1 }} />}
>
<div className="log-detail-drawer__content" data-log-detail-ignore="true">
<div className="log-detail-drawer__log">
<Divider type="vertical" className={cx('log-type-indicator', logType)} />
<Tooltip title={removeEscapeCharacters(log?.body)} placement="left">
<div className="log-body" dangerouslySetInnerHTML={htmlBody} />
</Tooltip>
<div className="log-detail-drawer__log">
<Divider type="vertical" className={cx('log-type-indicator', logType)} />
<Tooltip title={removeEscapeCharacters(log?.body)} placement="left">
<div className="log-body" dangerouslySetInnerHTML={htmlBody} />
</Tooltip>
<div className="log-overflow-shadow">&nbsp;</div>
</div>
<div className="log-overflow-shadow">&nbsp;</div>
</div>
<div className="tabs-and-search">
<Radio.Group
className="views-tabs"
onChange={handleModeChange}
value={selectedView}
<div className="tabs-and-search">
<Radio.Group
className="views-tabs"
onChange={handleModeChange}
value={selectedView}
>
<Radio.Button
className={
// eslint-disable-next-line sonarjs/no-duplicate-string
selectedView === VIEW_TYPES.OVERVIEW ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.OVERVIEW}
>
<Radio.Button
className={
// eslint-disable-next-line sonarjs/no-duplicate-string
selectedView === VIEW_TYPES.OVERVIEW ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.OVERVIEW}
>
<div className="view-title">
<Table size={14} />
Overview
</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.JSON ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.JSON}
>
<div className="view-title">
<Braces size={14} />
JSON
</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.CONTEXT ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.CONTEXT}
>
<div className="view-title">
<TextSelect size={14} />
Context
</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.INFRAMETRICS ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.INFRAMETRICS}
>
<div className="view-title">
<BarChart2 size={14} />
Metrics
</div>
</Radio.Button>
</Radio.Group>
<div className="log-detail-drawer__actions">
{selectedView === VIEW_TYPES.CONTEXT && (
<Tooltip
title="Show Filters"
placement="topLeft"
aria-label="Show Filters"
>
<Button
className="action-btn"
icon={<Filter size={16} />}
onClick={handleFilterVisible}
/>
</Tooltip>
)}
<div className="view-title">
<Table size={14} />
Overview
</div>
</Radio.Button>
<Radio.Button
className={selectedView === VIEW_TYPES.JSON ? 'selected_view tab' : 'tab'}
value={VIEW_TYPES.JSON}
>
<div className="view-title">
<Braces size={14} />
JSON
</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.CONTEXT ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.CONTEXT}
>
<div className="view-title">
<TextSelect size={14} />
Context
</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.INFRAMETRICS ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.INFRAMETRICS}
>
<div className="view-title">
<BarChart2 size={14} />
Metrics
</div>
</Radio.Button>
</Radio.Group>
<div className="log-detail-drawer__actions">
{selectedView === VIEW_TYPES.CONTEXT && (
<Tooltip
title={selectedView === VIEW_TYPES.JSON ? 'Copy JSON' : 'Copy Log Link'}
title="Show Filters"
placement="topLeft"
aria-label={
selectedView === VIEW_TYPES.JSON ? 'Copy JSON' : 'Copy Log Link'
}
aria-label="Show Filters"
>
<Button
className="action-btn"
icon={<Copy size={16} />}
onClick={selectedView === VIEW_TYPES.JSON ? handleJSONCopy : onLogCopy}
icon={<Filter size={16} />}
onClick={handleFilterVisible}
/>
</Tooltip>
</div>
</div>
{isFilterVisible && contextQuery?.builder.queryData[0] && (
<div className="log-detail-drawer-query-container">
<QuerySearch
onChange={(value): void => handleQueryExpressionChange(value, 0)}
dataSource={DataSource.LOGS}
queryData={contextQuery?.builder.queryData[0]}
onRun={handleRunQuery}
)}
<Tooltip
title={selectedView === VIEW_TYPES.JSON ? 'Copy JSON' : 'Copy Log Link'}
placement="topLeft"
aria-label={
selectedView === VIEW_TYPES.JSON ? 'Copy JSON' : 'Copy Log Link'
}
>
<Button
className="action-btn"
icon={<Copy size={16} />}
onClick={selectedView === VIEW_TYPES.JSON ? handleJSONCopy : onLogCopy}
/>
</div>
)}
{selectedView === VIEW_TYPES.OVERVIEW && (
<Overview
logData={log}
onAddToQuery={onAddToQuery}
onClickActionItem={onClickActionItem}
isListViewPanel={isListViewPanel}
selectedOptions={options}
listViewPanelSelectedFields={listViewPanelSelectedFields}
handleChangeSelectedView={handleChangeSelectedView}
/>
)}
{selectedView === VIEW_TYPES.JSON && <JSONView logData={log} />}
{selectedView === VIEW_TYPES.CONTEXT && (
<ContextView
log={log}
filters={filters}
contextQuery={contextQuery}
isEdit={isEdit}
/>
)}
{selectedView === VIEW_TYPES.INFRAMETRICS && (
<InfraMetrics
clusterName={log.resources_string?.[RESOURCE_KEYS.CLUSTER_NAME] || ''}
podName={log.resources_string?.[RESOURCE_KEYS.POD_NAME] || ''}
nodeName={log.resources_string?.[RESOURCE_KEYS.NODE_NAME] || ''}
hostName={log.resources_string?.[RESOURCE_KEYS.HOST_NAME] || ''}
timestamp={log.timestamp.toString()}
dataSource={DataSource.LOGS}
/>
)}
{selectedView === VIEW_TYPES.OVERVIEW && (
<div className="log-detail-drawer__footer-hint">
<div className="log-detail-drawer__footer-hint-content">
<Typography.Text
type="secondary"
className="log-detail-drawer__footer-hint-text"
>
Use
</Typography.Text>
<ArrowUp size={14} className="log-detail-drawer__footer-hint-icon" />
<span>/</span>
<ArrowDown size={14} className="log-detail-drawer__footer-hint-icon" />
<Typography.Text
type="secondary"
className="log-detail-drawer__footer-hint-text"
>
to view previous/next log
</Typography.Text>
</div>
</div>
)}
</Tooltip>
</div>
</div>
{isFilterVisible && contextQuery?.builder.queryData[0] && (
<div className="log-detail-drawer-query-container">
<QuerySearch
onChange={(value): void => handleQueryExpressionChange(value, 0)}
dataSource={DataSource.LOGS}
queryData={contextQuery?.builder.queryData[0]}
onRun={handleRunQuery}
/>
</div>
)}
{selectedView === VIEW_TYPES.OVERVIEW && (
<Overview
logData={log}
onAddToQuery={onAddToQuery}
onClickActionItem={onClickActionItem}
isListViewPanel={isListViewPanel}
selectedOptions={options}
listViewPanelSelectedFields={listViewPanelSelectedFields}
handleChangeSelectedView={handleChangeSelectedView}
/>
)}
{selectedView === VIEW_TYPES.JSON && <JSONView logData={log} />}
{selectedView === VIEW_TYPES.CONTEXT && (
<ContextView
log={log}
filters={filters}
contextQuery={contextQuery}
isEdit={isEdit}
/>
)}
{selectedView === VIEW_TYPES.INFRAMETRICS && (
<InfraMetrics
clusterName={log.resources_string?.[RESOURCE_KEYS.CLUSTER_NAME] || ''}
podName={log.resources_string?.[RESOURCE_KEYS.POD_NAME] || ''}
nodeName={log.resources_string?.[RESOURCE_KEYS.NODE_NAME] || ''}
hostName={log.resources_string?.[RESOURCE_KEYS.HOST_NAME] || ''}
timestamp={log.timestamp.toString()}
dataSource={DataSource.LOGS}
/>
)}
</Drawer>
);
}

View File

@@ -2,11 +2,13 @@ import { memo, useCallback, useMemo } from 'react';
import { blue } from '@ant-design/colors';
import { Typography } from 'antd';
import cx from 'classnames';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { getSanitizedLogBody } from 'container/LogDetailedView/utils';
import { FontSize } from 'container/OptionsMenu/types';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useIsDarkMode } from 'hooks/useDarkMode';
// utils
@@ -102,17 +104,12 @@ function LogSelectedField({
type ListLogViewProps = {
logData: ILog;
selectedFields: IField[];
onSetActiveLog: (
log: ILog,
selectedTab?: typeof VIEW_TYPES[keyof typeof VIEW_TYPES],
) => void;
onSetActiveLog: (log: ILog) => void;
onAddToQuery: AddToQueryHOCProps['onAddToQuery'];
activeLog?: ILog | null;
linesPerRow: number;
fontSize: FontSize;
handleChangeSelectedView?: ChangeViewFunctionType;
isActiveLog?: boolean;
onClearActiveLog?: () => void;
};
function ListLogView({
@@ -123,8 +120,7 @@ function ListLogView({
activeLog,
linesPerRow,
fontSize,
isActiveLog,
onClearActiveLog,
handleChangeSelectedView,
}: ListLogViewProps): JSX.Element {
const flattenLogData = useMemo(() => FlatLogData(logData), [logData]);
@@ -133,24 +129,35 @@ function ListLogView({
);
const isReadOnlyLog = !isLogsExplorerPage;
const {
activeLog: activeContextLog,
onAddToQuery: handleAddToQuery,
onSetActiveLog: handleSetActiveContextLog,
onClearActiveLog: handleClearActiveContextLog,
} = useActiveLog();
const isDarkMode = useIsDarkMode();
const handleDetailedView = useCallback(() => {
if (isActiveLog) {
onClearActiveLog?.();
return;
}
const handlerClearActiveContextLog = useCallback(
(event: React.MouseEvent | React.KeyboardEvent) => {
event.preventDefault();
event.stopPropagation();
handleClearActiveContextLog();
},
[handleClearActiveContextLog],
);
const handleDetailedView = useCallback(() => {
onSetActiveLog(logData);
}, [logData, onSetActiveLog, isActiveLog, onClearActiveLog]);
}, [logData, onSetActiveLog]);
const handleShowContext = useCallback(
(event: React.MouseEvent) => {
event.preventDefault();
event.stopPropagation();
onSetActiveLog(logData, VIEW_TYPES.CONTEXT);
handleSetActiveContextLog(logData);
},
[logData, onSetActiveLog],
[logData, handleSetActiveContextLog],
);
const updatedSelecedFields = useMemo(
@@ -179,7 +186,11 @@ function ListLogView({
return (
<>
<Container
$isActiveLog={isHighlighted || activeLog?.id === logData.id}
$isActiveLog={
isHighlighted ||
activeLog?.id === logData.id ||
activeContextLog?.id === logData.id
}
$isDarkMode={isDarkMode}
$logType={logType}
onClick={handleDetailedView}
@@ -240,6 +251,15 @@ function ListLogView({
/>
)}
</Container>
{activeContextLog && (
<LogDetail
log={activeContextLog}
onAddToQuery={handleAddToQuery}
selectedTab={VIEW_TYPES.CONTEXT}
onClose={handlerClearActiveContextLog}
handleChangeSelectedView={handleChangeSelectedView}
/>
)}
</>
);
}

View File

@@ -1,15 +1,19 @@
import {
KeyboardEvent,
memo,
MouseEvent,
MouseEventHandler,
useCallback,
useMemo,
useState,
} from 'react';
import { Color } from '@signozhq/design-tokens';
import { Tooltip } from 'antd';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import { DrawerProps, Tooltip } from 'antd';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES, VIEWS } from 'components/LogDetail/constants';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { getSanitizedLogBody } from 'container/LogDetailedView/utils';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
// hooks
import { useIsDarkMode } from 'hooks/useDarkMode';
@@ -35,8 +39,7 @@ function RawLogView({
selectedFields = [],
fontSize,
onLogClick,
onSetActiveLog,
onClearActiveLog,
handleChangeSelectedView,
}: RawLogViewProps): JSX.Element {
const {
isHighlighted: isUrlHighlighted,
@@ -45,6 +48,15 @@ function RawLogView({
} = useCopyLogLink(data.id);
const flattenLogData = useMemo(() => FlatLogData(data), [data]);
const {
activeLog,
onSetActiveLog,
onClearActiveLog,
onAddToQuery,
} = useActiveLog();
const [selectedTab, setSelectedTab] = useState<VIEWS | undefined>();
const isDarkMode = useIsDarkMode();
const isReadOnlyLog = !isLogsExplorerPage || isReadOnly;
@@ -122,24 +134,34 @@ function RawLogView({
// Use custom click handler if provided, otherwise use default behavior
if (onLogClick) {
onLogClick(data, event);
return;
} else {
onSetActiveLog(data);
setSelectedTab(VIEW_TYPES.OVERVIEW);
}
if (isActiveLog) {
onClearActiveLog?.();
return;
}
onSetActiveLog?.(data);
},
[isReadOnly, onLogClick, isActiveLog, onSetActiveLog, data, onClearActiveLog],
[isReadOnly, data, onSetActiveLog, onLogClick],
);
const handleCloseLogDetail: DrawerProps['onClose'] = useCallback(
(
event: MouseEvent<Element, globalThis.MouseEvent> | KeyboardEvent<Element>,
) => {
event.preventDefault();
event.stopPropagation();
onClearActiveLog();
setSelectedTab(undefined);
},
[onClearActiveLog],
);
const handleShowContext: MouseEventHandler<HTMLElement> = useCallback(
(event) => {
event.preventDefault();
event.stopPropagation();
onSetActiveLog?.(data, VIEW_TYPES.CONTEXT);
// handleSetActiveContextLog(data);
setSelectedTab(VIEW_TYPES.CONTEXT);
onSetActiveLog(data);
},
[data, onSetActiveLog],
);
@@ -159,7 +181,7 @@ function RawLogView({
$isDarkMode={isDarkMode}
$isReadOnly={isReadOnly}
$isHightlightedLog={isUrlHighlighted}
$isActiveLog={isActiveLog}
$isActiveLog={activeLog?.id === data.id || isActiveLog}
$isCustomHighlighted={isHighlighted}
$logType={logType}
fontSize={fontSize}
@@ -196,6 +218,17 @@ function RawLogView({
onLogCopy={onLogCopy}
/>
)}
{selectedTab && (
<LogDetail
selectedTab={selectedTab}
log={activeLog}
onClose={handleCloseLogDetail}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
handleChangeSelectedView={handleChangeSelectedView}
/>
)}
</RawLogViewContainer>
);
}

View File

@@ -45,6 +45,9 @@ export const RawLogViewContainer = styled(Row)<{
: `margin: 2px 0;`}
}
${({ $isActiveLog, $logType }): string =>
getActiveLogBackground($isActiveLog, true, $logType)}
${({ $isReadOnly, $isActiveLog, $isDarkMode, $logType }): string =>
$isActiveLog
? getActiveLogBackground($isActiveLog, $isDarkMode, $logType)

View File

@@ -1,5 +1,4 @@
import { MouseEvent } from 'react';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { FontSize } from 'container/OptionsMenu/types';
import { IField } from 'types/api/logs/fields';
@@ -17,11 +16,6 @@ export interface RawLogViewProps {
selectedFields?: IField[];
onLogClick?: (log: ILog, event: MouseEvent) => void;
handleChangeSelectedView?: ChangeViewFunctionType;
onSetActiveLog?: (
log: ILog,
selectedTab?: typeof VIEW_TYPES[keyof typeof VIEW_TYPES],
) => void;
onClearActiveLog?: () => void;
}
export interface RawLogContentProps {

View File

@@ -14,6 +14,6 @@ export const VIEW_TYPES = {
export const SPAN_ATTRIBUTES = {
URL_PATH: 'http.url',
RESPONSE_STATUS_CODE: 'response_status_code',
SERVER_NAME: 'http_host',
SERVER_NAME: 'net.peer.name',
SERVER_PORT: 'net.peer.port',
} as const;

View File

@@ -4,7 +4,6 @@ import { rest, server } from 'mocks-server/server';
import { fireEvent, render, screen, waitFor, within } from 'tests/test-utils';
import { DataSource } from 'types/common/queryBuilder';
import { SPAN_ATTRIBUTES } from '../Explorer/Domains/DomainDetails/constants';
import TopErrors from '../Explorer/Domains/DomainDetails/TopErrors';
import { getTopErrorsQueryPayload } from '../utils';
@@ -216,7 +215,7 @@ describe('TopErrors', () => {
value: 'true',
}),
expect.objectContaining({
key: expect.objectContaining({ key: SPAN_ATTRIBUTES.SERVER_NAME }),
key: expect.objectContaining({ key: 'net.peer.name' }),
op: '=',
value: 'test-domain',
}),

View File

@@ -638,7 +638,7 @@ export const getEndPointsQueryPayload = (
key: {
dataType: DataTypes.String,
key: SPAN_ATTRIBUTES.SERVER_NAME,
type: '',
type: 'tag',
},
op: '=',
value: domainName,
@@ -685,7 +685,7 @@ export const getEndPointsQueryPayload = (
key: {
dataType: DataTypes.String,
key: SPAN_ATTRIBUTES.SERVER_NAME,
type: '',
type: 'tag',
},
op: '=',
value: domainName,
@@ -733,7 +733,7 @@ export const getEndPointsQueryPayload = (
key: {
dataType: DataTypes.String,
key: SPAN_ATTRIBUTES.SERVER_NAME,
type: '',
type: 'tag',
},
op: '=',
value: domainName,
@@ -780,7 +780,7 @@ export const getEndPointsQueryPayload = (
key: {
dataType: DataTypes.String,
key: SPAN_ATTRIBUTES.SERVER_NAME,
type: '',
type: 'tag',
},
op: '=',
value: domainName,
@@ -1302,7 +1302,7 @@ export const getTopErrorsCoRelationQueryFilters = (
{
id: 'e8a043b7',
key: {
key: SPAN_ATTRIBUTES.SERVER_NAME,
key: 'net.peer.name',
dataType: DataTypes.String,
type: '',
},
@@ -2198,7 +2198,7 @@ export const getEndPointZeroStateQueryPayload = (
key: {
key: SPAN_ATTRIBUTES.SERVER_NAME,
dataType: DataTypes.String,
type: '',
type: 'tag',
},
op: '=',
value: domainName,
@@ -2793,7 +2793,7 @@ export const getStatusCodeBarChartWidgetData = (
key: {
dataType: DataTypes.String,
key: SPAN_ATTRIBUTES.SERVER_NAME,
type: '',
type: 'tag',
},
op: '=',
value: domainName,

View File

@@ -50,10 +50,6 @@
}
.variable-select {
.ant-select-selector {
overflow-y: hidden !important;
}
.ant-select-item {
display: flex;
align-items: center;

View File

@@ -1,4 +1,3 @@
import { ExecStats } from 'api/v5/v5';
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { getInitialStackedBands } from 'container/DashboardContainer/visualization/charts/utils/stackSeriesUtils';
@@ -55,13 +54,6 @@ export function prepareBarPanelConfig({
minTimeScale?: number;
maxTimeScale?: number;
}): UPlotConfigBuilder {
const stepIntervals: ExecStats['stepIntervals'] = get(
apiResponse,
'data.newResult.meta.stepIntervals',
{},
);
const minStepInterval = Math.min(...Object.values(stepIntervals));
const builder = buildBaseConfig({
widget,
isDarkMode,
@@ -73,7 +65,12 @@ export function prepareBarPanelConfig({
panelType: PANEL_TYPES.BAR,
minTimeScale,
maxTimeScale,
stepInterval: minStepInterval,
});
builder.setCursor({
focus: {
prox: 1e3,
},
});
if (widget.stackedBarChart) {
@@ -81,6 +78,12 @@ export function prepareBarPanelConfig({
builder.setBands(getInitialStackedBands(seriesCount));
}
const stepIntervals: Record<string, number> = get(
apiResponse,
'data.newResult.meta.stepIntervals',
{},
);
const seriesList: QueryData[] = apiResponse?.data?.result || [];
seriesList.forEach((series) => {
const baseLabelName = getLabelName(

View File

@@ -1,4 +1,3 @@
import { ExecStats } from 'api/v5/v5';
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
import { PANEL_TYPES } from 'constants/queryBuilder';
import {
@@ -15,7 +14,6 @@ import {
VisibilityMode,
} from 'lib/uPlotV2/config/types';
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
import get from 'lodash-es/get';
import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
@@ -56,13 +54,6 @@ export const prepareUPlotConfig = ({
minTimeScale?: number;
maxTimeScale?: number;
}): UPlotConfigBuilder => {
const stepIntervals: ExecStats['stepIntervals'] = get(
apiResponse,
'data.newResult.meta.stepIntervals',
{},
);
const minStepInterval = Math.min(...Object.values(stepIntervals));
const builder = buildBaseConfig({
widget,
isDarkMode,
@@ -74,12 +65,9 @@ export const prepareUPlotConfig = ({
panelType: PANEL_TYPES.TIME_SERIES,
minTimeScale,
maxTimeScale,
stepInterval: minStepInterval,
});
const seriesList = apiResponse.data?.result || [];
seriesList.forEach((series) => {
apiResponse.data?.result?.forEach((series) => {
const baseLabelName = getLabelName(
series.metric,
series.queryName || '', // query

View File

@@ -1,233 +0,0 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { STEP_INTERVAL_MULTIPLIER } from 'lib/uPlotV2/constants';
import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import uPlot from 'uplot';
import { PanelMode } from '../../types';
import { buildBaseConfig } from '../baseConfigBuilder';
jest.mock(
'container/DashboardContainer/visualization/panels/utils/legendVisibilityUtils',
() => ({
getStoredSeriesVisibility: jest.fn(),
}),
);
jest.mock('lib/uPlotV2/utils', () => ({
calculateWidthBasedOnStepInterval: jest.fn(),
}));
const calculateWidthBasedOnStepIntervalMock = jest.requireMock(
'lib/uPlotV2/utils',
).calculateWidthBasedOnStepInterval as jest.Mock;
jest.mock('lib/uPlotLib/plugins/onClickPlugin', () => ({
__esModule: true,
default: jest.fn().mockReturnValue({ name: 'onClickPlugin' }),
}));
const createWidget = (overrides: Partial<Widgets> = {}): Widgets =>
({
id: 'widget-1',
yAxisUnit: 'ms',
isLogScale: false,
softMin: undefined,
softMax: undefined,
thresholds: [],
...overrides,
} as Widgets);
const createApiResponse = (
overrides: Partial<MetricRangePayloadProps> = {},
): MetricRangePayloadProps =>
({
data: { result: [], resultType: 'matrix', newResult: null },
...overrides,
} as MetricRangePayloadProps);
const baseProps = {
widget: createWidget(),
apiResponse: createApiResponse(),
isDarkMode: true,
panelMode: PanelMode.DASHBOARD_VIEW,
panelType: PANEL_TYPES.TIME_SERIES,
};
describe('buildBaseConfig', () => {
it('returns a UPlotConfigBuilder instance', () => {
const builder = buildBaseConfig(baseProps);
expect(builder).toBeDefined();
expect(typeof builder.getConfig).toBe('function');
expect(typeof builder.getLegendItems).toBe('function');
});
it('configures builder with widgetId and DASHBOARD_VIEW preferences', () => {
const builder = buildBaseConfig({
...baseProps,
panelMode: PanelMode.DASHBOARD_VIEW,
widget: createWidget({ id: 'my-widget' }),
});
expect(builder.getWidgetId()).toBe('my-widget');
expect(builder.getShouldSaveSelectionPreference()).toBe(true);
});
it('configures builder with IN_MEMORY selection when panelMode is DASHBOARD_EDIT', () => {
const builder = buildBaseConfig({
...baseProps,
panelMode: PanelMode.DASHBOARD_EDIT,
});
expect(builder.getShouldSaveSelectionPreference()).toBe(false);
const config = builder.getConfig();
expect(config.series).toBeDefined();
});
it('passes stepInterval to builder and cursor prox uses width * multiplier', () => {
const stepInterval = 60;
const mockWidth = 100;
calculateWidthBasedOnStepIntervalMock.mockReturnValue(mockWidth);
const builder = buildBaseConfig({
...baseProps,
stepInterval,
});
const config = builder.getConfig();
const prox = config.cursor?.hover?.prox;
expect(typeof prox).toBe('function');
const uPlotInstance = {} as uPlot;
const proxResult = (prox as (u: uPlot) => number)(uPlotInstance);
expect(calculateWidthBasedOnStepIntervalMock).toHaveBeenCalledWith({
uPlotInstance,
stepInterval,
});
expect(proxResult).toBe(mockWidth * STEP_INTERVAL_MULTIPLIER);
});
it('adds x scale with time config and min/max when provided', () => {
const builder = buildBaseConfig({
...baseProps,
minTimeScale: 1000,
maxTimeScale: 2000,
});
const config = builder.getConfig();
expect(config.scales?.x).toBeDefined();
expect(config.scales?.x?.time).toBe(true);
const range = config.scales?.x?.range;
expect(Array.isArray(range)).toBe(true);
expect((range as [number, number])[0]).toBe(1000);
});
it('configures log scale on y axis when widget.isLogScale is true', () => {
const builder = buildBaseConfig({
...baseProps,
widget: createWidget({ isLogScale: true }),
});
const config = builder.getConfig();
expect(config.scales?.y).toBeDefined();
expect(config.scales?.y?.log).toBe(10);
});
it('adds onClick plugin when onClick is a function', () => {
const onClickPlugin = jest.requireMock('lib/uPlotLib/plugins/onClickPlugin')
.default;
const onClick = jest.fn();
buildBaseConfig({
...baseProps,
onClick,
apiResponse: createApiResponse(),
});
expect(onClickPlugin).toHaveBeenCalledWith({
onClick,
apiResponse: expect.any(Object),
});
});
it('does not add onClick plugin when onClick is not a function', () => {
const onClickPlugin = jest.requireMock('lib/uPlotLib/plugins/onClickPlugin')
.default;
const builder = buildBaseConfig({
...baseProps,
});
const config = builder.getConfig();
const plugins = config.plugins ?? [];
expect(
plugins.some((p) => (p as { name?: string }).name === 'onClickPlugin'),
).toBe(false);
expect(onClickPlugin).not.toHaveBeenCalled();
});
it('adds thresholds from widget', () => {
const builder = buildBaseConfig({
...baseProps,
widget: createWidget({
thresholds: [
{
thresholdValue: 80,
thresholdColor: '#ff0000',
thresholdUnit: 'ms',
thresholdLabel: 'High',
},
] as Widgets['thresholds'],
}),
});
const config = builder.getConfig();
const drawHooks = config.hooks?.draw ?? [];
expect(drawHooks.length).toBeGreaterThan(0);
});
it('adds x and y axes with correct scaleKeys and panelType', () => {
const builder = buildBaseConfig(baseProps);
const config = builder.getConfig();
expect(config.axes).toHaveLength(2);
expect(config.axes?.[0].scale).toBe('x');
expect(config.axes?.[1].scale).toBe('y');
});
it('sets tzDate when timezone is provided', () => {
const builder = buildBaseConfig({
...baseProps,
timezone: {
name: 'America/New_York',
value: 'America/New_York',
offset: 'UTC-5',
searchIndex: 'America/New_York',
},
});
const config = builder.getConfig();
expect(config.tzDate).toBeDefined();
expect(typeof config.tzDate).toBe('function');
});
it('leaves tzDate undefined when timezone is not provided', () => {
const builder = buildBaseConfig(baseProps);
const config = builder.getConfig();
expect(config.tzDate).toBeUndefined();
});
it('register setSelect hook when onDragSelect is provided', () => {
const onDragSelect = jest.fn();
const builder = buildBaseConfig({
...baseProps,
onDragSelect,
});
const config = builder.getConfig();
expect(config.hooks?.setSelect).toBeDefined();
});
});

View File

@@ -26,7 +26,6 @@ export interface BaseConfigBuilderProps {
panelType: PANEL_TYPES;
minTimeScale?: number;
maxTimeScale?: number;
stepInterval?: number;
}
export function buildBaseConfig({
@@ -40,7 +39,6 @@ export function buildBaseConfig({
panelType,
minTimeScale,
maxTimeScale,
stepInterval,
}: BaseConfigBuilderProps): UPlotConfigBuilder {
const tzDate = timezone
? (timestamp: number): Date =>
@@ -58,7 +56,6 @@ export function buildBaseConfig({
].includes(panelMode)
? SelectionPreferencesSource.LOCAL_STORAGE
: SelectionPreferencesSource.IN_MEMORY,
stepInterval,
});
const thresholdOptions: ThresholdsDrawHookOptions = {

View File

@@ -1,9 +1,8 @@
/* eslint-disable no-nested-ternary */
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { useCallback, useEffect, useMemo } from 'react';
import { useQuery } from 'react-query';
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
import { Virtuoso } from 'react-virtuoso';
import { Card } from 'antd';
import LogDetail from 'components/LogDetail';
import RawLogView from 'components/Logs/RawLogView';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
@@ -12,8 +11,6 @@ import LogsError from 'container/LogsError/LogsError';
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
import { FontSize } from 'container/OptionsMenu/types';
import { useHandleLogsPagination } from 'hooks/infraMonitoring/useHandleLogsPagination';
import useLogDetailHandlers from 'hooks/logs/useLogDetailHandlers';
import useScrollToLog from 'hooks/logs/useScrollToLog';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { ILog } from 'types/api/logs/log';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
@@ -43,15 +40,6 @@ function EntityLogs({
category,
queryKeyFilters,
}: Props): JSX.Element {
const virtuosoRef = useRef<VirtuosoHandle>(null);
const {
activeLog,
onAddToQuery,
selectedTab,
handleSetActiveLog,
handleCloseLogDetail,
} = useLogDetailHandlers();
const basePayload = getEntityEventsOrLogsQueryPayload(
timeRange.startTime,
timeRange.endTime,
@@ -74,40 +62,29 @@ function EntityLogs({
basePayload,
});
const handleScrollToLog = useScrollToLog({
logs,
virtuosoRef,
});
const getItemContent = useCallback(
(_: number, logToRender: ILog): JSX.Element => {
return (
<div key={logToRender.id}>
<RawLogView
isTextOverflowEllipsisDisabled
data={logToRender}
linesPerRow={5}
fontSize={FontSize.MEDIUM}
selectedFields={[
{
dataType: 'string',
type: '',
name: 'body',
},
{
dataType: 'string',
type: '',
name: 'timestamp',
},
]}
onSetActiveLog={handleSetActiveLog}
onClearActiveLog={handleCloseLogDetail}
isActiveLog={activeLog?.id === logToRender.id}
/>
</div>
);
},
[activeLog, handleSetActiveLog, handleCloseLogDetail],
(_: number, logToRender: ILog): JSX.Element => (
<RawLogView
isTextOverflowEllipsisDisabled
key={logToRender.id}
data={logToRender}
linesPerRow={5}
fontSize={FontSize.MEDIUM}
selectedFields={[
{
dataType: 'string',
type: '',
name: 'body',
},
{
dataType: 'string',
type: '',
name: 'timestamp',
},
]}
/>
),
[],
);
const { data, isLoading, isFetching, isError } = useQuery({
@@ -154,7 +131,6 @@ function EntityLogs({
<Virtuoso
className="entity-logs-virtuoso"
key="entity-logs-virtuoso"
ref={virtuosoRef}
data={logs}
endReached={loadMoreLogs}
totalCount={logs.length}
@@ -178,21 +154,7 @@ function EntityLogs({
)}
{isError && !isLoading && <LogsError />}
{!isLoading && !isError && logs.length > 0 && (
<div className="entity-logs-list-container" data-log-detail-ignore="true">
{renderContent}
</div>
)}
{selectedTab && activeLog && (
<LogDetail
log={activeLog}
onClose={handleCloseLogDetail}
logs={logs}
onNavigateLog={handleSetActiveLog}
selectedTab={selectedTab}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
onScrollToLog={handleScrollToLog}
/>
<div className="entity-logs-list-container">{renderContent}</div>
)}
</div>
);

View File

@@ -2,6 +2,7 @@ import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
import { Card, Typography } from 'antd';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import ListLogView from 'components/Logs/ListLogView';
import RawLogView from 'components/Logs/RawLogView';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
@@ -13,9 +14,8 @@ import { InfinityWrapperStyled } from 'container/LogsExplorerList/styles';
import { convertKeysToColumnFields } from 'container/LogsExplorerList/utils';
import { useOptionsMenu } from 'container/OptionsMenu';
import { defaultLogsSelectedColumns } from 'container/OptionsMenu/constants';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import useLogDetailHandlers from 'hooks/logs/useLogDetailHandlers';
import useScrollToLog from 'hooks/logs/useScrollToLog';
import { useEventSource } from 'providers/EventSource';
// interfaces
import { ILog } from 'types/api/logs/log';
@@ -38,11 +38,10 @@ function LiveLogsList({
const {
activeLog,
onClearActiveLog,
onAddToQuery,
selectedTab,
handleSetActiveLog,
handleCloseLogDetail,
} = useLogDetailHandlers();
onSetActiveLog,
} = useActiveLog();
// get only data from the logs object
const formattedLogs: ILog[] = useMemo(
@@ -66,56 +65,42 @@ function LiveLogsList({
...options.selectColumns,
]);
const handleScrollToLog = useScrollToLog({
logs: formattedLogs,
virtuosoRef: ref,
});
const getItemContent = useCallback(
(_: number, log: ILog): JSX.Element => {
if (options.format === 'raw') {
return (
<div key={log.id}>
<RawLogView
data={log}
isActiveLog={activeLog?.id === log.id}
linesPerRow={options.maxLines}
selectedFields={selectedFields}
fontSize={options.fontSize}
handleChangeSelectedView={handleChangeSelectedView}
onSetActiveLog={handleSetActiveLog}
onClearActiveLog={handleCloseLogDetail}
/>
</div>
<RawLogView
key={log.id}
data={log}
linesPerRow={options.maxLines}
selectedFields={selectedFields}
fontSize={options.fontSize}
handleChangeSelectedView={handleChangeSelectedView}
/>
);
}
return (
<div key={log.id}>
<ListLogView
logData={log}
isActiveLog={activeLog?.id === log.id}
selectedFields={selectedFields}
linesPerRow={options.maxLines}
onAddToQuery={onAddToQuery}
onSetActiveLog={handleSetActiveLog}
onClearActiveLog={handleCloseLogDetail}
fontSize={options.fontSize}
handleChangeSelectedView={handleChangeSelectedView}
/>
</div>
<ListLogView
key={log.id}
logData={log}
selectedFields={selectedFields}
linesPerRow={options.maxLines}
onAddToQuery={onAddToQuery}
onSetActiveLog={onSetActiveLog}
fontSize={options.fontSize}
handleChangeSelectedView={handleChangeSelectedView}
/>
);
},
[
handleChangeSelectedView,
onAddToQuery,
onSetActiveLog,
options.fontSize,
options.format,
options.maxLines,
options.fontSize,
activeLog?.id,
selectedFields,
onAddToQuery,
handleSetActiveLog,
handleCloseLogDetail,
handleChangeSelectedView,
],
);
@@ -171,10 +156,6 @@ function LiveLogsList({
activeLogIndex,
}}
handleChangeSelectedView={handleChangeSelectedView}
logs={formattedLogs}
onSetActiveLog={handleSetActiveLog}
onClearActiveLog={handleCloseLogDetail}
activeLog={activeLog}
/>
) : (
<Card style={{ width: '100%' }} bodyStyle={CARD_BODY_STYLE}>
@@ -192,17 +173,14 @@ function LiveLogsList({
</InfinityWrapperStyled>
)}
{activeLog && selectedTab && (
{activeLog && (
<LogDetail
selectedTab={selectedTab}
selectedTab={VIEW_TYPES.OVERVIEW}
log={activeLog}
onClose={handleCloseLogDetail}
onClose={onClearActiveLog}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
handleChangeSelectedView={handleChangeSelectedView}
logs={formattedLogs}
onNavigateLog={handleSetActiveLog}
onScrollToLog={handleScrollToLog}
/>
)}
</div>

View File

@@ -395,7 +395,7 @@ export default function TableViewActions(
onOpenChange={setIsOpen}
arrow={false}
content={
<div data-log-detail-ignore="true">
<div>
<Button
className="more-filter-actions"
type="text"
@@ -481,7 +481,7 @@ export default function TableViewActions(
onOpenChange={setIsOpen}
arrow={false}
content={
<div data-log-detail-ignore="true">
<div>
<Button
className="more-filter-actions"
type="text"

View File

@@ -7,7 +7,6 @@ import {
useMemo,
} from 'react';
import { ColumnsType } from 'antd/es/table';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import LogLinesActionButtons from 'components/Logs/LogLinesActionButtons/LogLinesActionButtons';
import { ColumnTypeRender } from 'components/Logs/TableView/types';
import { FontSize } from 'container/OptionsMenu/types';
@@ -23,27 +22,22 @@ interface TableRowProps {
tableColumns: ColumnsType<Record<string, unknown>>;
index: number;
log: Record<string, unknown>;
onShowLogDetails?: (
log: ILog,
selectedTab?: typeof VIEW_TYPES[keyof typeof VIEW_TYPES],
) => void;
handleSetActiveContextLog: (log: ILog) => void;
onShowLogDetails: (log: ILog) => void;
logs: ILog[];
hasActions: boolean;
fontSize: FontSize;
isActiveLog?: boolean;
onClearActiveLog?: () => void;
}
export default function TableRow({
tableColumns,
index,
log,
handleSetActiveContextLog,
onShowLogDetails,
logs,
hasActions,
fontSize,
isActiveLog,
onClearActiveLog,
}: TableRowProps): JSX.Element {
const isDarkMode = useIsDarkMode();
@@ -58,31 +52,21 @@ export default function TableRow({
(event) => {
event.preventDefault();
event.stopPropagation();
if (!currentLog) {
if (!handleSetActiveContextLog || !currentLog) {
return;
}
onShowLogDetails?.(currentLog, VIEW_TYPES.CONTEXT);
handleSetActiveContextLog(currentLog);
},
[currentLog, onShowLogDetails],
[currentLog, handleSetActiveContextLog],
);
const handleShowLogDetails = useCallback(() => {
if (!currentLog) {
if (!onShowLogDetails || !currentLog) {
return;
}
// If this log is already active, close the detail drawer
if (isActiveLog && onClearActiveLog) {
onClearActiveLog();
return;
}
// Otherwise, open the detail drawer for this log
if (onShowLogDetails) {
onShowLogDetails(currentLog);
}
}, [currentLog, onShowLogDetails, isActiveLog, onClearActiveLog]);
onShowLogDetails(currentLog);
}, [currentLog, onShowLogDetails]);
const hasSingleColumn =
tableColumns.filter((column) => column.key !== 'state-indicator').length ===

View File

@@ -4,6 +4,7 @@ import {
TableVirtuoso,
TableVirtuosoHandle,
} from 'react-virtuoso';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import { getLogIndicatorType } from 'components/Logs/LogStateIndicator/utils';
import { useTableView } from 'components/Logs/TableView/useTableView';
@@ -57,40 +58,26 @@ const CustomTableRow: TableComponents<ILog>['TableRow'] = ({
const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
function InfinityTableView(
{
isLoading,
tableViewProps,
infitiyTableProps,
onSetActiveLog,
onClearActiveLog,
activeLog,
},
{ isLoading, tableViewProps, infitiyTableProps, handleChangeSelectedView },
ref,
): JSX.Element | null {
const { activeLog: activeContextLog } = useActiveLog();
const onSetActiveLogExpand = useCallback(
(log: ILog) => {
onSetActiveLog?.(log);
},
[onSetActiveLog],
);
const onSetActiveLogContext = useCallback(
(log: ILog) => {
onSetActiveLog?.(log, VIEW_TYPES.CONTEXT);
},
[onSetActiveLog],
);
const onCloseActiveLog = useCallback(() => {
onClearActiveLog?.();
}, [onClearActiveLog]);
const {
activeLog: activeContextLog,
onSetActiveLog: handleSetActiveContextLog,
onClearActiveLog: handleClearActiveContextLog,
onAddToQuery: handleAddToQuery,
} = useActiveLog();
const {
activeLog,
onSetActiveLog,
onClearActiveLog,
onAddToQuery,
} = useActiveLog();
const { dataSource, columns } = useTableView({
...tableViewProps,
onClickExpand: onSetActiveLogExpand,
onOpenLogsContext: onSetActiveLogContext,
onClickExpand: onSetActiveLog,
onOpenLogsContext: handleSetActiveContextLog,
});
const { draggedColumns, onDragColumns } = useDragColumns<
@@ -111,32 +98,27 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
);
const itemContent = useCallback(
(index: number, log: Record<string, unknown>): JSX.Element => {
return (
<div key={log.id as string}>
<TableRow
tableColumns={tableColumns}
index={index}
log={log}
logs={tableViewProps.logs}
hasActions
fontSize={tableViewProps.fontSize}
onShowLogDetails={onSetActiveLog}
isActiveLog={activeLog?.id === log.id}
onClearActiveLog={onCloseActiveLog}
/>
</div>
);
},
(index: number, log: Record<string, unknown>): JSX.Element => (
<TableRow
tableColumns={tableColumns}
index={index}
log={log}
handleSetActiveContextLog={handleSetActiveContextLog}
logs={tableViewProps.logs}
hasActions
fontSize={tableViewProps.fontSize}
onShowLogDetails={onSetActiveLog}
/>
),
[
handleSetActiveContextLog,
tableColumns,
onSetActiveLog,
tableViewProps.logs,
tableViewProps.fontSize,
activeLog?.id,
onCloseActiveLog,
tableViewProps.logs,
onSetActiveLog,
],
);
const tableHeader = useCallback(
() => (
<tr>
@@ -197,6 +179,24 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
? { endReached: infitiyTableProps.onEndReached }
: {})}
/>
{activeContextLog && (
<LogDetail
log={activeContextLog}
onClose={handleClearActiveContextLog}
onAddToQuery={handleAddToQuery}
selectedTab={VIEW_TYPES.CONTEXT}
handleChangeSelectedView={handleChangeSelectedView}
/>
)}
<LogDetail
selectedTab={VIEW_TYPES.OVERVIEW}
log={activeLog}
onClose={onClearActiveLog}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
handleChangeSelectedView={handleChangeSelectedView}
/>
</>
);
},

View File

@@ -1,7 +1,5 @@
import { VIEW_TYPES } from 'components/LogDetail/constants';
import { UseTableViewProps } from 'components/Logs/TableView/types';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { ILog } from 'types/api/logs/log';
export type InfinityTableProps = {
isLoading?: boolean;
@@ -10,11 +8,4 @@ export type InfinityTableProps = {
onEndReached: (index: number) => void;
};
handleChangeSelectedView?: ChangeViewFunctionType;
logs?: ILog[];
onSetActiveLog?: (
log: ILog,
selectedTab?: typeof VIEW_TYPES[keyof typeof VIEW_TYPES],
) => void;
onClearActiveLog?: () => void;
activeLog?: ILog | null;
};

View File

@@ -4,6 +4,7 @@ import { Card } from 'antd';
import logEvent from 'api/common/logEvent';
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
// components
import ListLogView from 'components/Logs/ListLogView';
import RawLogView from 'components/Logs/RawLogView';
@@ -15,9 +16,8 @@ import EmptyLogsSearch from 'container/EmptyLogsSearch/EmptyLogsSearch';
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
import { useOptionsMenu } from 'container/OptionsMenu';
import { FontSize } from 'container/OptionsMenu/types';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import useLogDetailHandlers from 'hooks/logs/useLogDetailHandlers';
import useScrollToLog from 'hooks/logs/useScrollToLog';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import APIError from 'types/api/error';
// interfaces
@@ -55,11 +55,10 @@ function LogsExplorerList({
const {
activeLog,
onClearActiveLog,
onAddToQuery,
selectedTab,
handleSetActiveLog,
handleCloseLogDetail,
} = useLogDetailHandlers();
onSetActiveLog,
} = useActiveLog();
const { options } = useOptionsMenu({
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
@@ -83,12 +82,6 @@ function LogsExplorerList({
() => convertKeysToColumnFields(options.selectColumns),
[options],
);
const handleScrollToLog = useScrollToLog({
logs,
virtuosoRef: ref,
});
useEffect(() => {
if (!isLoading && !isFetching && !isError && logs.length !== 0) {
logEvent('Logs Explorer: Data present', {
@@ -101,48 +94,40 @@ function LogsExplorerList({
(_: number, log: ILog): JSX.Element => {
if (options.format === 'raw') {
return (
<div key={log.id}>
<RawLogView
data={log}
isActiveLog={activeLog?.id === log.id}
linesPerRow={options.maxLines}
selectedFields={selectedFields}
fontSize={options.fontSize}
handleChangeSelectedView={handleChangeSelectedView}
onSetActiveLog={handleSetActiveLog}
onClearActiveLog={handleCloseLogDetail}
/>
</div>
<RawLogView
key={log.id}
data={log}
linesPerRow={options.maxLines}
selectedFields={selectedFields}
fontSize={options.fontSize}
handleChangeSelectedView={handleChangeSelectedView}
/>
);
}
return (
<div key={log.id}>
<ListLogView
logData={log}
isActiveLog={activeLog?.id === log.id}
selectedFields={selectedFields}
onAddToQuery={onAddToQuery}
onSetActiveLog={handleSetActiveLog}
activeLog={activeLog}
fontSize={options.fontSize}
linesPerRow={options.maxLines}
handleChangeSelectedView={handleChangeSelectedView}
onClearActiveLog={handleCloseLogDetail}
/>
</div>
<ListLogView
key={log.id}
logData={log}
selectedFields={selectedFields}
onAddToQuery={onAddToQuery}
onSetActiveLog={onSetActiveLog}
activeLog={activeLog}
fontSize={options.fontSize}
linesPerRow={options.maxLines}
handleChangeSelectedView={handleChangeSelectedView}
/>
);
},
[
options.format,
options.fontSize,
options.maxLines,
activeLog,
selectedFields,
onAddToQuery,
handleSetActiveLog,
handleChangeSelectedView,
handleCloseLogDetail,
onAddToQuery,
onSetActiveLog,
options.fontSize,
options.format,
options.maxLines,
selectedFields,
],
);
@@ -168,10 +153,6 @@ function LogsExplorerList({
}}
infitiyTableProps={{ onEndReached }}
handleChangeSelectedView={handleChangeSelectedView}
logs={logs}
onSetActiveLog={handleSetActiveLog}
onClearActiveLog={handleCloseLogDetail}
activeLog={activeLog}
/>
);
}
@@ -218,9 +199,6 @@ function LogsExplorerList({
getItemContent,
selectedFields,
handleChangeSelectedView,
handleSetActiveLog,
handleCloseLogDetail,
activeLog,
]);
const isTraceToLogsNavigation = useMemo(() => {
@@ -300,19 +278,14 @@ function LogsExplorerList({
{renderContent}
</InfinityWrapperStyled>
{selectedTab && activeLog && (
<LogDetail
selectedTab={selectedTab}
log={activeLog}
onClose={handleCloseLogDetail}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
handleChangeSelectedView={handleChangeSelectedView}
logs={logs}
onNavigateLog={handleSetActiveLog}
onScrollToLog={handleScrollToLog}
/>
)}
<LogDetail
selectedTab={VIEW_TYPES.OVERVIEW}
log={activeLog}
onClose={onClearActiveLog}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
handleChangeSelectedView={handleChangeSelectedView}
/>
</>
)}
</div>

View File

@@ -466,10 +466,7 @@ function LogsExplorerViewsContainer({
</div>
)}
<div
className="logs-explorer-views-type-content"
data-log-detail-ignore="true"
>
<div className="logs-explorer-views-type-content">
{showLiveLogs && (
<LiveLogs handleChangeSelectedView={handleChangeSelectedView} />
)}

View File

@@ -8,6 +8,7 @@ import {
} from 'react';
import { UseQueryResult } from 'react-query';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { ResizeTable } from 'components/ResizeTable';
import { SOMETHING_WENT_WRONG } from 'constants/api';
@@ -15,7 +16,7 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
import Controls from 'container/Controls';
import { PER_PAGE_OPTIONS } from 'container/TracesExplorer/ListView/configs';
import { tableStyles } from 'container/TracesExplorer/ListView/styles';
import useLogDetailHandlers from 'hooks/logs/useLogDetailHandlers';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useLogsData } from 'hooks/useLogsData';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { FlatLogData } from 'lib/logs/flatLogData';
@@ -82,24 +83,24 @@ function LogsPanelComponent({
() => logs.map((log) => FlatLogData(log) as RowData),
[logs],
);
const {
activeLog,
onSetActiveLog,
onClearActiveLog,
onAddToQuery,
selectedTab,
handleSetActiveLog,
handleCloseLogDetail,
} = useLogDetailHandlers();
} = useActiveLog();
const handleRow = useCallback(
(record: RowData): HTMLAttributes<RowData> => ({
onClick: (): void => {
const log = logs.find((item) => item.id === record.id);
if (log) {
handleSetActiveLog(log);
onSetActiveLog(log);
}
},
}),
[handleSetActiveLog, logs],
[logs, onSetActiveLog],
);
const handleRequestData = (newOffset: number): void => {
@@ -131,7 +132,7 @@ function LogsPanelComponent({
return (
<>
<div className="logs-table" data-log-detail-ignore="true">
<div className="logs-table">
<div className="resize-table">
<OverlayScrollbar>
<ResizeTable
@@ -165,19 +166,15 @@ function LogsPanelComponent({
</div>
)}
</div>
{selectedTab && activeLog && (
<LogDetail
selectedTab={selectedTab}
log={activeLog}
onClose={handleCloseLogDetail}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
isListViewPanel
listViewPanelSelectedFields={widget?.selectedLogFields}
logs={logs}
onNavigateLog={handleSetActiveLog}
/>
)}
<LogDetail
selectedTab={VIEW_TYPES.OVERVIEW}
log={activeLog}
onClose={onClearActiveLog}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
isListViewPanel
listViewPanelSelectedFields={widget?.selectedLogFields}
/>
</>
);
}

View File

@@ -1,59 +0,0 @@
import { useCallback, useState } from 'react';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import type { UseActiveLog } from 'hooks/logs/types';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { ILog } from 'types/api/logs/log';
type SelectedTab = typeof VIEW_TYPES[keyof typeof VIEW_TYPES] | undefined;
type UseLogDetailHandlersParams = {
defaultTab?: SelectedTab;
};
type UseLogDetailHandlersResult = {
activeLog: UseActiveLog['activeLog'];
onAddToQuery: UseActiveLog['onAddToQuery'];
selectedTab: SelectedTab;
handleSetActiveLog: (log: ILog, selectedTab?: SelectedTab) => void;
handleCloseLogDetail: () => void;
};
function useLogDetailHandlers({
defaultTab = VIEW_TYPES.OVERVIEW,
}: UseLogDetailHandlersParams = {}): UseLogDetailHandlersResult {
const {
activeLog,
onSetActiveLog,
onClearActiveLog,
onAddToQuery,
} = useActiveLog();
const [selectedTab, setSelectedTab] = useState<SelectedTab>(defaultTab);
const handleSetActiveLog = useCallback(
(log: ILog, nextTab: SelectedTab = defaultTab): void => {
if (activeLog?.id === log.id) {
onClearActiveLog();
setSelectedTab(undefined);
return;
}
onSetActiveLog(log);
setSelectedTab(nextTab ?? defaultTab);
},
[activeLog?.id, defaultTab, onClearActiveLog, onSetActiveLog],
);
const handleCloseLogDetail = useCallback((): void => {
onClearActiveLog();
setSelectedTab(undefined);
}, [onClearActiveLog]);
return {
activeLog,
onAddToQuery,
selectedTab,
handleSetActiveLog,
handleCloseLogDetail,
};
}
export default useLogDetailHandlers;

View File

@@ -1,28 +0,0 @@
import { useCallback } from 'react';
import type { VirtuosoHandle } from 'react-virtuoso';
type UseScrollToLogParams = {
logs: Array<{ id: string }>;
virtuosoRef: React.RefObject<VirtuosoHandle | null>;
};
function useScrollToLog({
logs,
virtuosoRef,
}: UseScrollToLogParams): (logId: string) => void {
return useCallback(
(logId: string): void => {
const logIndex = logs.findIndex(({ id }) => id === logId);
if (logIndex !== -1 && virtuosoRef.current) {
virtuosoRef.current.scrollToIndex({
index: logIndex,
align: 'center',
behavior: 'smooth',
});
}
},
[logs, virtuosoRef],
);
}
export default useScrollToLog;

View File

@@ -1,444 +0,0 @@
import { ReactElement } from 'react';
import { renderHook, waitFor } from '@testing-library/react';
import { ENVIRONMENT } from 'constants/env';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { AllTheProviders } from 'tests/test-utils';
import { useAuthZ } from './useAuthZ';
import { BrandedPermission, buildPermission } from './utils';
const BASE_URL = ENVIRONMENT.baseURL || '';
const wrapper = ({ children }: { children: ReactElement }): ReactElement => (
<AllTheProviders>{children}</AllTheProviders>
);
describe('useAuthZ', () => {
it('should fetch and return permissions successfully', async () => {
const permission1 = buildPermission('read', 'dashboard:*');
const permission2 = buildPermission('update', 'dashboard:123');
const mockApiResponse = {
[permission1]: true,
[permission2]: false,
};
const expectedResponse = {
[permission1]: {
isGranted: true,
},
[permission2]: {
isGranted: false,
},
};
server.use(
rest.post(`${BASE_URL}/api/v1/permissions/check`, (_req, res, ctx) => {
return res(ctx.status(200), ctx.json(mockApiResponse));
}),
);
const { result } = renderHook(() => useAuthZ([permission1, permission2]), {
wrapper,
});
expect(result.current.isLoading).toBe(true);
expect(result.current.permissions).toBeNull();
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
expect(result.current.error).toBeNull();
expect(result.current.permissions).toEqual(expectedResponse);
});
it('should handle API errors', async () => {
const permission = buildPermission('read', 'dashboard:*');
server.use(
rest.post(`${BASE_URL}/api/v1/permissions/check`, (_req, res, ctx) => {
return res(ctx.status(500), ctx.json({ error: 'Internal Server Error' }));
}),
);
const { result } = renderHook(() => useAuthZ([permission]), {
wrapper,
});
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
expect(result.current.error).not.toBeNull();
expect(result.current.permissions).toBeNull();
});
it('should refetch when permissions array changes', async () => {
const permission1 = buildPermission('read', 'dashboard:*');
const permission2 = buildPermission('update', 'dashboard:123');
const permission3 = buildPermission('delete', 'dashboard:456');
let requestCount = 0;
server.use(
rest.post(`${BASE_URL}/api/v1/permissions/check`, (req, res, ctx) => {
requestCount++;
const body = req.body as {
permissions: Array<{ relation: string; object: string }>;
};
if (body.permissions.length === 1) {
return res(
ctx.status(200),
ctx.json({
[permission1]: true,
}),
);
}
return res(
ctx.status(200),
ctx.json({
[permission1]: true,
[permission2]: false,
[permission3]: true,
}),
);
}),
);
const { result, rerender } = renderHook<
ReturnType<typeof useAuthZ>,
BrandedPermission[]
>((permissions) => useAuthZ(permissions), {
wrapper,
initialProps: [permission1],
});
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
expect(requestCount).toBe(1);
expect(result.current.permissions).toEqual({
[permission1]: {
isGranted: true,
},
});
rerender([permission1, permission2, permission3]);
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
expect(requestCount).toBe(2);
expect(result.current.permissions).toEqual({
[permission1]: {
isGranted: true,
},
[permission2]: {
isGranted: false,
},
[permission3]: {
isGranted: true,
},
});
});
it('should not refetch when permissions array order changes but content is the same', async () => {
const permission1 = buildPermission('read', 'dashboard:*');
const permission2 = buildPermission('update', 'dashboard:123');
let requestCount = 0;
server.use(
rest.post(`${BASE_URL}/api/v1/permissions/check`, (_req, res, ctx) => {
requestCount++;
return res(
ctx.status(200),
ctx.json({
[permission1]: true,
[permission2]: false,
}),
);
}),
);
const { result, rerender } = renderHook<
ReturnType<typeof useAuthZ>,
BrandedPermission[]
>((permissions) => useAuthZ(permissions), {
wrapper,
initialProps: [permission1, permission2],
});
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
expect(requestCount).toBe(1);
rerender([permission2, permission1]);
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
expect(requestCount).toBe(1);
});
it('should handle empty permissions array', async () => {
server.use(
rest.post(`${BASE_URL}/api/v1/permissions/check`, (_req, res, ctx) => {
return res(ctx.status(200), ctx.json({}));
}),
);
const { result } = renderHook(() => useAuthZ([]), {
wrapper,
});
expect(result.current.isLoading).toBe(false);
expect(result.current.error).toBeNull();
expect(result.current.permissions).toEqual({});
});
it('should send correct payload format to API', async () => {
const permission1 = buildPermission('read', 'dashboard:*');
const permission2 = buildPermission('update', 'dashboard:123');
let receivedPayload: any = null;
server.use(
rest.post(`${BASE_URL}/api/v1/permissions/check`, async (req, res, ctx) => {
receivedPayload = await req.json();
return res(
ctx.status(200),
ctx.json({
[permission1]: true,
[permission2]: false,
}),
);
}),
);
const { result } = renderHook(() => useAuthZ([permission1, permission2]), {
wrapper,
});
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
expect(receivedPayload).toEqual({
permissions: [
{ relation: 'read', object: 'dashboard:*' },
{ relation: 'update', object: 'dashboard:123' },
],
});
});
it('should batch multiple hooks into single flight request', async () => {
const permission1 = buildPermission('read', 'dashboard:*');
const permission2 = buildPermission('update', 'dashboard:123');
const permission3 = buildPermission('delete', 'dashboard:456');
const permission4 = buildPermission('create', 'dashboards');
let requestCount = 0;
const receivedPayloads: any[] = [];
server.use(
rest.post(`${BASE_URL}/api/v1/permissions/check`, async (req, res, ctx) => {
requestCount++;
const payload = await req.json();
receivedPayloads.push(payload);
const allPermissions = [permission1, permission2, permission3, permission4];
const response: Record<string, boolean> = {};
payload.permissions.forEach((p: { relation: string; object: string }) => {
const perm = `${p.relation}/${p.object}` as BrandedPermission;
if (allPermissions.includes(perm)) {
response[perm] = perm === permission1 || perm === permission3;
}
});
return res(ctx.status(200), ctx.json(response));
}),
);
const { result: result1 } = renderHook(() => useAuthZ([permission1]), {
wrapper,
});
const { result: result2 } = renderHook(() => useAuthZ([permission2]), {
wrapper,
});
const { result: result3 } = renderHook(() => useAuthZ([permission3]), {
wrapper,
});
await waitFor(
() => {
expect(result1.current.isLoading).toBe(false);
expect(result2.current.isLoading).toBe(false);
expect(result3.current.isLoading).toBe(false);
},
{ timeout: 200 },
);
expect(requestCount).toBe(1);
expect(receivedPayloads).toHaveLength(1);
expect(receivedPayloads[0].permissions).toHaveLength(3);
expect(receivedPayloads[0].permissions).toEqual(
expect.arrayContaining([
{ relation: 'read', object: 'dashboard:*' },
{ relation: 'update', object: 'dashboard:123' },
{ relation: 'delete', object: 'dashboard:456' },
]),
);
expect(result1.current.permissions).toEqual({
[permission1]: { isGranted: true },
});
expect(result2.current.permissions).toEqual({
[permission2]: { isGranted: false },
});
expect(result3.current.permissions).toEqual({
[permission3]: { isGranted: true },
});
});
it('should create separate batches for calls after single flight window', async () => {
const permission1 = buildPermission('read', 'dashboard:*');
const permission2 = buildPermission('update', 'dashboard:123');
const permission3 = buildPermission('delete', 'dashboard:456');
let requestCount = 0;
const receivedPayloads: any[] = [];
server.use(
rest.post(`${BASE_URL}/api/v1/permissions/check`, async (req, res, ctx) => {
requestCount++;
const payload = await req.json();
receivedPayloads.push(payload);
const response: Record<string, boolean> = {};
payload.permissions.forEach((p: { relation: string; object: string }) => {
const perm = `${p.relation}/${p.object}` as BrandedPermission;
response[perm] = perm === permission1 || perm === permission3;
});
return res(ctx.status(200), ctx.json(response));
}),
);
const { result: result1 } = renderHook(() => useAuthZ([permission1]), {
wrapper,
});
await waitFor(
() => {
expect(result1.current.isLoading).toBe(false);
},
{ timeout: 200 },
);
expect(requestCount).toBe(1);
expect(receivedPayloads[0].permissions).toHaveLength(1);
await new Promise((resolve) => setTimeout(resolve, 100));
const { result: result2 } = renderHook(() => useAuthZ([permission2]), {
wrapper,
});
const { result: result3 } = renderHook(() => useAuthZ([permission3]), {
wrapper,
});
await waitFor(
() => {
expect(result2.current.isLoading).toBe(false);
expect(result3.current.isLoading).toBe(false);
},
{ timeout: 200 },
);
expect(requestCount).toBe(2);
expect(receivedPayloads).toHaveLength(2);
expect(receivedPayloads[1].permissions).toHaveLength(2);
expect(receivedPayloads[1].permissions).toEqual(
expect.arrayContaining([
{ relation: 'update', object: 'dashboard:123' },
{ relation: 'delete', object: 'dashboard:456' },
]),
);
});
it('should not leak state between separate batches', async () => {
const permission1 = buildPermission('read', 'dashboard:*');
const permission2 = buildPermission('update', 'dashboard:123');
const permission3 = buildPermission('delete', 'dashboard:456');
let requestCount = 0;
server.use(
rest.post(`${BASE_URL}/api/v1/permissions/check`, async (req, res, ctx) => {
requestCount++;
const payload = await req.json();
const response: Record<string, boolean> = {};
payload.permissions.forEach((p: { relation: string; object: string }) => {
const perm = `${p.relation}/${p.object}` as BrandedPermission;
response[perm] = perm === permission1 || perm === permission3;
});
return res(ctx.status(200), ctx.json(response));
}),
);
const { result: result1 } = renderHook(() => useAuthZ([permission1]), {
wrapper,
});
await waitFor(
() => {
expect(result1.current.isLoading).toBe(false);
},
{ timeout: 200 },
);
expect(requestCount).toBe(1);
expect(result1.current.permissions).toEqual({
[permission1]: { isGranted: true },
});
await new Promise((resolve) => setTimeout(resolve, 100));
const { result: result2 } = renderHook(() => useAuthZ([permission2]), {
wrapper,
});
await waitFor(
() => {
expect(result2.current.isLoading).toBe(false);
},
{ timeout: 200 },
);
expect(requestCount).toBe(2);
expect(result1.current.permissions).toEqual({
[permission1]: { isGranted: true },
});
expect(result2.current.permissions).toEqual({
[permission2]: { isGranted: false },
});
expect(result1.current.permissions).not.toHaveProperty(permission2);
expect(result2.current.permissions).not.toHaveProperty(permission1);
});
});

View File

@@ -1,136 +0,0 @@
import { useMemo } from 'react';
import { useQueries } from 'react-query';
import api from 'api';
import { BrandedPermission } from './utils';
export type UseAuthZPermissionResult = {
isGranted: boolean;
};
export type AuthZCheckResponse = Record<
BrandedPermission,
UseAuthZPermissionResult
>;
export type UseAuthZResult = {
isLoading: boolean;
error: Error | null;
permissions: AuthZCheckResponse | null;
};
let ctx: Promise<AuthZCheckResponse> | null;
let pendingPermissions: BrandedPermission[] = [];
const SINGLE_FLIGHT_WAIT_TIME_MS = 50;
const AUTHZ_CACHE_TIME = 20_000;
function dispatchPermission(
permission: BrandedPermission,
): Promise<AuthZCheckResponse> {
pendingPermissions.push(permission);
if (!ctx) {
let resolve: (v: AuthZCheckResponse) => void, reject: (reason?: any) => void;
ctx = new Promise<AuthZCheckResponse>((r, re) => {
resolve = r;
reject = re;
});
setTimeout(() => {
const copiedPermissions = pendingPermissions.slice();
pendingPermissions = [];
ctx = null;
fetchManyPermissions(copiedPermissions).then(resolve).catch(reject);
}, SINGLE_FLIGHT_WAIT_TIME_MS);
}
return ctx;
}
async function fetchManyPermissions(
permissions: BrandedPermission[],
): Promise<AuthZCheckResponse> {
const permissionsPayload = permissions.map((permission) => {
const [relation, object] = permission.split('/');
return {
relation,
object,
};
});
const permissionsCheckResponse = await api
.post<Record<string, boolean>>('/permissions/check', {
permissions: permissionsPayload,
})
.then((response) => response.data);
return Object.keys(permissionsCheckResponse).reduce<AuthZCheckResponse>(
(acc, permission): AuthZCheckResponse => {
acc[permission as BrandedPermission] = {
isGranted: !!permissionsCheckResponse[permission],
};
return acc;
},
{} as AuthZCheckResponse,
);
}
export function useAuthZ(permissions: BrandedPermission[]): UseAuthZResult {
const queryResults = useQueries(
permissions.map((permission) => {
return {
queryKey: ['authz', permission],
cacheTime: AUTHZ_CACHE_TIME,
refetchOnMount: false,
refetchIntervalInBackground: false,
refetchOnWindowFocus: false,
refetchOnReconnect: true,
queryFn: async (): Promise<AuthZCheckResponse> => {
const response = await dispatchPermission(permission);
return {
[permission]: {
isGranted: response[permission].isGranted,
},
};
},
};
}),
);
const isLoading = useMemo(() => queryResults.some((q) => q.isLoading), [
queryResults,
]);
const error = useMemo(
() =>
!isLoading
? (queryResults.find((q) => !!q.error)?.error as Error) || null
: null,
[isLoading, queryResults],
);
const data = useMemo(() => {
if (isLoading || error) {
return null;
}
return queryResults.reduce((acc, q) => {
if (!q.data) {
return acc;
}
for (const [key, value] of Object.entries(q.data)) {
acc[key as BrandedPermission] = value;
}
return acc;
}, {} as AuthZCheckResponse);
}, [isLoading, error, queryResults]);
return {
isLoading,
error,
permissions: data ?? null,
};
}

View File

@@ -1,32 +0,0 @@
export type AuthZRelation =
| 'list'
| 'create'
| 'read'
| 'update'
| 'delete'
| 'assignee';
export type AuthZResource = 'dashboard';
export type AuthZObject<R extends AuthZRelation> = R extends 'list' | 'create'
? `${AuthZResource}s`
: `${AuthZResource}:${string}`;
export type AuthZPermissionCheck<R extends AuthZRelation> = {
object: AuthZObject<R>;
relation: R;
};
export type BrandedPermission = string & { __brandedPermission: true };
export function buildPermission<R extends AuthZRelation>(
relation: R,
object: AuthZObject<R>,
): BrandedPermission {
return `${relation}/${object}` as BrandedPermission;
}
export function buildObjectString(
resource: AuthZResource,
objectId: string,
): `${AuthZResource}:${string}` {
return `${resource}:${objectId}`;
}

View File

@@ -7,16 +7,11 @@ import { merge } from 'lodash-es';
import noop from 'lodash-es/noop';
import uPlot, { Cursor, Hooks, Options } from 'uplot';
import {
DEFAULT_CURSOR_CONFIG,
DEFAULT_HOVER_PROXIMITY_VALUE,
DEFAULT_PLOT_CONFIG,
STEP_INTERVAL_MULTIPLIER,
} from '../constants';
import { calculateWidthBasedOnStepInterval } from '../utils';
import {
ConfigBuilder,
ConfigBuilderProps,
DEFAULT_CURSOR_CONFIG,
DEFAULT_PLOT_CONFIG,
LegendItem,
SelectionPreferencesSource,
} from './types';
@@ -50,8 +45,6 @@ export class UPlotConfigBuilder extends ConfigBuilder<
private axes: Record<string, UPlotAxisBuilder> = {};
private stepInterval: number | undefined;
readonly scales: UPlotScaleBuilder[] = [];
private bands: uPlot.Band[] = [];
@@ -86,7 +79,6 @@ export class UPlotConfigBuilder extends ConfigBuilder<
tzDate,
selectionPreferencesSource,
shouldSaveSelectionPreference,
stepInterval,
} = args ?? {};
if (widgetId) {
this.widgetId = widgetId;
@@ -104,10 +96,6 @@ export class UPlotConfigBuilder extends ConfigBuilder<
this.shouldSaveSelectionPreference = shouldSaveSelectionPreference;
}
if (stepInterval) {
this.stepInterval = stepInterval;
}
this.onDragSelect = noop;
if (onDragSelect) {
this.onDragSelect = onDragSelect;
@@ -384,31 +372,6 @@ export class UPlotConfigBuilder extends ConfigBuilder<
return this.widgetId;
}
/**
* Get cursor configuration
*/
getCursorConfig(): Cursor {
if (this.stepInterval) {
const cursorConfig = {
...DEFAULT_CURSOR_CONFIG,
hover: {
...DEFAULT_CURSOR_CONFIG.hover,
prox: this.stepInterval
? (uPlotInstance: uPlot): number => {
const width = calculateWidthBasedOnStepInterval({
uPlotInstance,
stepInterval: this.stepInterval ?? 0,
});
return width * STEP_INTERVAL_MULTIPLIER;
}
: DEFAULT_HOVER_PROXIMITY_VALUE,
},
};
return merge({}, DEFAULT_CURSOR_CONFIG, cursorConfig, this.cursor);
}
return merge({}, DEFAULT_CURSOR_CONFIG, this.cursor);
}
/**
* Build the final uPlot.Options configuration
*/
@@ -450,7 +413,7 @@ export class UPlotConfigBuilder extends ConfigBuilder<
config.hooks = this.hooks;
config.select = this.select;
config.cursor = this.getCursorConfig();
config.cursor = merge({}, DEFAULT_CURSOR_CONFIG, this.cursor);
config.tzDate = this.tzDate;
config.plugins = this.plugins.length > 0 ? this.plugins : undefined;
config.bands = this.bands.length > 0 ? this.bands : undefined;

View File

@@ -1,7 +1,6 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { themeColors } from 'constants/theme';
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
import { calculateWidthBasedOnStepInterval } from 'lib/uPlotV2/utils';
import uPlot, { Series } from 'uplot';
import {
@@ -292,16 +291,21 @@ function getBarPathBuilder({
idx1: number,
): Series.Paths | null => {
let effectiveBarMaxWidth = barMaxWidth;
const widthBasedOnStepInterval = calculateWidthBasedOnStepInterval({
uPlotInstance: self,
stepInterval,
});
if (widthBasedOnStepInterval > 0) {
effectiveBarMaxWidth = Math.min(
effectiveBarMaxWidth,
widthBasedOnStepInterval,
);
const xScale = self.scales.x as uPlot.Scale | undefined;
if (xScale && typeof xScale.min === 'number') {
const start = xScale.min as number;
const end = start + stepInterval;
const startPx = self.valToPos(start, 'x');
const endPx = self.valToPos(end, 'x');
const intervalPx = Math.abs(endPx - startPx);
if (intervalPx > 0) {
effectiveBarMaxWidth =
typeof barMaxWidth === 'number'
? Math.min(barMaxWidth, intervalPx)
: intervalPx;
}
}
const barsCfgKey = `bars|${barAlignment}|${barWidthFactor}|${effectiveBarMaxWidth}`;

View File

@@ -1,10 +1,6 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import uPlot from 'uplot';
import {
DEFAULT_HOVER_PROXIMITY_VALUE,
STEP_INTERVAL_MULTIPLIER,
} from '../../constants';
import type { SeriesProps } from '../types';
import { DrawStyle, SelectionPreferencesSource } from '../types';
import { UPlotConfigBuilder } from '../UPlotConfigBuilder';
@@ -17,14 +13,6 @@ jest.mock(
}),
);
jest.mock('lib/uPlotV2/utils', () => ({
calculateWidthBasedOnStepInterval: jest.fn(),
}));
const calculateWidthBasedOnStepIntervalMock = jest.requireMock(
'lib/uPlotV2/utils',
).calculateWidthBasedOnStepInterval as jest.Mock;
const getStoredSeriesVisibilityMock = jest.requireMock(
'container/DashboardContainer/visualization/panels/utils/legendVisibilityUtils',
) as {
@@ -396,52 +384,6 @@ describe('UPlotConfigBuilder', () => {
expect(config.cursor?.points).toBeDefined();
});
describe('getCursorConfig', () => {
it('returns default cursor merged with custom cursor when no stepInterval', () => {
const builder = new UPlotConfigBuilder();
builder.setCursor({
drag: { setScale: false },
});
const cursorConfig = builder.getCursorConfig();
expect(cursorConfig.drag?.setScale).toBe(false);
expect(cursorConfig.hover?.prox).toBe(DEFAULT_HOVER_PROXIMITY_VALUE);
expect(cursorConfig.points).toBeDefined();
});
it('returns hover prox as DEFAULT_HOVER_PROXIMITY_VALUE when stepInterval is not set', () => {
const builder = new UPlotConfigBuilder();
const cursorConfig = builder.getCursorConfig();
expect(cursorConfig.hover?.prox).toBe(DEFAULT_HOVER_PROXIMITY_VALUE);
});
it('returns hover prox as function when stepInterval is set, computing width * multiplier', () => {
const stepInterval = 60;
const mockWidth = 100;
calculateWidthBasedOnStepIntervalMock.mockReturnValue(mockWidth);
const builder = new UPlotConfigBuilder({ stepInterval });
const cursorConfig = builder.getCursorConfig();
expect(typeof cursorConfig.hover?.prox).toBe('function');
const uPlotInstance = {} as uPlot;
const proxResult = (cursorConfig.hover!.prox as (u: uPlot) => number)(
uPlotInstance,
);
expect(calculateWidthBasedOnStepIntervalMock).toHaveBeenCalledWith({
uPlotInstance,
stepInterval,
});
expect(proxResult).toBe(mockWidth * STEP_INTERVAL_MULTIPLIER);
});
});
it('adds plugins and includes them in config', () => {
const builder = new UPlotConfigBuilder();
const plugin: uPlot.Plugin = {

View File

@@ -1,6 +1,6 @@
import { PrecisionOption } from 'components/Graph/types';
import { PANEL_TYPES } from 'constants/queryBuilder';
import uPlot, { Series } from 'uplot';
import uPlot, { Cursor, Options, Series } from 'uplot';
import { ThresholdsDrawHookOptions } from '../hooks/types';
@@ -39,7 +39,6 @@ export interface ConfigBuilderProps {
tzDate?: uPlot.LocalDateFromUnix;
selectionPreferencesSource?: SelectionPreferencesSource;
shouldSaveSelectionPreference?: boolean;
stepInterval?: number;
}
/**
@@ -187,3 +186,47 @@ export interface LegendItem {
color: uPlot.Series['stroke'];
show: boolean;
}
export const DEFAULT_PLOT_CONFIG: Partial<Options> = {
focus: {
alpha: 0.3,
},
cursor: {
focus: {
prox: 30,
},
},
legend: {
show: false,
},
padding: [16, 16, 8, 8],
series: [],
hooks: {},
};
const POINTS_FILL_COLOR = '#FFFFFF';
export const DEFAULT_CURSOR_CONFIG: Cursor = {
drag: { setScale: true },
points: {
one: true,
size: (u, seriesIdx) => (u.series[seriesIdx]?.points?.size ?? 0) * 3,
width: (_u, _seriesIdx, size) => size / 4,
stroke: (u, seriesIdx): string => {
const points = u.series[seriesIdx]?.points;
const strokeFn =
typeof points?.stroke === 'function' ? points.stroke : undefined;
const strokeValue =
strokeFn !== undefined
? strokeFn(u, seriesIdx)
: typeof points?.stroke === 'string'
? points.stroke
: '';
return `${strokeValue}90`;
},
fill: (): string => POINTS_FILL_COLOR,
},
focus: {
prox: 30,
},
};

View File

@@ -1,48 +0,0 @@
import { Cursor, Options } from 'uplot';
const POINTS_FILL_COLOR = '#FFFFFF';
export const DEFAULT_HOVER_PROXIMITY_VALUE = 30; // only snap if within 30px horizontally
export const DEFAULT_FOCUS_PROXIMITY_VALUE = 1e6;
export const STEP_INTERVAL_MULTIPLIER = 3; // multiply the width computed by STEP_INTERVAL_MULTIPLIER to get the hover prox value
export const DEFAULT_PLOT_CONFIG: Partial<Options> = {
focus: {
alpha: 0.3,
},
legend: {
show: false,
},
padding: [16, 16, 8, 8],
series: [],
hooks: {},
};
export const DEFAULT_CURSOR_CONFIG: Cursor = {
drag: { setScale: true },
points: {
one: true,
size: (u, seriesIdx) => (u.series[seriesIdx]?.points?.size ?? 0) * 3,
width: (_u, _seriesIdx, size) => size / 4,
stroke: (u, seriesIdx): string => {
const points = u.series[seriesIdx]?.points;
const strokeFn =
typeof points?.stroke === 'function' ? points.stroke : undefined;
const strokeValue =
strokeFn !== undefined
? strokeFn(u, seriesIdx)
: typeof points?.stroke === 'string'
? points.stroke
: '';
return `${strokeValue}90`;
},
fill: (): string => POINTS_FILL_COLOR,
},
focus: {
prox: DEFAULT_FOCUS_PROXIMITY_VALUE,
},
hover: {
prox: DEFAULT_HOVER_PROXIMITY_VALUE,
bias: 0,
},
};

View File

@@ -87,7 +87,7 @@ export function shouldShowTooltipForSync(
export function shouldShowTooltipForInteraction(
controller: TooltipControllerState,
): boolean {
return controller.focusedSeriesIndex != null;
return controller.focusedSeriesIndex != null || controller.isAnySeriesActive;
}
export function updateHoverState(

View File

@@ -1,70 +0,0 @@
import uPlot from 'uplot';
import { calculateWidthBasedOnStepInterval } from '../index';
describe('calculateWidthBasedOnStepInterval', () => {
it('returns pixel width between start and start+stepInterval when xScale exists with numeric min', () => {
const valToPos = jest
.fn()
.mockReturnValueOnce(100) // startPx for start
.mockReturnValueOnce(250); // endPx for start + stepInterval
const uPlotInstance = ({
scales: { x: { min: 1000 } },
valToPos,
} as unknown) as uPlot;
const result = calculateWidthBasedOnStepInterval({
uPlotInstance,
stepInterval: 60,
});
expect(valToPos).toHaveBeenCalledWith(1000, 'x');
expect(valToPos).toHaveBeenCalledWith(1060, 'x');
expect(result).toBe(150); // Math.abs(250 - 100)
});
it('returns absolute pixel width when endPx is less than startPx', () => {
const valToPos = jest.fn().mockReturnValueOnce(250).mockReturnValueOnce(100);
const uPlotInstance = ({
scales: { x: { min: 0 } },
valToPos,
} as unknown) as uPlot;
const result = calculateWidthBasedOnStepInterval({
uPlotInstance,
stepInterval: 60,
});
expect(result).toBe(150); // Math.abs(100 - 250)
});
it('returns 0 when xScale is undefined', () => {
const uPlotInstance = ({
scales: { x: undefined },
valToPos: jest.fn(),
} as unknown) as uPlot;
const result = calculateWidthBasedOnStepInterval({
uPlotInstance,
stepInterval: 60,
});
expect(result).toBe(0);
});
it('returns 0 when xScale.min is not a number', () => {
const uPlotInstance = ({
scales: { x: { min: undefined } },
valToPos: jest.fn(),
} as unknown) as uPlot;
const result = calculateWidthBasedOnStepInterval({
uPlotInstance,
stepInterval: 60,
});
expect(result).toBe(0);
});
});

View File

@@ -1,17 +0,0 @@
export function calculateWidthBasedOnStepInterval({
uPlotInstance,
stepInterval,
}: {
uPlotInstance: uPlot;
stepInterval: number;
}): number {
const xScale = uPlotInstance.scales.x;
if (xScale && typeof xScale.min === 'number') {
const start = xScale.min as number;
const end = start + stepInterval;
const startPx = uPlotInstance.valToPos(start, 'x');
const endPx = uPlotInstance.valToPos(end, 'x');
return Math.abs(endPx - startPx);
}
return 0;
}

View File

@@ -567,15 +567,6 @@ body {
border: 1px solid var(--bg-vanilla-300);
}
.ant-tooltip {
--antd-arrow-background-color: var(--bg-vanilla-100);
.ant-tooltip-inner {
background-color: var(--bg-vanilla-100);
color: var(---bg-ink-500);
}
}
.ant-dropdown-menu {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);

View File

@@ -9,9 +9,10 @@ export const getDefaultLogBackground = (
if (isReadOnly) {
return '';
}
// TODO handle the light mode here
return `&:hover {
background-color: ${
isDarkMode ? 'rgba(171, 189, 255, 0.04)' : 'rgba(0, 0, 0, 0.04)'
isDarkMode ? 'rgba(171, 189, 255, 0.04)' : 'var(--bg-vanilla-200)'
};
}`;
};
@@ -27,38 +28,22 @@ export const getActiveLogBackground = (
if (isDarkMode) {
switch (logType) {
case LogType.INFO:
return `background-color: ${Color.BG_ROBIN_500}40 !important;`;
return `background-color: ${Color.BG_ROBIN_500}10 !important;`;
case LogType.WARN:
return `background-color: ${Color.BG_AMBER_500}40 !important;`;
return `background-color: ${Color.BG_AMBER_500}10 !important;`;
case LogType.ERROR:
return `background-color: ${Color.BG_CHERRY_500}40 !important;`;
return `background-color: ${Color.BG_CHERRY_500}10 !important;`;
case LogType.TRACE:
return `background-color: ${Color.BG_FOREST_400}40 !important;`;
return `background-color: ${Color.BG_FOREST_400}10 !important;`;
case LogType.DEBUG:
return `background-color: ${Color.BG_AQUA_500}40 !important;`;
return `background-color: ${Color.BG_AQUA_500}10 !important;`;
case LogType.FATAL:
return `background-color: ${Color.BG_SAKURA_500}40 !important;`;
return `background-color: ${Color.BG_SAKURA_500}10 !important;`;
default:
return `background-color: ${Color.BG_ROBIN_500}40 !important;`;
return `background-color: ${Color.BG_SLATE_200} !important;`;
}
}
// Light mode - use lighter background colors
switch (logType) {
case LogType.INFO:
return `background-color: ${Color.BG_ROBIN_100} !important;`;
case LogType.WARN:
return `background-color: ${Color.BG_AMBER_100} !important;`;
case LogType.ERROR:
return `background-color: ${Color.BG_CHERRY_100} !important;`;
case LogType.TRACE:
return `background-color: ${Color.BG_FOREST_200} !important;`;
case LogType.DEBUG:
return `background-color: ${Color.BG_AQUA_100} !important;`;
case LogType.FATAL:
return `background-color: ${Color.BG_SAKURA_100} !important;`;
default:
return `background-color: ${Color.BG_VANILLA_300} !important;`;
}
return `background-color: ${Color.BG_VANILLA_400}!important; color: ${Color.TEXT_SLATE_400} !important;`;
};
export const getHightLightedLogBackground = (

View File

@@ -10,26 +10,6 @@ import (
)
func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
if err := router.Handle("/api/v2/metrics", handler.New(
provider.authZ.ViewAccess(provider.metricsExplorerHandler.ListMetrics),
handler.OpenAPIDef{
ID: "ListMetrics",
Tags: []string{"metrics"},
Summary: "List metric names",
Description: "This endpoint returns a list of distinct metric names within the specified time range",
Request: nil,
RequestQuery: new(metricsexplorertypes.ListMetricsParams),
RequestContentType: "",
Response: new(metricsexplorertypes.ListMetricsResponse),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusInternalServerError},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/metrics/stats", handler.New(
provider.authZ.ViewAccess(provider.metricsExplorerHandler.GetStats),
handler.OpenAPIDef{
@@ -68,27 +48,26 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/metrics/{metric_name}/attributes", handler.New(
if err := router.Handle("/api/v2/metrics/attributes", handler.New(
provider.authZ.ViewAccess(provider.metricsExplorerHandler.GetMetricAttributes),
handler.OpenAPIDef{
ID: "GetMetricAttributes",
Tags: []string{"metrics"},
Summary: "Get metric attributes",
Description: "This endpoint returns attribute keys and their unique values for a specified metric",
Request: nil,
RequestQuery: new(metricsexplorertypes.MetricAttributesRequest),
RequestContentType: "",
Request: new(metricsexplorertypes.MetricAttributesRequest),
RequestContentType: "application/json",
Response: new(metricsexplorertypes.MetricAttributesResponse),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusInternalServerError},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/metrics/{metric_name}/metadata", handler.New(
if err := router.Handle("/api/v2/metrics/metadata", handler.New(
provider.authZ.ViewAccess(provider.metricsExplorerHandler.GetMetricMetadata),
handler.OpenAPIDef{
ID: "GetMetricMetadata",
@@ -96,6 +75,7 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
Summary: "Get metric metadata",
Description: "This endpoint returns metadata information like metric description, unit, type, temporality, monotonicity for a specified metric",
Request: nil,
RequestQuery: new(metricsexplorertypes.MetricNameParams),
RequestContentType: "",
Response: new(metricsexplorertypes.MetricMetadata),
ResponseContentType: "application/json",
@@ -126,7 +106,7 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/metrics/{metric_name}/highlights", handler.New(
if err := router.Handle("/api/v2/metric/highlights", handler.New(
provider.authZ.ViewAccess(provider.metricsExplorerHandler.GetMetricHighlights),
handler.OpenAPIDef{
ID: "GetMetricHighlights",
@@ -134,6 +114,7 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
Summary: "Get metric highlights",
Description: "This endpoint returns highlights like number of datapoints, totaltimeseries, active time series, last received time for a specified metric",
Request: nil,
RequestQuery: new(metricsexplorertypes.MetricNameParams),
RequestContentType: "",
Response: new(metricsexplorertypes.MetricHighlightsResponse),
ResponseContentType: "application/json",
@@ -145,7 +126,7 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/metrics/{metric_name}/alerts", handler.New(
if err := router.Handle("/api/v2/metric/alerts", handler.New(
provider.authZ.ViewAccess(provider.metricsExplorerHandler.GetMetricAlerts),
handler.OpenAPIDef{
ID: "GetMetricAlerts",
@@ -153,6 +134,7 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
Summary: "Get metric alerts",
Description: "This endpoint returns associated alerts for a specified metric",
Request: nil,
RequestQuery: new(metricsexplorertypes.MetricNameParams),
RequestContentType: "",
Response: new(metricsexplorertypes.MetricAlertsResponse),
ResponseContentType: "application/json",
@@ -164,7 +146,7 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/metrics/{metric_name}/dashboards", handler.New(
if err := router.Handle("/api/v2/metric/dashboards", handler.New(
provider.authZ.ViewAccess(provider.metricsExplorerHandler.GetMetricDashboards),
handler.OpenAPIDef{
ID: "GetMetricDashboards",
@@ -172,6 +154,7 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
Summary: "Get metric dashboards",
Description: "This endpoint returns associated dashboards for a specified metric",
Request: nil,
RequestQuery: new(metricsexplorertypes.MetricNameParams),
RequestContentType: "",
Response: new(metricsexplorertypes.MetricDashboardsResponse),
ResponseContentType: "application/json",

View File

@@ -7,7 +7,6 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/swaggest/openapi-go"
"github.com/swaggest/openapi-go/openapi3"
)
type ServeOpenAPIFunc func(openapi.OperationContext)
@@ -60,39 +59,7 @@ func (handler *handler) ServeOpenAPI(opCtx openapi.OperationContext) {
}
// Add request structure
reqOpts := []openapi.ContentOption{openapi.WithContentType(handler.openAPIDef.RequestContentType)}
if len(handler.openAPIDef.RequestExamples) > 0 {
reqOpts = append(reqOpts, openapi.WithCustomize(func(cor openapi.ContentOrReference) {
rbOrRef, ok := cor.(*openapi3.RequestBodyOrRef)
if !ok || rbOrRef.RequestBody == nil {
return
}
ct := handler.openAPIDef.RequestContentType
if ct == "" {
ct = "application/json"
}
mt, exists := rbOrRef.RequestBody.Content[ct]
if !exists {
return
}
if mt.Examples == nil {
mt.Examples = make(map[string]openapi3.ExampleOrRef)
}
for _, ex := range handler.openAPIDef.RequestExamples {
val := ex.Value
oaExample := openapi3.Example{Value: &val}
if ex.Summary != "" {
oaExample.WithSummary(ex.Summary)
}
if ex.Description != "" {
oaExample.WithDescription(ex.Description)
}
mt.Examples[ex.Name] = openapi3.ExampleOrRef{Example: &oaExample}
}
rbOrRef.RequestBody.Content[ct] = mt
}))
}
opCtx.AddReqStructure(handler.openAPIDef.Request, reqOpts...)
opCtx.AddReqStructure(handler.openAPIDef.Request, openapi.WithContentType(handler.openAPIDef.RequestContentType))
// Add request query structure
opCtx.AddReqStructure(handler.openAPIDef.RequestQuery)

View File

@@ -9,14 +9,6 @@ import (
"github.com/swaggest/rest/openapi"
)
// OpenAPIExample is a named example for an OpenAPI operation.
type OpenAPIExample struct {
Name string
Summary string
Description string
Value any
}
// Def is the definition of an OpenAPI operation
type OpenAPIDef struct {
ID string
@@ -26,7 +18,6 @@ type OpenAPIDef struct {
Request any
RequestQuery any
RequestContentType string
RequestExamples []OpenAPIExample
Response any
ResponseContentType string
SuccessStatusCode int

View File

@@ -0,0 +1,44 @@
package middleware
import (
"log/slog"
"net/http"
"runtime/debug"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/render"
)
// Recovery is a middleware that recovers from panics, logs the panic,
// and returns a 500 Internal Server Error.
type Recovery struct {
logger *slog.Logger
}
// NewRecovery creates a new Recovery middleware.
func NewRecovery(logger *slog.Logger) Wrapper {
return &Recovery{
logger: logger.With("pkg", "http-middleware-recovery"),
}
}
// Wrap is the middleware handler.
func (m *Recovery) Wrap(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
m.logger.ErrorContext(
r.Context(),
"panic recovered",
"err", err, "stack", string(debug.Stack()),
)
render.Error(w, errors.NewInternalf(
errors.CodeInternal, "internal server error",
))
}
}()
next.ServeHTTP(w, r)
})
}

View File

@@ -13,14 +13,6 @@ import (
"github.com/gorilla/mux"
)
func extractMetricName(req *http.Request) (string, error) {
metricName := mux.Vars(req)["metric_name"]
if metricName == "" {
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "metric_name is required in URL path")
}
return metricName, nil
}
type handler struct {
module metricsexplorer.Module
}
@@ -32,34 +24,6 @@ func NewHandler(m metricsexplorer.Module) metricsexplorer.Handler {
}
}
func (h *handler) ListMetrics(rw http.ResponseWriter, req *http.Request) {
claims, err := authtypes.ClaimsFromContext(req.Context())
if err != nil {
render.Error(rw, err)
return
}
var params metricsexplorertypes.ListMetricsParams
if err := binding.Query.BindQuery(req.URL.Query(), &params); err != nil {
render.Error(rw, err)
return
}
if err := params.Validate(); err != nil {
render.Error(rw, err)
return
}
orgID := valuer.MustNewUUID(claims.OrgID)
out, err := h.module.ListMetrics(req.Context(), orgID, &params)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, out)
}
func (h *handler) GetStats(rw http.ResponseWriter, req *http.Request) {
claims, err := authtypes.ClaimsFromContext(req.Context())
if err != nil {
@@ -150,23 +114,28 @@ func (h *handler) GetMetricMetadata(rw http.ResponseWriter, req *http.Request) {
return
}
metricName, err := extractMetricName(req)
if err != nil {
var params metricsexplorertypes.MetricNameParams
if err := binding.Query.BindQuery(req.URL.Query(), &params); err != nil {
render.Error(rw, err)
return
}
if params.MetricName == "" {
render.Error(rw, errors.NewInvalidInputf(errors.CodeInvalidInput, "metricName query parameter is required"))
return
}
orgID := valuer.MustNewUUID(claims.OrgID)
metadataMap, err := h.module.GetMetricMetadataMulti(req.Context(), orgID, []string{metricName})
metadataMap, err := h.module.GetMetricMetadataMulti(req.Context(), orgID, []string{params.MetricName})
if err != nil {
render.Error(rw, err)
return
}
metadata, ok := metadataMap[metricName]
metadata, ok := metadataMap[params.MetricName]
if !ok || metadata == nil {
render.Error(rw, errors.NewNotFoundf(errors.CodeNotFound, "metadata not found for metric %q", metricName))
render.Error(rw, errors.NewNotFoundf(errors.CodeNotFound, "metadata not found for metric %q", params.MetricName))
return
}
@@ -180,14 +149,19 @@ func (h *handler) GetMetricAlerts(rw http.ResponseWriter, req *http.Request) {
return
}
metricName, err := extractMetricName(req)
if err != nil {
var params metricsexplorertypes.MetricNameParams
if err := binding.Query.BindQuery(req.URL.Query(), &params); err != nil {
render.Error(rw, err)
return
}
if params.MetricName == "" {
render.Error(rw, errors.NewInvalidInputf(errors.CodeInvalidInput, "metricName query parameter is required"))
return
}
orgID := valuer.MustNewUUID(claims.OrgID)
out, err := h.module.GetMetricAlerts(req.Context(), orgID, metricName)
out, err := h.module.GetMetricAlerts(req.Context(), orgID, params.MetricName)
if err != nil {
render.Error(rw, err)
return
@@ -202,14 +176,19 @@ func (h *handler) GetMetricDashboards(rw http.ResponseWriter, req *http.Request)
return
}
metricName, err := extractMetricName(req)
if err != nil {
var params metricsexplorertypes.MetricNameParams
if err := binding.Query.BindQuery(req.URL.Query(), &params); err != nil {
render.Error(rw, err)
return
}
if params.MetricName == "" {
render.Error(rw, errors.NewInvalidInputf(errors.CodeInvalidInput, "metricName query parameter is required"))
return
}
orgID := valuer.MustNewUUID(claims.OrgID)
out, err := h.module.GetMetricDashboards(req.Context(), orgID, metricName)
out, err := h.module.GetMetricDashboards(req.Context(), orgID, params.MetricName)
if err != nil {
render.Error(rw, err)
return
@@ -224,14 +203,19 @@ func (h *handler) GetMetricHighlights(rw http.ResponseWriter, req *http.Request)
return
}
metricName, err := extractMetricName(req)
if err != nil {
var params metricsexplorertypes.MetricNameParams
if err := binding.Query.BindQuery(req.URL.Query(), &params); err != nil {
render.Error(rw, err)
return
}
if params.MetricName == "" {
render.Error(rw, errors.NewInvalidInputf(errors.CodeInvalidInput, "metricName query parameter is required"))
return
}
orgID := valuer.MustNewUUID(claims.OrgID)
highlights, err := h.module.GetMetricHighlights(req.Context(), orgID, metricName)
highlights, err := h.module.GetMetricHighlights(req.Context(), orgID, params.MetricName)
if err != nil {
render.Error(rw, err)
return
@@ -246,21 +230,8 @@ func (h *handler) GetMetricAttributes(rw http.ResponseWriter, req *http.Request)
return
}
metricName, err := extractMetricName(req)
if err != nil {
render.Error(rw, err)
return
}
var in metricsexplorertypes.MetricAttributesRequest
if err := binding.Query.BindQuery(req.URL.Query(), &in); err != nil {
render.Error(rw, err)
return
}
in.MetricName = metricName
if err := in.Validate(); err != nil {
if err := binding.JSON.BindBody(req.Body, &in); err != nil {
render.Error(rw, err)
return
}

View File

@@ -56,88 +56,6 @@ func NewModule(ts telemetrystore.TelemetryStore, telemetryMetadataStore telemetr
}
}
func (m *module) ListMetrics(ctx context.Context, orgID valuer.UUID, params *metricsexplorertypes.ListMetricsParams) (*metricsexplorertypes.ListMetricsResponse, error) {
if err := params.Validate(); err != nil {
return nil, err
}
sb := sqlbuilder.NewSelectBuilder()
sb.Select("DISTINCT metric_name")
if params.Start != nil && params.End != nil {
start, end, distributedTsTable, _ := telemetrymetrics.WhichTSTableToUse(uint64(*params.Start), uint64(*params.End), nil)
sb.From(fmt.Sprintf("%s.%s", telemetrymetrics.DBName, distributedTsTable))
sb.Where(sb.Between("unix_milli", start, end))
} else {
sb.From(fmt.Sprintf("%s.%s", telemetrymetrics.DBName, telemetrymetrics.TimeseriesV41weekTableName))
}
sb.Where(sb.E("__normalized", false))
if params.Search != "" {
searchLower := strings.ToLower(params.Search)
searchLower = strings.ReplaceAll(searchLower, "%", "\\%")
searchLower = strings.ReplaceAll(searchLower, "_", "\\_")
sb.Where(sb.Like("lower(metric_name)", fmt.Sprintf("%%%s%%", searchLower)))
}
sb.OrderBy("metric_name ASC")
sb.Limit(params.Limit)
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
valueCtx := ctxtypes.SetClickhouseMaxThreads(ctx, m.config.TelemetryStore.Threads)
db := m.telemetryStore.ClickhouseDB()
rows, err := db.Query(valueCtx, query, args...)
if err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to list metrics")
}
defer rows.Close()
metricNames := make([]string, 0)
for rows.Next() {
var name string
if err := rows.Scan(&name); err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to scan metric name")
}
metricNames = append(metricNames, name)
}
if err := rows.Err(); err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "error iterating metric names")
}
if len(metricNames) == 0 {
return &metricsexplorertypes.ListMetricsResponse{
Metrics: []metricsexplorertypes.ListMetric{},
}, nil
}
metadata, err := m.GetMetricMetadataMulti(ctx, orgID, metricNames)
if err != nil {
return nil, err
}
metrics := make([]metricsexplorertypes.ListMetric, 0, len(metricNames))
for _, name := range metricNames {
metric := metricsexplorertypes.ListMetric{
MetricName: name,
}
if meta, ok := metadata[name]; ok && meta != nil {
metric.Description = meta.Description
metric.MetricType = meta.MetricType
metric.MetricUnit = meta.MetricUnit
metric.Temporality = meta.Temporality
metric.IsMonotonic = meta.IsMonotonic
}
metrics = append(metrics, metric)
}
return &metricsexplorertypes.ListMetricsResponse{
Metrics: metrics,
}, nil
}
func (m *module) GetStats(ctx context.Context, orgID valuer.UUID, req *metricsexplorertypes.StatsRequest) (*metricsexplorertypes.StatsResponse, error) {
if err := req.Validate(); err != nil {
return nil, err

View File

@@ -10,7 +10,6 @@ import (
// Handler exposes HTTP handlers for the metrics module.
type Handler interface {
ListMetrics(http.ResponseWriter, *http.Request)
GetStats(http.ResponseWriter, *http.Request)
GetTreemap(http.ResponseWriter, *http.Request)
GetMetricMetadata(http.ResponseWriter, *http.Request)
@@ -23,7 +22,6 @@ type Handler interface {
// Module represents the metrics module interface.
type Module interface {
ListMetrics(ctx context.Context, orgID valuer.UUID, params *metricsexplorertypes.ListMetricsParams) (*metricsexplorertypes.ListMetricsResponse, error)
GetStats(ctx context.Context, orgID valuer.UUID, req *metricsexplorertypes.StatsRequest) (*metricsexplorertypes.StatsResponse, error)
GetTreemap(ctx context.Context, orgID valuer.UUID, req *metricsexplorertypes.TreemapRequest) (*metricsexplorertypes.TreemapResponse, error)
GetMetricMetadataMulti(ctx context.Context, orgID valuer.UUID, metricNames []string) (map[string]*metricsexplorertypes.MetricMetadata, error)

View File

@@ -12,30 +12,60 @@ import (
)
const (
derivedKeyHTTPURL = "http_url" // https://signoz.io/docs/traces-management/guides/derived-fields-spans/#http_url
derivedKeyHTTPHost = "http_host"
urlPathKeyLegacy = "http.url"
serverAddressKeyLegacy = "net.peer.name"
urlPathKey = "url.full"
serverAddressKey = "server.address"
)
var defaultStepInterval = 60 * time.Second
var (
groupByKeyHTTPHost = qbtypes.GroupByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: derivedKeyHTTPHost,
FieldDataType: telemetrytypes.FieldDataTypeString,
FieldContext: telemetrytypes.FieldContextSpan,
Signal: telemetrytypes.SignalTraces,
type SemconvFieldMapping struct {
LegacyField string
CurrentField string
FieldType telemetrytypes.FieldDataType
Context telemetrytypes.FieldContext
}
var dualSemconvGroupByKeys = map[string][]qbtypes.GroupByKey{
"server": {
{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: serverAddressKey,
FieldDataType: telemetrytypes.FieldDataTypeString,
FieldContext: telemetrytypes.FieldContextAttribute,
Signal: telemetrytypes.SignalTraces,
},
},
}
groupByKeyHTTPURL = qbtypes.GroupByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: derivedKeyHTTPURL,
FieldDataType: telemetrytypes.FieldDataTypeString,
FieldContext: telemetrytypes.FieldContextSpan,
Signal: telemetrytypes.SignalTraces,
{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: serverAddressKeyLegacy,
FieldDataType: telemetrytypes.FieldDataTypeString,
FieldContext: telemetrytypes.FieldContextAttribute,
Signal: telemetrytypes.SignalTraces,
},
},
}
)
},
"url": {
{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: urlPathKey,
FieldDataType: telemetrytypes.FieldDataTypeString,
FieldContext: telemetrytypes.FieldContextAttribute,
Signal: telemetrytypes.SignalTraces,
},
},
{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: urlPathKeyLegacy,
FieldDataType: telemetrytypes.FieldDataTypeString,
FieldContext: telemetrytypes.FieldContextAttribute,
Signal: telemetrytypes.SignalTraces,
},
},
},
}
func FilterIntermediateColumns(result *qbtypes.QueryRangeResponse) *qbtypes.QueryRangeResponse {
if result == nil || result.Data.Results == nil {
@@ -84,6 +114,103 @@ func FilterIntermediateColumns(result *qbtypes.QueryRangeResponse) *qbtypes.Quer
return result
}
func MergeSemconvColumns(result *qbtypes.QueryRangeResponse) *qbtypes.QueryRangeResponse {
if result == nil || result.Data.Results == nil {
return result
}
for _, res := range result.Data.Results {
scalarData, ok := res.(*qbtypes.ScalarData)
if !ok {
continue
}
serverAddressKeyIdx := -1
serverAddressKeyLegacyIdx := -1
for i, col := range scalarData.Columns {
if col.Name == serverAddressKey {
serverAddressKeyIdx = i
} else if col.Name == serverAddressKeyLegacy {
serverAddressKeyLegacyIdx = i
}
}
if serverAddressKeyIdx == -1 || serverAddressKeyLegacyIdx == -1 {
continue
}
var newRows [][]any
for _, row := range scalarData.Data {
if len(row) <= serverAddressKeyIdx || len(row) <= serverAddressKeyLegacyIdx {
continue
}
var serverName any
if isValidValue(row[serverAddressKeyIdx]) {
serverName = row[serverAddressKeyIdx]
} else if isValidValue(row[serverAddressKeyLegacyIdx]) {
serverName = row[serverAddressKeyLegacyIdx]
}
if serverName != nil {
newRow := make([]any, len(row)-1)
newRow[0] = serverName
targetIdx := 1
for i, val := range row {
if i != serverAddressKeyLegacyIdx && i != serverAddressKeyIdx {
if targetIdx < len(newRow) {
newRow[targetIdx] = val
targetIdx++
}
}
}
newRows = append(newRows, newRow)
}
}
newColumns := make([]*qbtypes.ColumnDescriptor, len(scalarData.Columns)-1)
targetIdx := 0
for i, col := range scalarData.Columns {
if i == serverAddressKeyIdx {
newCol := &qbtypes.ColumnDescriptor{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: serverAddressKeyLegacy,
FieldDataType: col.FieldDataType,
FieldContext: col.FieldContext,
Signal: col.Signal,
},
QueryName: col.QueryName,
AggregationIndex: col.AggregationIndex,
Meta: col.Meta,
Type: col.Type,
}
newColumns[targetIdx] = newCol
targetIdx++
} else if i != serverAddressKeyLegacyIdx {
newColumns[targetIdx] = col
targetIdx++
}
}
scalarData.Columns = newColumns
scalarData.Data = newRows
}
return result
}
func isValidValue(val any) bool {
if val == nil {
return false
}
if str, ok := val.(string); ok {
return str != "" && str != "n/a"
}
return true
}
func FilterResponse(results []*qbtypes.QueryRangeResponse) []*qbtypes.QueryRangeResponse {
filteredResults := make([]*qbtypes.QueryRangeResponse, 0, len(results))
@@ -134,7 +261,7 @@ func FilterResponse(results []*qbtypes.QueryRangeResponse) []*qbtypes.QueryRange
func shouldIncludeSeries(series *qbtypes.TimeSeries) bool {
for _, label := range series.Labels {
if label.Key.Name == derivedKeyHTTPHost {
if label.Key.Name == serverAddressKeyLegacy || label.Key.Name == serverAddressKey {
if strVal, ok := label.Value.(string); ok {
if net.ParseIP(strVal) != nil {
return false
@@ -147,10 +274,12 @@ func shouldIncludeSeries(series *qbtypes.TimeSeries) bool {
func shouldIncludeRow(row *qbtypes.RawRow) bool {
if row.Data != nil {
if domainVal, ok := row.Data[derivedKeyHTTPHost]; ok {
if domainStr, ok := domainVal.(string); ok {
if net.ParseIP(domainStr) != nil {
return false
for _, key := range []string{serverAddressKeyLegacy, serverAddressKey} {
if domainVal, ok := row.Data[key]; ok {
if domainStr, ok := domainVal.(string); ok {
if net.ParseIP(domainStr) != nil {
return false
}
}
}
}
@@ -158,8 +287,8 @@ func shouldIncludeRow(row *qbtypes.RawRow) bool {
return true
}
func mergeGroupBy(base qbtypes.GroupByKey, additional []qbtypes.GroupByKey) []qbtypes.GroupByKey {
return append([]qbtypes.GroupByKey{base}, additional...)
func mergeGroupBy(base, additional []qbtypes.GroupByKey) []qbtypes.GroupByKey {
return append(base, additional...)
}
func BuildDomainList(req *thirdpartyapitypes.ThirdPartyApiRequest) (*qbtypes.QueryRangeRequest, error) {
@@ -225,10 +354,10 @@ func buildEndpointsQuery(req *thirdpartyapitypes.ThirdPartyApiRequest) qbtypes.Q
Signal: telemetrytypes.SignalTraces,
StepInterval: qbtypes.Step{Duration: defaultStepInterval},
Aggregations: []qbtypes.TraceAggregation{
{Expression: fmt.Sprintf("count_distinct(%s)", derivedKeyHTTPURL)},
{Expression: "count_distinct(http.url)"},
},
Filter: buildBaseFilter(req.Filter),
GroupBy: mergeGroupBy(groupByKeyHTTPHost, req.GroupBy),
GroupBy: mergeGroupBy(dualSemconvGroupByKeys["server"], req.GroupBy),
},
}
}
@@ -244,7 +373,7 @@ func buildLastSeenQuery(req *thirdpartyapitypes.ThirdPartyApiRequest) qbtypes.Qu
{Expression: "max(timestamp)"},
},
Filter: buildBaseFilter(req.Filter),
GroupBy: mergeGroupBy(groupByKeyHTTPHost, req.GroupBy),
GroupBy: mergeGroupBy(dualSemconvGroupByKeys["server"], req.GroupBy),
},
}
}
@@ -260,7 +389,7 @@ func buildRpsQuery(req *thirdpartyapitypes.ThirdPartyApiRequest) qbtypes.QueryEn
{Expression: "rate()"},
},
Filter: buildBaseFilter(req.Filter),
GroupBy: mergeGroupBy(groupByKeyHTTPHost, req.GroupBy),
GroupBy: mergeGroupBy(dualSemconvGroupByKeys["server"], req.GroupBy),
},
}
}
@@ -278,7 +407,7 @@ func buildErrorQuery(req *thirdpartyapitypes.ThirdPartyApiRequest) qbtypes.Query
{Expression: "count()"},
},
Filter: filter,
GroupBy: mergeGroupBy(groupByKeyHTTPHost, req.GroupBy),
GroupBy: mergeGroupBy(dualSemconvGroupByKeys["server"], req.GroupBy),
},
}
}
@@ -294,7 +423,7 @@ func buildTotalSpanQuery(req *thirdpartyapitypes.ThirdPartyApiRequest) qbtypes.Q
{Expression: "count()"},
},
Filter: buildBaseFilter(req.Filter),
GroupBy: mergeGroupBy(groupByKeyHTTPHost, req.GroupBy),
GroupBy: mergeGroupBy(dualSemconvGroupByKeys["server"], req.GroupBy),
},
}
}
@@ -310,7 +439,7 @@ func buildP99Query(req *thirdpartyapitypes.ThirdPartyApiRequest) qbtypes.QueryEn
{Expression: "p99(duration_nano)"},
},
Filter: buildBaseFilter(req.Filter),
GroupBy: mergeGroupBy(groupByKeyHTTPHost, req.GroupBy),
GroupBy: mergeGroupBy(dualSemconvGroupByKeys["server"], req.GroupBy),
},
}
}
@@ -333,10 +462,10 @@ func buildEndpointsInfoQuery(req *thirdpartyapitypes.ThirdPartyApiRequest) qbtyp
Signal: telemetrytypes.SignalTraces,
StepInterval: qbtypes.Step{Duration: defaultStepInterval},
Aggregations: []qbtypes.TraceAggregation{
{Expression: fmt.Sprintf("rate(%s)", derivedKeyHTTPURL)},
{Expression: "rate(http.url)"},
},
Filter: buildBaseFilter(req.Filter),
GroupBy: mergeGroupBy(groupByKeyHTTPURL, req.GroupBy),
GroupBy: mergeGroupBy(dualSemconvGroupByKeys["url"], req.GroupBy),
},
}
}
@@ -390,7 +519,8 @@ func buildLastSeenInfoQuery(req *thirdpartyapitypes.ThirdPartyApiRequest) qbtype
}
func buildBaseFilter(additionalFilter *qbtypes.Filter) *qbtypes.Filter {
baseExpression := fmt.Sprintf("%s EXISTS AND kind_string = 'Client'", derivedKeyHTTPURL)
baseExpression := fmt.Sprintf("(%s EXISTS OR %s EXISTS) AND kind_string = 'Client'",
urlPathKeyLegacy, urlPathKey)
if additionalFilter != nil && additionalFilter.Expression != "" {
// even if it contains kind_string we add with an AND so it doesn't matter if the user is overriding it.

View File

@@ -1,9 +1,8 @@
package thirdpartyapi
import (
"testing"
"github.com/SigNoz/signoz/pkg/types/thirdpartyapitypes"
"testing"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
@@ -29,7 +28,7 @@ func TestFilterResponse(t *testing.T) {
{
Labels: []*qbtypes.Label{
{
Key: telemetrytypes.TelemetryFieldKey{Name: derivedKeyHTTPHost},
Key: telemetrytypes.TelemetryFieldKey{Name: "net.peer.name"},
Value: "192.168.1.1",
},
},
@@ -37,7 +36,7 @@ func TestFilterResponse(t *testing.T) {
{
Labels: []*qbtypes.Label{
{
Key: telemetrytypes.TelemetryFieldKey{Name: derivedKeyHTTPHost},
Key: telemetrytypes.TelemetryFieldKey{Name: "net.peer.name"},
Value: "example.com",
},
},
@@ -61,7 +60,7 @@ func TestFilterResponse(t *testing.T) {
{
Labels: []*qbtypes.Label{
{
Key: telemetrytypes.TelemetryFieldKey{Name: derivedKeyHTTPHost},
Key: telemetrytypes.TelemetryFieldKey{Name: "net.peer.name"},
Value: "example.com",
},
},
@@ -85,12 +84,12 @@ func TestFilterResponse(t *testing.T) {
Rows: []*qbtypes.RawRow{
{
Data: map[string]any{
derivedKeyHTTPHost: "192.168.1.1",
"net.peer.name": "192.168.1.1",
},
},
{
Data: map[string]any{
derivedKeyHTTPHost: "example.com",
"net.peer.name": "example.com",
},
},
},
@@ -107,7 +106,7 @@ func TestFilterResponse(t *testing.T) {
Rows: []*qbtypes.RawRow{
{
Data: map[string]any{
derivedKeyHTTPHost: "example.com",
"net.peer.name": "example.com",
},
},
},

View File

@@ -13,6 +13,7 @@ import (
root "github.com/SigNoz/signoz/pkg/modules/user"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/integrationstypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux"
)
@@ -462,7 +463,7 @@ func (h *handler) UpdateAPIKey(w http.ResponseWriter, r *http.Request) {
return
}
if slices.Contains(types.AllIntegrationUserEmails, types.IntegrationUserEmail(createdByUser.Email.String())) {
if slices.Contains(integrationstypes.IntegrationUserEmails, createdByUser.Email) {
render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "API Keys for integration users cannot be revoked"))
return
}
@@ -507,7 +508,7 @@ func (h *handler) RevokeAPIKey(w http.ResponseWriter, r *http.Request) {
return
}
if slices.Contains(types.AllIntegrationUserEmails, types.IntegrationUserEmail(createdByUser.Email.String())) {
if slices.Contains(integrationstypes.IntegrationUserEmails, createdByUser.Email) {
render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "API Keys for integration users cannot be revoked"))
return
}

View File

@@ -19,6 +19,7 @@ import (
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/emailtypes"
"github.com/SigNoz/signoz/pkg/types/integrationstypes"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/dustin/go-humanize"
@@ -173,7 +174,7 @@ func (m *Module) DeleteInvite(ctx context.Context, orgID string, id valuer.UUID)
func (module *Module) CreateUser(ctx context.Context, input *types.User, opts ...root.CreateUserOption) error {
createUserOpts := root.NewCreateUserOptions(opts...)
// since assign is idempotant multiple calls to assign won't cause issues in case of retries.
// since assign is idempotent multiple calls to assign won't cause issues in case of retries.
err := module.authz.Grant(ctx, input.OrgID, roletypes.MustGetSigNozManagedRoleFromExistingRole(input.Role), authtypes.MustNewSubject(authtypes.TypeableUser, input.ID.StringValue(), input.OrgID, nil))
if err != nil {
return err
@@ -279,7 +280,7 @@ func (module *Module) DeleteUser(ctx context.Context, orgID valuer.UUID, id stri
return errors.WithAdditionalf(err, "cannot delete root user")
}
if slices.Contains(types.AllIntegrationUserEmails, types.IntegrationUserEmail(user.Email.String())) {
if slices.Contains(integrationstypes.IntegrationUserEmails, user.Email) {
return errors.New(errors.TypeForbidden, errors.CodeForbidden, "integration user cannot be deleted")
}
@@ -293,7 +294,7 @@ func (module *Module) DeleteUser(ctx context.Context, orgID valuer.UUID, id stri
return errors.New(errors.TypeForbidden, errors.CodeForbidden, "cannot delete the last admin")
}
// since revoke is idempotant multiple calls to revoke won't cause issues in case of retries
// since revoke is idempotent multiple calls to revoke won't cause issues in case of retries
err = module.authz.Revoke(ctx, orgID, roletypes.MustGetSigNozManagedRoleFromExistingRole(user.Role), authtypes.MustNewSubject(authtypes.TypeableUser, id, orgID, nil))
if err != nil {
return err

View File

@@ -1,454 +0,0 @@
package querier
import (
"net/http"
"github.com/SigNoz/signoz/pkg/http/handler"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
)
// QueryRangeV5OpenAPIDef is the OpenAPI definition for the /api/v5/query_range endpoint.
var QueryRangeV5OpenAPIDef = handler.OpenAPIDef{
ID: "QueryRangeV5",
Tags: []string{"query"},
Summary: "Query range",
Description: "Execute a composite query over a time range. Supports builder queries (traces, logs, metrics), formulas, trace operators, PromQL, and ClickHouse SQL.",
Request: new(qbtypes.QueryRangeRequest),
RequestContentType: "application/json",
RequestExamples: queryRangeV5Examples,
Response: new(qbtypes.QueryRangeResponse),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest},
SecuritySchemes: []handler.OpenAPISecurityScheme{
{Name: ctxtypes.AuthTypeAPIKey.StringValue(), Scopes: []string{"VIEWER"}},
{Name: ctxtypes.AuthTypeTokenizer.StringValue(), Scopes: []string{"VIEWER"}},
},
}
var queryRangeV5Examples = []handler.OpenAPIExample{
{
Name: "traces_time_series",
Summary: "Time series: count spans grouped by service",
Value: map[string]any{
"schemaVersion": "v1",
"start": 1640995200000,
"end": 1640998800000,
"requestType": "time_series",
"compositeQuery": map[string]any{
"queries": []any{
map[string]any{
"type": "builder_query",
"spec": map[string]any{
"name": "A",
"signal": "traces",
"aggregations": []any{
map[string]any{"expression": "count()", "alias": "span_count"},
},
"stepInterval": "60s",
"filter": map[string]any{"expression": "service.name = 'frontend'"},
"groupBy": []any{map[string]any{"name": "service.name", "fieldContext": "resource"}},
"order": []any{map[string]any{"key": map[string]any{"name": "span_count"}, "direction": "desc"}},
"limit": 10,
},
},
},
},
},
},
{
Name: "logs_time_series",
Summary: "Time series: count error logs grouped by service",
Value: map[string]any{
"schemaVersion": "v1",
"start": 1640995200000,
"end": 1640998800000,
"requestType": "time_series",
"compositeQuery": map[string]any{
"queries": []any{
map[string]any{
"type": "builder_query",
"spec": map[string]any{
"name": "A",
"signal": "logs",
"aggregations": []any{
map[string]any{"expression": "count()", "alias": "log_count"},
},
"stepInterval": "60s",
"filter": map[string]any{"expression": "severity_text = 'ERROR'"},
"groupBy": []any{map[string]any{"name": "service.name", "fieldContext": "resource"}},
"order": []any{map[string]any{"key": map[string]any{"name": "log_count"}, "direction": "desc"}},
"limit": 10,
},
},
},
},
},
},
{
Name: "metrics_gauge_time_series",
Summary: "Time series: latest gauge value averaged across series",
Value: map[string]any{
"schemaVersion": "v1",
"start": 1640995200000,
"end": 1640998800000,
"requestType": "time_series",
"compositeQuery": map[string]any{
"queries": []any{
map[string]any{
"type": "builder_query",
"spec": map[string]any{
"name": "A",
"signal": "metrics",
"aggregations": []any{
map[string]any{"metricName": "system.cpu.utilization", "timeAggregation": "latest", "spaceAggregation": "avg"},
},
"stepInterval": "60s",
"groupBy": []any{map[string]any{"name": "host.name", "fieldContext": "resource"}},
},
},
},
},
},
},
{
Name: "metrics_rate_time_series",
Summary: "Time series: rate of cumulative counter",
Value: map[string]any{
"schemaVersion": "v1",
"start": 1640995200000,
"end": 1640998800000,
"requestType": "time_series",
"compositeQuery": map[string]any{
"queries": []any{
map[string]any{
"type": "builder_query",
"spec": map[string]any{
"name": "A",
"signal": "metrics",
"aggregations": []any{
map[string]any{"metricName": "http.server.duration.count", "timeAggregation": "rate", "spaceAggregation": "sum"},
},
"stepInterval": 120,
"groupBy": []any{map[string]any{"name": "service.name", "fieldContext": "resource"}},
},
},
},
},
},
},
{
Name: "metrics_histogram_time_series",
Summary: "Time series: p99 latency from histogram",
Value: map[string]any{
"schemaVersion": "v1",
"start": 1640995200000,
"end": 1640998800000,
"requestType": "time_series",
"compositeQuery": map[string]any{
"queries": []any{
map[string]any{
"type": "builder_query",
"spec": map[string]any{
"name": "A",
"signal": "metrics",
"aggregations": []any{
map[string]any{"metricName": "http.server.duration.bucket", "spaceAggregation": "p99"},
},
"stepInterval": "60s",
"groupBy": []any{map[string]any{"name": "service.name", "fieldContext": "resource"}},
},
},
},
},
},
},
{
Name: "logs_raw",
Summary: "Raw: fetch raw log records",
Value: map[string]any{
"schemaVersion": "v1",
"start": 1640995200000,
"end": 1640998800000,
"requestType": "raw",
"compositeQuery": map[string]any{
"queries": []any{
map[string]any{
"type": "builder_query",
"spec": map[string]any{
"name": "A",
"signal": "logs",
"filter": map[string]any{"expression": "severity_text = 'ERROR'"},
"selectFields": []any{
map[string]any{"name": "body", "fieldContext": "log"},
map[string]any{"name": "service.name", "fieldContext": "resource"},
},
"order": []any{
map[string]any{"key": map[string]any{"name": "timestamp", "fieldContext": "log"}, "direction": "desc"},
map[string]any{"key": map[string]any{"name": "id"}, "direction": "desc"},
},
"limit": 50,
"offset": 0,
},
},
},
},
},
},
{
Name: "traces_raw",
Summary: "Raw: fetch raw span records",
Value: map[string]any{
"schemaVersion": "v1",
"start": 1640995200000,
"end": 1640998800000,
"requestType": "raw",
"compositeQuery": map[string]any{
"queries": []any{
map[string]any{
"type": "builder_query",
"spec": map[string]any{
"name": "A",
"signal": "traces",
"filter": map[string]any{"expression": "service.name = 'frontend' AND has_error = true"},
"selectFields": []any{
map[string]any{"name": "name", "fieldContext": "span"},
map[string]any{"name": "duration_nano", "fieldContext": "span"},
},
"order": []any{
map[string]any{"key": map[string]any{"name": "timestamp", "fieldContext": "span"}, "direction": "desc"},
},
"limit": 100,
},
},
},
},
},
},
{
Name: "traces_scalar",
Summary: "Scalar: total span count",
Value: map[string]any{
"schemaVersion": "v1",
"start": 1640995200000,
"end": 1640998800000,
"requestType": "scalar",
"compositeQuery": map[string]any{
"queries": []any{
map[string]any{
"type": "builder_query",
"spec": map[string]any{
"name": "A",
"signal": "traces",
"aggregations": []any{
map[string]any{"expression": "count()", "alias": "span_count"},
},
"filter": map[string]any{"expression": "service.name = 'frontend'"},
},
},
},
},
},
},
{
Name: "logs_scalar",
Summary: "Scalar: total error log count",
Value: map[string]any{
"schemaVersion": "v1",
"start": 1640995200000,
"end": 1640998800000,
"requestType": "scalar",
"compositeQuery": map[string]any{
"queries": []any{
map[string]any{
"type": "builder_query",
"spec": map[string]any{
"name": "A",
"signal": "logs",
"aggregations": []any{
map[string]any{"expression": "count()", "alias": "error_count"},
},
"filter": map[string]any{"expression": "severity_text = 'ERROR'"},
},
},
},
},
},
},
{
Name: "metrics_scalar",
Summary: "Scalar: single reduced metric value",
Value: map[string]any{
"schemaVersion": "v1",
"start": 1640995200000,
"end": 1640998800000,
"requestType": "scalar",
"compositeQuery": map[string]any{
"queries": []any{
map[string]any{
"type": "builder_query",
"spec": map[string]any{
"name": "A",
"signal": "metrics",
"aggregations": []any{
map[string]any{"metricName": "http.server.duration.count", "timeAggregation": "rate", "spaceAggregation": "sum", "reduceTo": "sum"},
},
"stepInterval": "60s",
},
},
},
},
},
},
{
Name: "formula",
Summary: "Formula: error rate from two trace queries",
Value: map[string]any{
"schemaVersion": "v1",
"start": 1640995200000,
"end": 1640998800000,
"requestType": "time_series",
"compositeQuery": map[string]any{
"queries": []any{
map[string]any{
"type": "builder_query",
"spec": map[string]any{
"name": "A",
"signal": "traces",
"aggregations": []any{map[string]any{"expression": "countIf(has_error = true)"}},
"stepInterval": "60s",
"groupBy": []any{map[string]any{"name": "service.name", "fieldContext": "resource"}},
},
},
map[string]any{
"type": "builder_query",
"spec": map[string]any{
"name": "B",
"signal": "traces",
"aggregations": []any{map[string]any{"expression": "count()"}},
"stepInterval": "60s",
"groupBy": []any{map[string]any{"name": "service.name", "fieldContext": "resource"}},
},
},
map[string]any{
"type": "builder_formula",
"spec": map[string]any{
"name": "error_rate",
"expression": "A / B * 100",
},
},
},
},
},
},
{
Name: "promql",
Summary: "PromQL: request rate with UTF-8 metric name",
Value: map[string]any{
"schemaVersion": "v1",
"start": 1640995200000,
"end": 1640998800000,
"requestType": "time_series",
"compositeQuery": map[string]any{
"queries": []any{
map[string]any{
"type": "promql",
"spec": map[string]any{
"name": "request_rate",
"query": "sum(rate({\"http.server.duration.count\"}[5m])) by (\"service.name\")",
"step": 60,
},
},
},
},
},
},
{
Name: "clickhouse_sql_traces_time_series",
Summary: "ClickHouse SQL: traces time series with resource filter",
Value: map[string]any{
"schemaVersion": "v1",
"start": 1640995200000,
"end": 1640998800000,
"requestType": "time_series",
"compositeQuery": map[string]any{
"queries": []any{
map[string]any{
"type": "clickhouse_sql",
"spec": map[string]any{
"name": "span_rate",
"query": "WITH __resource_filter AS (" +
" SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource" +
" WHERE seen_at_ts_bucket_start >= $start_timestamp - 1800 AND seen_at_ts_bucket_start <= $end_timestamp" +
" ) SELECT toStartOfInterval(timestamp, INTERVAL 60 SECOND) AS ts, count() AS value" +
" FROM signoz_traces.distributed_signoz_index_v3" +
" WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter)" +
" AND timestamp >= $start_datetime AND timestamp <= $end_datetime" +
" AND ts_bucket_start >= $start_timestamp - 1800 AND ts_bucket_start <= $end_timestamp" +
" GROUP BY ts ORDER BY ts",
},
},
},
},
},
},
{
Name: "clickhouse_sql_logs_raw",
Summary: "ClickHouse SQL: raw logs with resource filter",
Value: map[string]any{
"schemaVersion": "v1",
"start": 1640995200000,
"end": 1640998800000,
"requestType": "raw",
"compositeQuery": map[string]any{
"queries": []any{
map[string]any{
"type": "clickhouse_sql",
"spec": map[string]any{
"name": "recent_errors",
"query": "WITH __resource_filter AS (" +
" SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource" +
" WHERE seen_at_ts_bucket_start >= $start_timestamp - 1800 AND seen_at_ts_bucket_start <= $end_timestamp" +
" ) SELECT timestamp, body" +
" FROM signoz_logs.distributed_logs_v2" +
" WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter)" +
" AND timestamp >= $start_timestamp_nano AND timestamp <= $end_timestamp_nano" +
" AND ts_bucket_start >= $start_timestamp - 1800 AND ts_bucket_start <= $end_timestamp" +
" AND severity_text = 'ERROR'" +
" ORDER BY timestamp DESC LIMIT 100",
},
},
},
},
},
},
{
Name: "clickhouse_sql_traces_scalar",
Summary: "ClickHouse SQL: scalar aggregate with resource filter",
Value: map[string]any{
"schemaVersion": "v1",
"start": 1640995200000,
"end": 1640998800000,
"requestType": "scalar",
"compositeQuery": map[string]any{
"queries": []any{
map[string]any{
"type": "clickhouse_sql",
"spec": map[string]any{
"name": "total_spans",
"query": "WITH __resource_filter AS (" +
" SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource" +
" WHERE seen_at_ts_bucket_start >= $start_timestamp - 1800 AND seen_at_ts_bucket_start <= $end_timestamp" +
" ) SELECT count() AS value" +
" FROM signoz_traces.distributed_signoz_index_v3" +
" WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter)" +
" AND timestamp >= $start_datetime AND timestamp <= $end_datetime" +
" AND ts_bucket_start >= $start_timestamp - 1800 AND ts_bucket_start <= $end_timestamp",
},
},
},
},
},
},
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,814 @@
package implawsprovider
import (
"context"
"fmt"
"log/slog"
"net/url"
"slices"
"sort"
"sync"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations/services"
integrationstore "github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations/store"
"github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/types/integrationstypes"
"github.com/SigNoz/signoz/pkg/types/metrictypes"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
"golang.org/x/exp/maps"
)
var (
CodeInvalidAWSRegion = errors.MustNewCode("invalid_aws_region")
CodeDashboardNotFound = errors.MustNewCode("dashboard_not_found")
)
type awsProvider struct {
logger *slog.Logger
querier querier.Querier
accountsRepo integrationstore.CloudProviderAccountsRepository
serviceConfigRepo integrationstore.ServiceConfigDatabase
awsServiceDefinitions *services.AWSServicesProvider
}
func NewAWSCloudProvider(
logger *slog.Logger,
accountsRepo integrationstore.CloudProviderAccountsRepository,
serviceConfigRepo integrationstore.ServiceConfigDatabase,
querier querier.Querier,
) integrationstypes.CloudProvider {
awsServiceDefinitions, err := services.NewAWSCloudProviderServices()
if err != nil {
panic("failed to initialize AWS service definitions: " + err.Error())
}
return &awsProvider{
logger: logger,
querier: querier,
accountsRepo: accountsRepo,
serviceConfigRepo: serviceConfigRepo,
awsServiceDefinitions: awsServiceDefinitions,
}
}
func (a *awsProvider) GetAccountStatus(ctx context.Context, orgID, accountID string) (*integrationstypes.GettableAccountStatus, error) {
accountRecord, err := a.accountsRepo.Get(ctx, orgID, a.GetName().String(), accountID)
if err != nil {
return nil, err
}
return &integrationstypes.GettableAccountStatus{
Id: accountRecord.ID.String(),
CloudAccountId: accountRecord.AccountID,
Status: accountRecord.Status(),
}, nil
}
func (a *awsProvider) ListConnectedAccounts(ctx context.Context, orgID string) (*integrationstypes.GettableConnectedAccountsList, error) {
accountRecords, err := a.accountsRepo.ListConnected(ctx, orgID, a.GetName().String())
if err != nil {
return nil, err
}
connectedAccounts := make([]*integrationstypes.Account, 0, len(accountRecords))
for _, r := range accountRecords {
connectedAccounts = append(connectedAccounts, r.Account(a.GetName()))
}
return &integrationstypes.GettableConnectedAccountsList{
Accounts: connectedAccounts,
}, nil
}
func (a *awsProvider) AgentCheckIn(ctx context.Context, req *integrationstypes.PostableAgentCheckInPayload) (any, error) {
// agent can't check in unless the account is already created
existingAccount, err := a.accountsRepo.Get(ctx, req.OrgID, a.GetName().String(), req.ID)
if err != nil {
return nil, err
}
if existingAccount != nil && existingAccount.AccountID != nil && *existingAccount.AccountID != req.AccountID {
return nil, model.BadRequest(fmt.Errorf(
"can't check in with new %s account id %s for account %s with existing %s id %s",
a.GetName().String(), req.AccountID, existingAccount.ID.StringValue(), a.GetName().String(),
*existingAccount.AccountID,
))
}
existingAccount, err = a.accountsRepo.GetConnectedCloudAccount(ctx, req.OrgID, a.GetName().String(), req.AccountID)
if existingAccount != nil && existingAccount.ID.StringValue() != req.ID {
return nil, model.BadRequest(fmt.Errorf(
"can't check in to %s account %s with id %s. already connected with id %s",
a.GetName().String(), req.AccountID, req.ID, existingAccount.ID.StringValue(),
))
}
agentReport := integrationstypes.AgentReport{
TimestampMillis: time.Now().UnixMilli(),
Data: req.Data,
}
account, err := a.accountsRepo.Upsert(
ctx, req.OrgID, a.GetName().String(), &req.ID, nil, &req.AccountID, &agentReport, nil,
)
if err != nil {
return nil, err
}
agentConfig, err := a.getAWSAgentConfig(ctx, account)
if err != nil {
return nil, err
}
return &integrationstypes.GettableAWSAgentCheckIn{
AccountId: account.ID.StringValue(),
CloudAccountId: *account.AccountID,
RemovedAt: account.RemovedAt,
IntegrationConfig: *agentConfig,
}, nil
}
func (a *awsProvider) getAWSAgentConfig(ctx context.Context, account *integrationstypes.CloudIntegration) (*integrationstypes.AWSAgentIntegrationConfig, error) {
// prepare and return integration config to be consumed by agent
agentConfig := &integrationstypes.AWSAgentIntegrationConfig{
EnabledRegions: []string{},
TelemetryCollectionStrategy: &integrationstypes.AWSCollectionStrategy{
Provider: a.GetName(),
AWSMetrics: &integrationstypes.AWSMetricsStrategy{},
AWSLogs: &integrationstypes.AWSLogsStrategy{},
S3Buckets: map[string][]string{},
},
}
accountConfig := new(integrationstypes.AWSAccountConfig)
err := accountConfig.Unmarshal(account.Config)
if err != nil {
return nil, err
}
if accountConfig != nil && accountConfig.EnabledRegions != nil {
agentConfig.EnabledRegions = accountConfig.EnabledRegions
}
svcConfigs, err := a.serviceConfigRepo.GetAllForAccount(
ctx, account.OrgID, account.ID.StringValue(),
)
if err != nil {
return nil, err
}
// accumulate config in a fixed order to ensure same config generated across runs
configuredServices := maps.Keys(svcConfigs)
slices.Sort(configuredServices)
for _, svcType := range configuredServices {
definition, err := a.awsServiceDefinitions.GetServiceDefinition(ctx, svcType)
if err != nil {
continue
}
config := svcConfigs[svcType]
serviceConfig := new(integrationstypes.AWSCloudServiceConfig)
err = serviceConfig.Unmarshal(config)
if err != nil {
continue
}
if serviceConfig.Logs != nil && serviceConfig.Logs.Enabled {
if svcType == integrationstypes.S3Sync {
// S3 bucket sync; No cloudwatch logs are appended for this service type;
// Though definition is populated with a custom cloudwatch group that helps in calculating logs connection status
agentConfig.TelemetryCollectionStrategy.S3Buckets = serviceConfig.Logs.S3Buckets
} else if definition.Strategy.AWSLogs != nil { // services that includes a logs subscription
agentConfig.TelemetryCollectionStrategy.AWSLogs.Subscriptions = append(
agentConfig.TelemetryCollectionStrategy.AWSLogs.Subscriptions,
definition.Strategy.AWSLogs.Subscriptions...,
)
}
}
if serviceConfig.Metrics != nil && serviceConfig.Metrics.Enabled && definition.Strategy.AWSMetrics != nil {
agentConfig.TelemetryCollectionStrategy.AWSMetrics.StreamFilters = append(
agentConfig.TelemetryCollectionStrategy.AWSMetrics.StreamFilters,
definition.Strategy.AWSMetrics.StreamFilters...,
)
}
}
return agentConfig, nil
}
func (a *awsProvider) GetName() integrationstypes.CloudProviderType {
return integrationstypes.CloudProviderAWS
}
func (a *awsProvider) ListServices(ctx context.Context, orgID string, cloudAccountID *string) (any, error) {
svcConfigs := make(map[string]*integrationstypes.AWSCloudServiceConfig)
if cloudAccountID != nil {
activeAccount, err := a.accountsRepo.GetConnectedCloudAccount(ctx, orgID, a.GetName().String(), *cloudAccountID)
if err != nil {
return nil, err
}
serviceConfigs, err := a.serviceConfigRepo.GetAllForAccount(ctx, orgID, activeAccount.ID.String())
if err != nil {
return nil, err
}
for svcType, config := range serviceConfigs {
serviceConfig := new(integrationstypes.AWSCloudServiceConfig)
err = serviceConfig.Unmarshal(config)
if err != nil {
return nil, err
}
svcConfigs[svcType] = serviceConfig
}
}
summaries := make([]integrationstypes.AWSServiceSummary, 0)
definitions, err := a.awsServiceDefinitions.ListServiceDefinitions(ctx)
if err != nil {
return nil, model.InternalError(fmt.Errorf("couldn't list aws service definitions: %w", err))
}
for _, def := range definitions {
summary := integrationstypes.AWSServiceSummary{
DefinitionMetadata: def.DefinitionMetadata,
Config: nil,
}
summary.Config = svcConfigs[summary.Id]
summaries = append(summaries, summary)
}
sort.Slice(summaries, func(i, j int) bool {
return summaries[i].DefinitionMetadata.Title < summaries[j].DefinitionMetadata.Title
})
return &integrationstypes.GettableAWSServices{
Services: summaries,
}, nil
}
func (a *awsProvider) GetServiceDetails(ctx context.Context, req *integrationstypes.GetServiceDetailsReq) (any, error) {
details := new(integrationstypes.GettableAWSServiceDetails)
awsDefinition, err := a.awsServiceDefinitions.GetServiceDefinition(ctx, req.ServiceId)
if err != nil {
return nil, model.InternalError(fmt.Errorf("couldn't get aws service definition: %w", err))
}
details.AWSServiceDefinition = *awsDefinition
details.Strategy.Provider = a.GetName()
if req.CloudAccountID == nil {
return details, nil
}
config, err := a.getServiceConfig(ctx, &details.AWSServiceDefinition, req.OrgID, a.GetName().String(), req.ServiceId, *req.CloudAccountID)
if err != nil {
return nil, err
}
if config == nil {
return details, nil
}
details.Config = config
connectionStatus, err := a.getServiceConnectionStatus(
ctx,
*req.CloudAccountID,
req.OrgID,
&details.AWSServiceDefinition,
config,
)
if err != nil {
return nil, err
}
details.ConnectionStatus = connectionStatus
return details, nil
}
func (a *awsProvider) getServiceConnectionStatus(
ctx context.Context,
cloudAccountID string,
orgID string,
def *integrationstypes.AWSServiceDefinition,
serviceConfig *integrationstypes.AWSCloudServiceConfig,
) (*integrationstypes.ServiceConnectionStatus, error) {
if def.Strategy == nil {
return nil, nil
}
resp := new(integrationstypes.ServiceConnectionStatus)
wg := sync.WaitGroup{}
if def.Strategy.AWSMetrics != nil && serviceConfig.Metrics.Enabled {
wg.Add(1)
go func() {
defer func() {
if r := recover(); r != nil {
a.logger.ErrorContext(
ctx, "panic while getting service metrics connection status",
"error", r,
"service", def.DefinitionMetadata.Id,
)
}
}()
defer wg.Done()
status, _ := a.getServiceMetricsConnectionStatus(ctx, cloudAccountID, orgID, def)
resp.Metrics = status
}()
}
if def.Strategy.AWSLogs != nil && serviceConfig.Logs.Enabled {
wg.Add(1)
go func() {
defer func() {
if r := recover(); r != nil {
a.logger.ErrorContext(
ctx, "panic while getting service logs connection status",
"error", r,
"service", def.DefinitionMetadata.Id,
)
}
}()
defer wg.Done()
status, _ := a.getServiceLogsConnectionStatus(ctx, cloudAccountID, orgID, def)
resp.Logs = status
}()
}
wg.Wait()
return resp, nil
}
func (a *awsProvider) getServiceMetricsConnectionStatus(
ctx context.Context,
cloudAccountID string,
orgID string,
def *integrationstypes.AWSServiceDefinition,
) ([]*integrationstypes.SignalConnectionStatus, error) {
if def.Strategy == nil ||
len(def.Strategy.AWSMetrics.StreamFilters) < 1 ||
len(def.DataCollected.Metrics) < 1 {
return nil, nil
}
statusResp := make([]*integrationstypes.SignalConnectionStatus, 0)
for _, metric := range def.IngestionStatusCheck.Metrics {
statusResp = append(statusResp, &integrationstypes.SignalConnectionStatus{
CategoryID: metric.Category,
CategoryDisplayName: metric.DisplayName,
})
}
for index, category := range def.IngestionStatusCheck.Metrics {
queries := make([]qbtypes.QueryEnvelope, 0)
for _, check := range category.Checks {
filterExpression := fmt.Sprintf(`cloud.provider="aws" AND cloud.account.id="%s"`, cloudAccountID)
f := ""
for _, attribute := range check.Attributes {
f = fmt.Sprintf("%s %s", attribute.Name, attribute.Operator)
if attribute.Value != "" {
f = fmt.Sprintf("%s '%s'", f, attribute.Value)
}
filterExpression = fmt.Sprintf("%s AND %s", filterExpression, f)
}
queries = append(queries, qbtypes.QueryEnvelope{
Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
Name: valuer.GenerateUUID().String(),
Signal: telemetrytypes.SignalMetrics,
Aggregations: []qbtypes.MetricAggregation{{
MetricName: check.Key,
TimeAggregation: metrictypes.TimeAggregationAvg,
SpaceAggregation: metrictypes.SpaceAggregationAvg,
}},
Filter: &qbtypes.Filter{
Expression: filterExpression,
},
},
})
}
resp, err := a.querier.QueryRange(ctx, valuer.MustNewUUID(orgID), &qbtypes.QueryRangeRequest{
SchemaVersion: "v5",
Start: uint64(time.Now().Add(-time.Hour).UnixMilli()),
End: uint64(time.Now().UnixMilli()),
RequestType: qbtypes.RequestTypeScalar,
CompositeQuery: qbtypes.CompositeQuery{
Queries: queries,
},
})
if err != nil {
a.logger.DebugContext(ctx,
"error querying for service metrics connection status",
"error", err,
"service", def.DefinitionMetadata.Id,
)
continue
}
if resp != nil && len(resp.Data.Results) < 1 {
continue
}
queryResponse, ok := resp.Data.Results[0].(*qbtypes.TimeSeriesData)
if !ok {
continue
}
if queryResponse == nil ||
len(queryResponse.Aggregations) < 1 ||
len(queryResponse.Aggregations[0].Series) < 1 ||
len(queryResponse.Aggregations[0].Series[0].Values) < 1 {
continue
}
statusResp[index] = &integrationstypes.SignalConnectionStatus{
CategoryID: category.Category,
CategoryDisplayName: category.DisplayName,
LastReceivedTsMillis: queryResponse.Aggregations[0].Series[0].Values[0].Timestamp,
LastReceivedFrom: "signoz-aws-integration",
}
}
return statusResp, nil
}
func (a *awsProvider) getServiceLogsConnectionStatus(
ctx context.Context,
cloudAccountID string,
orgID string,
def *integrationstypes.AWSServiceDefinition,
) ([]*integrationstypes.SignalConnectionStatus, error) {
if def.Strategy == nil ||
len(def.Strategy.AWSLogs.Subscriptions) < 1 ||
len(def.DataCollected.Logs) < 1 {
return nil, nil
}
statusResp := make([]*integrationstypes.SignalConnectionStatus, 0)
for _, log := range def.IngestionStatusCheck.Logs {
statusResp = append(statusResp, &integrationstypes.SignalConnectionStatus{
CategoryID: log.Category,
CategoryDisplayName: log.DisplayName,
})
}
for index, category := range def.IngestionStatusCheck.Logs {
queries := make([]qbtypes.QueryEnvelope, 0)
for _, check := range category.Checks {
filterExpression := fmt.Sprintf(`cloud.account.id="%s"`, cloudAccountID)
f := ""
for _, attribute := range check.Attributes {
f = fmt.Sprintf("%s %s", attribute.Name, attribute.Operator)
if attribute.Value != "" {
f = fmt.Sprintf("%s '%s'", f, attribute.Value)
}
filterExpression = fmt.Sprintf("%s AND %s", filterExpression, f)
}
queries = append(queries, qbtypes.QueryEnvelope{
Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Name: valuer.GenerateUUID().String(),
Signal: telemetrytypes.SignalLogs,
Aggregations: []qbtypes.LogAggregation{{
Expression: "count()",
}},
Filter: &qbtypes.Filter{
Expression: filterExpression,
},
Limit: 10,
Offset: 0,
},
})
}
resp, err := a.querier.QueryRange(ctx, valuer.MustNewUUID(orgID), &qbtypes.QueryRangeRequest{
SchemaVersion: "v1",
Start: uint64(time.Now().Add(-time.Hour * 1).UnixMilli()),
End: uint64(time.Now().UnixMilli()),
RequestType: qbtypes.RequestTypeTimeSeries,
CompositeQuery: qbtypes.CompositeQuery{
Queries: queries,
},
})
if err != nil {
a.logger.DebugContext(ctx,
"error querying for service logs connection status",
"error", err,
"service", def.DefinitionMetadata.Id,
)
continue
}
if resp != nil && len(resp.Data.Results) < 1 {
continue
}
queryResponse, ok := resp.Data.Results[0].(*qbtypes.TimeSeriesData)
if !ok {
continue
}
if queryResponse == nil ||
len(queryResponse.Aggregations) < 1 ||
len(queryResponse.Aggregations[0].Series) < 1 ||
len(queryResponse.Aggregations[0].Series[0].Values) < 1 {
continue
}
statusResp[index] = &integrationstypes.SignalConnectionStatus{
CategoryID: category.Category,
CategoryDisplayName: category.DisplayName,
LastReceivedTsMillis: queryResponse.Aggregations[0].Series[0].Values[0].Timestamp,
LastReceivedFrom: "signoz-aws-integration",
}
}
return statusResp, nil
}
func (a *awsProvider) getServiceConfig(ctx context.Context,
def *integrationstypes.AWSServiceDefinition, orgID, cloudProvider, serviceId, cloudAccountId string,
) (*integrationstypes.AWSCloudServiceConfig, error) {
activeAccount, err := a.accountsRepo.GetConnectedCloudAccount(ctx, orgID, cloudProvider, cloudAccountId)
if err != nil {
return nil, err
}
config, err := a.serviceConfigRepo.Get(ctx, orgID, activeAccount.ID.StringValue(), serviceId)
if err != nil {
if errors.Ast(err, errors.TypeNotFound) {
return nil, nil
}
return nil, err
}
serviceConfig := new(integrationstypes.AWSCloudServiceConfig)
err = serviceConfig.Unmarshal(config)
if err != nil {
return nil, err
}
if config != nil && serviceConfig.Metrics != nil && serviceConfig.Metrics.Enabled {
def.PopulateDashboardURLs(serviceId)
}
return serviceConfig, nil
}
func (a *awsProvider) GetAvailableDashboards(ctx context.Context, orgID valuer.UUID) ([]*dashboardtypes.Dashboard, error) {
accountRecords, err := a.accountsRepo.ListConnected(ctx, orgID.StringValue(), a.GetName().String())
if err != nil {
return nil, err
}
// for now service dashboards are only available when metrics are enabled.
servicesWithAvailableMetrics := map[string]*time.Time{}
for _, ar := range accountRecords {
if ar.AccountID != nil {
configsBySvcId, err := a.serviceConfigRepo.GetAllForAccount(ctx, orgID.StringValue(), ar.ID.StringValue())
if err != nil {
return nil, err
}
for svcId, config := range configsBySvcId {
serviceConfig := new(integrationstypes.AWSCloudServiceConfig)
err = serviceConfig.Unmarshal(config)
if err != nil {
return nil, err
}
if serviceConfig.Metrics != nil && serviceConfig.Metrics.Enabled {
servicesWithAvailableMetrics[svcId] = &ar.CreatedAt
}
}
}
}
svcDashboards := make([]*dashboardtypes.Dashboard, 0)
allServices, err := a.awsServiceDefinitions.ListServiceDefinitions(ctx)
if err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to list aws service definitions")
}
for _, svc := range allServices {
serviceDashboardsCreatedAt, ok := servicesWithAvailableMetrics[svc.Id]
if ok {
svcDashboards = append(
svcDashboards,
integrationstypes.GetDashboardsFromAssets(svc.Id, a.GetName(), serviceDashboardsCreatedAt, svc.Assets)...,
)
servicesWithAvailableMetrics[svc.Id] = nil
}
}
return svcDashboards, nil
}
func (a *awsProvider) GetDashboard(ctx context.Context, req *integrationstypes.GettableDashboard) (*dashboardtypes.Dashboard, error) {
allDashboards, err := a.GetAvailableDashboards(ctx, req.OrgID)
if err != nil {
return nil, err
}
for _, d := range allDashboards {
if d.ID == req.ID {
return d, nil
}
}
return nil, errors.NewNotFoundf(CodeDashboardNotFound, "dashboard with id %s not found", req.ID)
}
func (a *awsProvider) GenerateConnectionArtifact(ctx context.Context, req *integrationstypes.PostableConnectionArtifact) (any, error) {
connection := new(integrationstypes.PostableAWSConnectionUrl)
err := connection.Unmarshal(req.Data)
if err != nil {
return nil, err
}
if connection.AccountConfig != nil {
for _, region := range connection.AccountConfig.EnabledRegions {
if integrationstypes.ValidAWSRegions[region] {
continue
}
return nil, errors.NewInvalidInputf(CodeInvalidAWSRegion, "invalid aws region: %s", region)
}
}
config, err := connection.AccountConfig.Marshal()
if err != nil {
return nil, err
}
account, err := a.accountsRepo.Upsert(
ctx, req.OrgID, integrationstypes.CloudProviderAWS.String(), nil, config,
nil, nil, nil,
)
if err != nil {
return nil, err
}
agentVersion := "v0.0.8"
if connection.AgentConfig.Version != "" {
agentVersion = connection.AgentConfig.Version
}
baseURL := fmt.Sprintf("https://%s.console.aws.amazon.com/cloudformation/home",
connection.AgentConfig.Region)
u, _ := url.Parse(baseURL)
q := u.Query()
q.Set("region", connection.AgentConfig.Region)
u.Fragment = "/stacks/quickcreate"
u.RawQuery = q.Encode()
q = u.Query()
q.Set("stackName", "signoz-integration")
q.Set("templateURL", fmt.Sprintf("https://signoz-integrations.s3.us-east-1.amazonaws.com/aws-quickcreate-template-%s.json", agentVersion))
q.Set("param_SigNozIntegrationAgentVersion", agentVersion)
q.Set("param_SigNozApiUrl", connection.AgentConfig.SigNozAPIUrl)
q.Set("param_SigNozApiKey", connection.AgentConfig.SigNozAPIKey)
q.Set("param_SigNozAccountId", account.ID.StringValue())
q.Set("param_IngestionUrl", connection.AgentConfig.IngestionUrl)
q.Set("param_IngestionKey", connection.AgentConfig.IngestionKey)
return &integrationstypes.GettableAWSConnectionUrl{
AccountId: account.ID.StringValue(),
ConnectionUrl: u.String() + "?&" + q.Encode(), // this format is required by AWS
}, nil
}
func (a *awsProvider) UpdateServiceConfig(ctx context.Context, req *integrationstypes.PatchableServiceConfig) (any, error) {
definition, err := a.awsServiceDefinitions.GetServiceDefinition(ctx, req.ServiceId)
if err != nil {
return nil, model.InternalError(fmt.Errorf("couldn't get aws service definition: %w", err))
}
serviceConfig := new(integrationstypes.PatchableAWSCloudServiceConfig)
err = serviceConfig.Unmarshal(req.Config)
if err != nil {
return nil, err
}
if err = serviceConfig.Config.Validate(definition); err != nil {
return nil, err
}
// can only update config for a connected cloud account id
_, err = a.accountsRepo.GetConnectedCloudAccount(
ctx, req.OrgID, a.GetName().String(), serviceConfig.CloudAccountId,
)
if err != nil {
return nil, err
}
serviceConfigBytes, err := serviceConfig.Config.Marshal()
if err != nil {
return nil, err
}
updatedConfig, err := a.serviceConfigRepo.Upsert(
ctx, req.OrgID, a.GetName().String(), serviceConfig.CloudAccountId, req.ServiceId, serviceConfigBytes,
)
if err != nil {
return nil, err
}
if err = serviceConfig.Unmarshal(updatedConfig); err != nil {
return nil, err
}
return &integrationstypes.PatchServiceConfigResponse{
ServiceId: req.ServiceId,
Config: serviceConfig,
}, nil
}
func (a *awsProvider) UpdateAccountConfig(ctx context.Context, req *integrationstypes.PatchableAccountConfig) (any, error) {
config := new(integrationstypes.PatchableAWSAccountConfig)
err := config.Unmarshal(req.Data)
if err != nil {
return nil, err
}
if config.Config == nil {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "account config can't be null")
}
for _, region := range config.Config.EnabledRegions {
if integrationstypes.ValidAWSRegions[region] {
continue
}
return nil, errors.NewInvalidInputf(CodeInvalidAWSRegion, "invalid aws region: %s", region)
}
configBytes, err := config.Config.Marshal()
if err != nil {
return nil, err
}
// account must exist to update config, but it doesn't need to be connected
_, err = a.accountsRepo.Get(ctx, req.OrgID, a.GetName().String(), req.AccountId)
if err != nil {
return nil, err
}
accountRecord, err := a.accountsRepo.Upsert(
ctx, req.OrgID, a.GetName().String(), &req.AccountId, configBytes, nil, nil, nil,
)
if err != nil {
return nil, err
}
return accountRecord.Account(a.GetName()), nil
}
func (a *awsProvider) DisconnectAccount(ctx context.Context, orgID, accountID string) (*integrationstypes.CloudIntegration, error) {
account, err := a.accountsRepo.Get(ctx, orgID, a.GetName().String(), accountID)
if err != nil {
return nil, err
}
tsNow := time.Now()
account, err = a.accountsRepo.Upsert(
ctx, orgID, a.GetName().String(), &accountID, nil, nil, nil, &tsNow,
)
if err != nil {
return nil, err
}
return account, nil
}

View File

@@ -0,0 +1,878 @@
package implazureprovider
import (
"context"
"fmt"
"log/slog"
"slices"
"sort"
"strings"
"sync"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations/services"
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations/store"
"github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/types/integrationstypes"
"github.com/SigNoz/signoz/pkg/types/metrictypes"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
"golang.org/x/exp/maps"
)
var (
CodeInvalidAzureRegion = errors.MustNewCode("invalid_azure_region")
CodeDashboardNotFound = errors.MustNewCode("dashboard_not_found")
)
type azureProvider struct {
logger *slog.Logger
accountsRepo store.CloudProviderAccountsRepository
serviceConfigRepo store.ServiceConfigDatabase
azureServiceDefinitions *services.AzureServicesProvider
querier querier.Querier
}
func NewAzureCloudProvider(
logger *slog.Logger,
accountsRepo store.CloudProviderAccountsRepository,
serviceConfigRepo store.ServiceConfigDatabase,
querier querier.Querier,
) integrationstypes.CloudProvider {
azureServiceDefinitions, err := services.NewAzureCloudProviderServices()
if err != nil {
panic("failed to initialize Azure service definitions: " + err.Error())
}
return &azureProvider{
logger: logger,
accountsRepo: accountsRepo,
serviceConfigRepo: serviceConfigRepo,
azureServiceDefinitions: azureServiceDefinitions,
querier: querier,
}
}
func (a *azureProvider) GetAccountStatus(ctx context.Context, orgID, accountID string) (*integrationstypes.GettableAccountStatus, error) {
account, err := a.accountsRepo.Get(ctx, orgID, a.GetName().String(), accountID)
if err != nil {
return nil, err
}
return &integrationstypes.GettableAccountStatus{
Id: account.ID.String(),
CloudAccountId: account.AccountID,
Status: account.Status(),
}, nil
}
func (a *azureProvider) ListConnectedAccounts(ctx context.Context, orgID string) (*integrationstypes.GettableConnectedAccountsList, error) {
accountRecords, err := a.accountsRepo.ListConnected(ctx, orgID, a.GetName().String())
if err != nil {
return nil, err
}
connectedAccounts := make([]*integrationstypes.Account, 0, len(accountRecords))
for _, r := range accountRecords {
connectedAccounts = append(connectedAccounts, r.Account(a.GetName()))
}
return &integrationstypes.GettableConnectedAccountsList{
Accounts: connectedAccounts,
}, nil
}
func (a *azureProvider) AgentCheckIn(ctx context.Context, req *integrationstypes.PostableAgentCheckInPayload) (any, error) {
existingAccount, err := a.accountsRepo.Get(ctx, req.OrgID, a.GetName().String(), req.ID)
if err != nil {
return nil, err
}
if existingAccount != nil && existingAccount.AccountID != nil && *existingAccount.AccountID != req.AccountID {
return nil, model.BadRequest(fmt.Errorf(
"can't check in with new %s account id %s for account %s with existing %s id %s",
a.GetName().String(), req.AccountID, existingAccount.ID.StringValue(), a.GetName().String(),
*existingAccount.AccountID,
))
}
existingAccount, err = a.accountsRepo.GetConnectedCloudAccount(ctx, req.OrgID, a.GetName().String(), req.AccountID)
if existingAccount != nil && existingAccount.ID.StringValue() != req.ID {
return nil, model.BadRequest(fmt.Errorf(
"can't check in to %s account %s with id %s. already connected with id %s",
a.GetName().String(), req.AccountID, req.ID, existingAccount.ID.StringValue(),
))
}
agentReport := integrationstypes.AgentReport{
TimestampMillis: time.Now().UnixMilli(),
Data: req.Data,
}
account, err := a.accountsRepo.Upsert(
ctx, req.OrgID, a.GetName().String(), &req.ID, nil, &req.AccountID, &agentReport, nil,
)
if err != nil {
return nil, err
}
agentConfig, err := a.getAzureAgentConfig(ctx, account)
if err != nil {
return nil, err
}
return &integrationstypes.GettableAzureAgentCheckIn{
AccountId: account.ID.StringValue(),
CloudAccountId: *account.AccountID,
RemovedAt: account.RemovedAt,
IntegrationConfig: *agentConfig,
}, nil
}
func (a *azureProvider) getAzureAgentConfig(ctx context.Context, account *integrationstypes.CloudIntegration) (*integrationstypes.AzureAgentIntegrationConfig, error) {
// prepare and return integration config to be consumed by agent
agentConfig := &integrationstypes.AzureAgentIntegrationConfig{
TelemetryCollectionStrategy: make(map[string]*integrationstypes.AzureCollectionStrategy),
}
accountConfig := new(integrationstypes.AzureAccountConfig)
err := accountConfig.Unmarshal(account.Config)
if err != nil {
return nil, err
}
if account.Config != nil {
agentConfig.DeploymentRegion = accountConfig.DeploymentRegion
agentConfig.EnabledResourceGroups = accountConfig.EnabledResourceGroups
}
svcConfigs, err := a.serviceConfigRepo.GetAllForAccount(
ctx, account.OrgID, account.ID.StringValue(),
)
if err != nil {
return nil, err
}
// accumulate config in a fixed order to ensure same config generated across runs
configuredServices := maps.Keys(svcConfigs)
slices.Sort(configuredServices)
metrics := make([]*integrationstypes.AzureMetricsStrategy, 0)
logs := make([]*integrationstypes.AzureLogsStrategy, 0)
for _, svcType := range configuredServices {
definition, err := a.azureServiceDefinitions.GetServiceDefinition(ctx, svcType)
if err != nil {
continue
}
config := svcConfigs[svcType]
serviceConfig := new(integrationstypes.AzureCloudServiceConfig)
err = serviceConfig.Unmarshal(config)
if err != nil {
continue
}
metricsStrategyMap := make(map[string]*integrationstypes.AzureMetricsStrategy)
logsStrategyMap := make(map[string]*integrationstypes.AzureLogsStrategy)
for _, metric := range definition.Strategy.AzureMetrics {
metricsStrategyMap[metric.Name] = metric
}
for _, log := range definition.Strategy.AzureLogs {
logsStrategyMap[log.Name] = log
}
if serviceConfig.Metrics != nil {
for _, metric := range serviceConfig.Metrics {
if metric.Enabled {
metrics = append(metrics, &integrationstypes.AzureMetricsStrategy{
CategoryType: metricsStrategyMap[metric.Name].CategoryType,
Name: metric.Name,
})
}
}
}
if serviceConfig.Logs != nil {
for _, log := range serviceConfig.Logs {
if log.Enabled {
logs = append(logs, &integrationstypes.AzureLogsStrategy{
CategoryType: logsStrategyMap[log.Name].CategoryType,
Name: log.Name,
})
}
}
}
strategy := integrationstypes.AzureCollectionStrategy{}
strategy.AzureMetrics = metrics
strategy.AzureLogs = logs
agentConfig.TelemetryCollectionStrategy[svcType] = &strategy
}
return agentConfig, nil
}
func (a *azureProvider) GetName() valuer.String {
return integrationstypes.CloudProviderAzure
}
func (a *azureProvider) ListServices(ctx context.Context, orgID string, cloudAccountID *string) (any, error) {
svcConfigs := make(map[string]*integrationstypes.AzureCloudServiceConfig)
if cloudAccountID != nil {
activeAccount, err := a.accountsRepo.GetConnectedCloudAccount(ctx, orgID, a.GetName().String(), *cloudAccountID)
if err != nil {
return nil, err
}
serviceConfigs, err := a.serviceConfigRepo.GetAllForAccount(ctx, orgID, activeAccount.ID.StringValue())
if err != nil {
return nil, err
}
for svcType, config := range serviceConfigs {
serviceConfig := new(integrationstypes.AzureCloudServiceConfig)
err = serviceConfig.Unmarshal(config)
if err != nil {
return nil, err
}
svcConfigs[svcType] = serviceConfig
}
}
summaries := make([]integrationstypes.AzureServiceSummary, 0)
definitions, err := a.azureServiceDefinitions.ListServiceDefinitions(ctx)
if err != nil {
return nil, model.InternalError(fmt.Errorf("couldn't list aws service definitions: %w", err))
}
for _, def := range definitions {
summary := integrationstypes.AzureServiceSummary{
DefinitionMetadata: def.DefinitionMetadata,
Config: nil,
}
summary.Config = svcConfigs[summary.Id]
summaries = append(summaries, summary)
}
sort.Slice(summaries, func(i, j int) bool {
return summaries[i].DefinitionMetadata.Title < summaries[j].DefinitionMetadata.Title
})
return &integrationstypes.GettableAzureServices{
Services: summaries,
}, nil
}
func (a *azureProvider) GetServiceDetails(ctx context.Context, req *integrationstypes.GetServiceDetailsReq) (any, error) {
details := new(integrationstypes.GettableAzureServiceDetails)
azureDefinition, err := a.azureServiceDefinitions.GetServiceDefinition(ctx, req.ServiceId)
if err != nil {
return nil, model.InternalError(fmt.Errorf("couldn't get aws service definition: %w", err))
}
details.AzureServiceDefinition = *azureDefinition
if req.CloudAccountID == nil {
return details, nil
}
config, err := a.getServiceConfig(ctx, &details.AzureServiceDefinition, req.OrgID, req.ServiceId, *req.CloudAccountID)
if err != nil {
return nil, err
}
details.Config = config
// fill default values for config
if details.Config == nil {
cfg := new(integrationstypes.AzureCloudServiceConfig)
logs := make([]*integrationstypes.AzureCloudServiceLogsConfig, 0)
for _, log := range azureDefinition.Strategy.AzureLogs {
logs = append(logs, &integrationstypes.AzureCloudServiceLogsConfig{
Enabled: false,
Name: log.Name,
})
}
metrics := make([]*integrationstypes.AzureCloudServiceMetricsConfig, 0)
for _, metric := range azureDefinition.Strategy.AzureMetrics {
metrics = append(metrics, &integrationstypes.AzureCloudServiceMetricsConfig{
Enabled: false,
Name: metric.Name,
})
}
cfg.Logs = logs
cfg.Metrics = metrics
details.Config = cfg
}
connectionStatus, err := a.getServiceConnectionStatus(
ctx,
*req.CloudAccountID,
req.OrgID,
&details.AzureServiceDefinition,
config,
)
if err != nil {
return nil, err
}
details.ConnectionStatus = connectionStatus
return details, nil
}
func (a *azureProvider) getServiceConnectionStatus(
ctx context.Context,
cloudAccountID string,
orgID string,
def *integrationstypes.AzureServiceDefinition,
serviceConfig *integrationstypes.AzureCloudServiceConfig,
) (*integrationstypes.ServiceConnectionStatus, error) {
if def.Strategy == nil {
return nil, nil
}
resp := new(integrationstypes.ServiceConnectionStatus)
wg := sync.WaitGroup{}
if def.Strategy.AzureMetrics != nil {
wg.Add(1)
go func() {
defer func() {
if r := recover(); r != nil {
a.logger.ErrorContext(
ctx, "panic while getting service metrics connection status",
"error", r,
"service", def.DefinitionMetadata.Id,
)
}
}()
defer wg.Done()
enabled := false
for _, cfg := range serviceConfig.Metrics {
if cfg.Enabled {
enabled = true
break
}
}
if !enabled {
return
}
status, _ := a.getServiceMetricsConnectionStatus(ctx, cloudAccountID, orgID, def)
resp.Metrics = status
}()
}
if def.Strategy.AzureLogs != nil {
wg.Add(1)
go func() {
defer func() {
if r := recover(); r != nil {
a.logger.ErrorContext(
ctx, "panic while getting service logs connection status",
"error", r,
"service", def.DefinitionMetadata.Id,
)
}
}()
defer wg.Done()
enabled := false
for _, cfg := range serviceConfig.Logs {
if cfg.Enabled {
enabled = true
break
}
}
if !enabled {
return
}
status, _ := a.getServiceLogsConnectionStatus(ctx, cloudAccountID, orgID, def)
resp.Logs = status
}()
}
wg.Wait()
return resp, nil
}
func (a *azureProvider) getServiceMetricsConnectionStatus(
ctx context.Context,
cloudAccountID string,
orgID string,
def *integrationstypes.AzureServiceDefinition,
) ([]*integrationstypes.SignalConnectionStatus, error) {
if def.Strategy == nil ||
len(def.Strategy.AzureMetrics) < 1 ||
len(def.DataCollected.Metrics) < 1 {
return nil, nil
}
statusResp := make([]*integrationstypes.SignalConnectionStatus, 0)
for _, metric := range def.IngestionStatusCheck.Metrics {
statusResp = append(statusResp, &integrationstypes.SignalConnectionStatus{
CategoryID: metric.Category,
CategoryDisplayName: metric.DisplayName,
})
}
for index, category := range def.IngestionStatusCheck.Metrics {
queries := make([]qbtypes.QueryEnvelope, 0)
for _, check := range category.Checks {
filterExpression := fmt.Sprintf(`cloud.provider="aws" AND cloud.account.id="%s"`, cloudAccountID)
f := ""
for _, attribute := range check.Attributes {
f = fmt.Sprintf("%s %s", attribute.Name, attribute.Operator)
if attribute.Value != "" {
f = fmt.Sprintf("%s '%s'", f, attribute.Value)
}
filterExpression = fmt.Sprintf("%s AND %s", filterExpression, f)
}
queries = append(queries, qbtypes.QueryEnvelope{
Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
Name: valuer.GenerateUUID().String(),
Signal: telemetrytypes.SignalMetrics,
Aggregations: []qbtypes.MetricAggregation{{
MetricName: check.Key,
TimeAggregation: metrictypes.TimeAggregationAvg,
SpaceAggregation: metrictypes.SpaceAggregationAvg,
}},
Filter: &qbtypes.Filter{
Expression: filterExpression,
},
},
})
}
resp, err := a.querier.QueryRange(ctx, valuer.MustNewUUID(orgID), &qbtypes.QueryRangeRequest{
SchemaVersion: "v5",
Start: uint64(time.Now().Add(-time.Hour).UnixMilli()),
End: uint64(time.Now().UnixMilli()),
RequestType: qbtypes.RequestTypeScalar,
CompositeQuery: qbtypes.CompositeQuery{
Queries: queries,
},
})
if err != nil {
a.logger.DebugContext(ctx,
"error querying for service metrics connection status",
"error", err,
"service", def.DefinitionMetadata.Id,
)
continue
}
if resp != nil && len(resp.Data.Results) < 1 {
continue
}
queryResponse, ok := resp.Data.Results[0].(*qbtypes.TimeSeriesData)
if !ok {
continue
}
if queryResponse == nil ||
len(queryResponse.Aggregations) < 1 ||
len(queryResponse.Aggregations[0].Series) < 1 ||
len(queryResponse.Aggregations[0].Series[0].Values) < 1 {
continue
}
statusResp[index] = &integrationstypes.SignalConnectionStatus{
CategoryID: category.Category,
CategoryDisplayName: category.DisplayName,
LastReceivedTsMillis: queryResponse.Aggregations[0].Series[0].Values[0].Timestamp,
LastReceivedFrom: "signoz-aws-integration",
}
}
return statusResp, nil
}
func (a *azureProvider) getServiceLogsConnectionStatus(
ctx context.Context,
cloudAccountID string,
orgID string,
def *integrationstypes.AzureServiceDefinition,
) ([]*integrationstypes.SignalConnectionStatus, error) {
if def.Strategy == nil ||
len(def.Strategy.AzureLogs) < 1 ||
len(def.DataCollected.Logs) < 1 {
return nil, nil
}
statusResp := make([]*integrationstypes.SignalConnectionStatus, 0)
for _, log := range def.IngestionStatusCheck.Logs {
statusResp = append(statusResp, &integrationstypes.SignalConnectionStatus{
CategoryID: log.Category,
CategoryDisplayName: log.DisplayName,
})
}
for index, category := range def.IngestionStatusCheck.Logs {
queries := make([]qbtypes.QueryEnvelope, 0)
for _, check := range category.Checks {
filterExpression := fmt.Sprintf(`cloud.account.id="%s"`, cloudAccountID)
f := ""
for _, attribute := range check.Attributes {
f = fmt.Sprintf("%s %s", attribute.Name, attribute.Operator)
if attribute.Value != "" {
f = fmt.Sprintf("%s '%s'", f, attribute.Value)
}
filterExpression = fmt.Sprintf("%s AND %s", filterExpression, f)
}
queries = append(queries, qbtypes.QueryEnvelope{
Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Name: valuer.GenerateUUID().String(),
Signal: telemetrytypes.SignalLogs,
Aggregations: []qbtypes.LogAggregation{{
Expression: "count()",
}},
Filter: &qbtypes.Filter{
Expression: filterExpression,
},
Limit: 10,
Offset: 0,
},
})
}
resp, err := a.querier.QueryRange(ctx, valuer.MustNewUUID(orgID), &qbtypes.QueryRangeRequest{
SchemaVersion: "v1",
Start: uint64(time.Now().Add(-time.Hour * 1).UnixMilli()),
End: uint64(time.Now().UnixMilli()),
RequestType: qbtypes.RequestTypeTimeSeries,
CompositeQuery: qbtypes.CompositeQuery{
Queries: queries,
},
})
if err != nil {
a.logger.DebugContext(ctx,
"error querying for service logs connection status",
"error", err,
"service", def.DefinitionMetadata.Id,
)
continue
}
if resp != nil && len(resp.Data.Results) < 1 {
continue
}
queryResponse, ok := resp.Data.Results[0].(*qbtypes.TimeSeriesData)
if !ok {
continue
}
if queryResponse == nil ||
len(queryResponse.Aggregations) < 1 ||
len(queryResponse.Aggregations[0].Series) < 1 ||
len(queryResponse.Aggregations[0].Series[0].Values) < 1 {
continue
}
statusResp[index] = &integrationstypes.SignalConnectionStatus{
CategoryID: category.Category,
CategoryDisplayName: category.DisplayName,
LastReceivedTsMillis: queryResponse.Aggregations[0].Series[0].Values[0].Timestamp,
LastReceivedFrom: "signoz-aws-integration",
}
}
return statusResp, nil
}
func (a *azureProvider) getServiceConfig(
ctx context.Context,
definition *integrationstypes.AzureServiceDefinition,
orgID string,
serviceId string,
cloudAccountId string,
) (*integrationstypes.AzureCloudServiceConfig, error) {
activeAccount, err := a.accountsRepo.GetConnectedCloudAccount(ctx, orgID, a.GetName().String(), cloudAccountId)
if err != nil {
return nil, err
}
configBytes, err := a.serviceConfigRepo.Get(ctx, orgID, activeAccount.ID.String(), serviceId)
if err != nil {
if errors.Ast(err, errors.TypeNotFound) {
return nil, nil
}
return nil, err
}
config := new(integrationstypes.AzureCloudServiceConfig)
err = config.Unmarshal(configBytes)
if err != nil {
return nil, err
}
for _, metric := range config.Metrics {
if metric.Enabled {
definition.PopulateDashboardURLs(serviceId)
break
}
}
return config, nil
}
func (a *azureProvider) GenerateConnectionArtifact(ctx context.Context, req *integrationstypes.PostableConnectionArtifact) (any, error) {
connection := new(integrationstypes.PostableAzureConnectionCommand)
err := connection.Unmarshal(req.Data)
if err != nil {
return nil, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "failed unmarshal request data into AWS connection config")
}
// validate connection config
if connection.AccountConfig != nil {
if !integrationstypes.ValidAzureRegions[connection.AccountConfig.DeploymentRegion] {
return nil, errors.NewInvalidInputf(CodeInvalidAzureRegion, "invalid azure region: %s",
connection.AccountConfig.DeploymentRegion,
)
}
}
config, err := connection.AccountConfig.Marshal()
if err != nil {
return nil, err
}
account, err := a.accountsRepo.Upsert(
ctx, req.OrgID, a.GetName().String(), nil, config,
nil, nil, nil,
)
if err != nil {
return nil, err
}
agentVersion := "v0.0.1"
if connection.AgentConfig.Version != "" {
agentVersion = connection.AgentConfig.Version
}
// TODO: improve the command and set url
cliCommand := []string{"az", "stack", "sub", "create", "--name", "SigNozIntegration", "--location",
connection.AccountConfig.DeploymentRegion, "--template-uri", fmt.Sprintf("<url>%s", agentVersion),
"--action-on-unmanage", "deleteAll", "--deny-settings-mode", "denyDelete", "--parameters", fmt.Sprintf("rgName=%s", "signoz-integration-rg"),
fmt.Sprintf("rgLocation=%s", connection.AccountConfig.DeploymentRegion)}
return &integrationstypes.GettableAzureConnectionCommand{
AccountId: account.ID.String(),
AzureShellConnectionCommand: "az create",
AzureCliConnectionCommand: strings.Join(cliCommand, " "),
}, nil
}
func (a *azureProvider) UpdateServiceConfig(ctx context.Context, req *integrationstypes.PatchableServiceConfig) (any, error) {
definition, err := a.azureServiceDefinitions.GetServiceDefinition(ctx, req.ServiceId)
if err != nil {
return nil, err
}
serviceConfig := new(integrationstypes.PatchableAzureCloudServiceConfig)
err = serviceConfig.Unmarshal(req.Config)
if err != nil {
return nil, err
}
if err = serviceConfig.Config.Validate(definition); err != nil {
return nil, err
}
// can only update config for a connected cloud account id
_, err = a.accountsRepo.GetConnectedCloudAccount(
ctx, req.OrgID, a.GetName().String(), serviceConfig.CloudAccountId,
)
if err != nil {
return nil, err
}
serviceConfigBytes, err := serviceConfig.Config.Marshal()
if err != nil {
return nil, err
}
updatedConfig, err := a.serviceConfigRepo.Upsert(
ctx, req.OrgID, a.GetName().String(), serviceConfig.CloudAccountId, req.ServiceId, serviceConfigBytes,
)
if err != nil {
return nil, err
}
if err = serviceConfig.Unmarshal(updatedConfig); err != nil {
return nil, err
}
return &integrationstypes.PatchServiceConfigResponse{
ServiceId: req.ServiceId,
Config: serviceConfig,
}, nil
}
func (a *azureProvider) GetAvailableDashboards(ctx context.Context, orgID valuer.UUID) ([]*dashboardtypes.Dashboard, error) {
accountRecords, err := a.accountsRepo.ListConnected(ctx, orgID.StringValue(), a.GetName().String())
if err != nil {
return nil, err
}
// for now service dashboards are only available when metrics are enabled.
servicesWithAvailableMetrics := map[string]*time.Time{}
for _, ar := range accountRecords {
if ar.AccountID == nil {
continue
}
configsBySvcId, err := a.serviceConfigRepo.GetAllForAccount(ctx, orgID.StringValue(), ar.ID.StringValue())
if err != nil {
return nil, err
}
for svcId, config := range configsBySvcId {
serviceConfig := new(integrationstypes.AzureCloudServiceConfig)
err = serviceConfig.Unmarshal(config)
if err != nil {
return nil, err
}
if serviceConfig.Metrics != nil {
for _, metric := range serviceConfig.Metrics {
if metric.Enabled {
servicesWithAvailableMetrics[svcId] = &ar.CreatedAt
break
}
}
}
}
}
svcDashboards := make([]*dashboardtypes.Dashboard, 0)
allServices, err := a.azureServiceDefinitions.ListServiceDefinitions(ctx)
if err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to list azure service definitions")
}
for _, svc := range allServices {
serviceDashboardsCreatedAt := servicesWithAvailableMetrics[svc.Id]
if serviceDashboardsCreatedAt != nil {
svcDashboards = integrationstypes.GetDashboardsFromAssets(svc.Id, a.GetName(), serviceDashboardsCreatedAt, svc.Assets)
servicesWithAvailableMetrics[svc.Id] = nil
}
}
return svcDashboards, nil
}
func (a *azureProvider) GetDashboard(ctx context.Context, req *integrationstypes.GettableDashboard) (*dashboardtypes.Dashboard, error) {
allDashboards, err := a.GetAvailableDashboards(ctx, req.OrgID)
if err != nil {
return nil, err
}
for _, dashboard := range allDashboards {
if dashboard.ID == req.ID {
return dashboard, nil
}
}
return nil, errors.NewNotFoundf(CodeDashboardNotFound, "dashboard with id %s not found", req.ID)
}
func (a *azureProvider) UpdateAccountConfig(ctx context.Context, req *integrationstypes.PatchableAccountConfig) (any, error) {
config := new(integrationstypes.PatchableAzureAccountConfig)
err := config.Unmarshal(req.Data)
if err != nil {
return nil, err
}
if config.Config == nil && len(config.Config.EnabledResourceGroups) < 1 {
return nil, errors.NewInvalidInputf(CodeInvalidAzureRegion, "azure region and resource groups must be provided")
}
//for azure, preserve deployment region if already set
account, err := a.accountsRepo.Get(ctx, req.OrgID, a.GetName().String(), req.AccountId)
if err != nil {
return nil, err
}
storedConfig := new(integrationstypes.AzureAccountConfig)
err = storedConfig.Unmarshal(account.Config)
if err != nil {
return nil, err
}
if account.Config != nil {
config.Config.DeploymentRegion = storedConfig.DeploymentRegion
}
configBytes, err := config.Config.Marshal()
if err != nil {
return nil, err
}
accountRecord, err := a.accountsRepo.Upsert(
ctx, req.OrgID, a.GetName().String(), &req.AccountId, configBytes, nil, nil, nil,
)
if err != nil {
return nil, err
}
return accountRecord.Account(a.GetName()), nil
}
func (a *azureProvider) DisconnectAccount(ctx context.Context, orgID, accountID string) (*integrationstypes.CloudIntegration, error) {
account, err := a.accountsRepo.Get(ctx, orgID, a.GetName().String(), accountID)
if err != nil {
return nil, err
}
tsNow := time.Now()
account, err = a.accountsRepo.Upsert(
ctx, orgID, a.GetName().String(), &accountID, nil, nil, nil, &tsNow,
)
if err != nil {
return nil, err
}
return account, nil
}

View File

@@ -1,94 +1 @@
package cloudintegrations
import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations/services"
"github.com/SigNoz/signoz/pkg/types"
)
type ServiceSummary struct {
services.Metadata
Config *types.CloudServiceConfig `json:"config"`
}
type ServiceDetails struct {
services.Definition
Config *types.CloudServiceConfig `json:"config"`
ConnectionStatus *ServiceConnectionStatus `json:"status,omitempty"`
}
type AccountStatus struct {
Integration AccountIntegrationStatus `json:"integration"`
}
type AccountIntegrationStatus struct {
LastHeartbeatTsMillis *int64 `json:"last_heartbeat_ts_ms"`
}
type LogsConfig struct {
Enabled bool `json:"enabled"`
S3Buckets map[string][]string `json:"s3_buckets,omitempty"`
}
type MetricsConfig struct {
Enabled bool `json:"enabled"`
}
type ServiceConnectionStatus struct {
Logs *SignalConnectionStatus `json:"logs"`
Metrics *SignalConnectionStatus `json:"metrics"`
}
type SignalConnectionStatus struct {
LastReceivedTsMillis int64 `json:"last_received_ts_ms"` // epoch milliseconds
LastReceivedFrom string `json:"last_received_from"` // resource identifier
}
type CompiledCollectionStrategy = services.CollectionStrategy
func NewCompiledCollectionStrategy(provider string) (*CompiledCollectionStrategy, error) {
if provider == "aws" {
return &CompiledCollectionStrategy{
Provider: "aws",
AWSMetrics: &services.AWSMetricsStrategy{},
AWSLogs: &services.AWSLogsStrategy{},
}, nil
}
return nil, errors.NewNotFoundf(services.CodeUnsupportedCloudProvider, "unsupported cloud provider: %s", provider)
}
// Helper for accumulating strategies for enabled services.
func AddServiceStrategy(serviceType string, cs *CompiledCollectionStrategy,
definitionStrat *services.CollectionStrategy, config *types.CloudServiceConfig) error {
if definitionStrat.Provider != cs.Provider {
return errors.NewInternalf(CodeMismatchCloudProvider, "can't add %s service strategy to compiled strategy for %s",
definitionStrat.Provider, cs.Provider)
}
if cs.Provider == "aws" {
if config.Logs != nil && config.Logs.Enabled {
if serviceType == services.S3Sync {
// S3 bucket sync; No cloudwatch logs are appended for this service type;
// Though definition is populated with a custom cloudwatch group that helps in calculating logs connection status
cs.S3Buckets = config.Logs.S3Buckets
} else if definitionStrat.AWSLogs != nil { // services that includes a logs subscription
cs.AWSLogs.Subscriptions = append(
cs.AWSLogs.Subscriptions,
definitionStrat.AWSLogs.Subscriptions...,
)
}
}
if config.Metrics != nil && config.Metrics.Enabled && definitionStrat.AWSMetrics != nil {
cs.AWSMetrics.StreamFilters = append(
cs.AWSMetrics.StreamFilters,
definitionStrat.AWSMetrics.StreamFilters...,
)
}
return nil
}
return errors.NewNotFoundf(services.CodeUnsupportedCloudProvider, "unsupported cloud provider: %s", cs.Provider)
}

View File

@@ -0,0 +1,26 @@
package cloudintegrations
import (
"log/slog"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations/implawsprovider"
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations/implazureprovider"
integrationstore "github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations/store"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/integrationstypes"
)
func NewCloudProviderRegistry(logger *slog.Logger, store sqlstore.SQLStore, querier querier.Querier) map[integrationstypes.CloudProviderType]integrationstypes.CloudProvider {
registry := make(map[integrationstypes.CloudProviderType]integrationstypes.CloudProvider)
accountsRepo := integrationstore.NewCloudProviderAccountsRepository(store)
serviceConfigRepo := integrationstore.NewServiceConfigRepository(store)
awsProviderImpl := implawsprovider.NewAWSCloudProvider(logger, accountsRepo, serviceConfigRepo, querier)
registry[integrationstypes.CloudProviderAWS] = awsProviderImpl
azureProviderImpl := implazureprovider.NewAzureCloudProvider(logger, accountsRepo, serviceConfigRepo, querier)
registry[integrationstypes.CloudProviderAzure] = azureProviderImpl
return registry
}

View File

@@ -7,6 +7,24 @@
"metrics": true,
"logs": false
},
"ingestion_status_check": {
"metrics": [
{
"category": "$default",
"display_name": "Default",
"checks": [
{
"key": "aws_ApplicationELB_ConsumedLCUs_count",
"attributes": []
},
{
"key": "aws_ApplicationELB_ProcessedBytes_sum",
"attributes": []
}
]
}
]
},
"data_collected": {
"metrics": [
{

View File

@@ -7,6 +7,75 @@
"metrics": true,
"logs": true
},
"ingestion_status_check": {
"metrics": [
{
"category": "rest_api",
"display_name": "REST API Metrics",
"checks": [
{
"key": "aws_ApiGateway_Count_count",
"attributes": [
{
"name": "ApiName",
"operator": "EXISTS",
"value": ""
}
]
}
]
},
{
"category": "http_api",
"display_name": "HTTP API Metrics",
"checks": [
{
"key": "aws_ApiGateway_Count_count",
"attributes": [
{
"name": "ApiId",
"operator": "EXISTS",
"value": ""
}
]
}
]
},
{
"category": "websocket_api",
"display_name": "Websocket API Metrics",
"checks": [
{
"key": "aws_ApiGateway_Count_count",
"attributes": [
{
"name": "ApiId",
"operator": "EXISTS",
"value": ""
}
]
}
]
}
],
"logs": [
{
"category": "$default",
"display_name": "Default",
"checks": [
{
"attributes": [
{
"name": "aws.cloudwatch.log_group_name",
"operator": "ILIKE",
"value": "API-Gateway%"
}
]
}
]
}
]
},
"data_collected": {
"metrics": [
{
@@ -148,6 +217,146 @@
"name": "aws_ApiGateway_Latency_sum",
"unit": "Milliseconds",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_4xx_sum",
"unit": "Bytes",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_4xx_max",
"unit": "Bytes",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_4xx_min",
"unit": "Bytes",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_4xx_count",
"unit": "Bytes",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_5xx_sum",
"unit": "Bytes",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_5xx_max",
"unit": "Bytes",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_5xx_min",
"unit": "Bytes",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_5xx_count",
"unit": "Bytes",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_DataProcessed_sum",
"unit": "Bytes",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_DataProcessed_max",
"unit": "Bytes",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_DataProcessed_min",
"unit": "Bytes",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_DataProcessed_count",
"unit": "Bytes",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_ExecutionError_sum",
"unit": "Count",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_ExecutionError_max",
"unit": "Count",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_ExecutionError_min",
"unit": "Count",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_ExecutionError_count",
"unit": "Count",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_ClientError_sum",
"unit": "Count",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_ClientError_max",
"unit": "Count",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_ClientError_min",
"unit": "Count",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_ClientError_count",
"unit": "Count",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_IntegrationError_sum",
"unit": "Count",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_IntegrationError_max",
"unit": "Count",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_IntegrationError_min",
"unit": "Count",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_IntegrationError_count",
"unit": "Count",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_ConnectCount_sum",
"unit": "Count",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_ConnectCount_max",
"unit": "Count",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_ConnectCount_min",
"unit": "Count",
"type": "Gauge"
},
{
"name": "aws_ApiGateway_ConnectCount_count",
"unit": "Count",
"type": "Gauge"
}
],
"logs": [

View File

@@ -7,6 +7,24 @@
"metrics": true,
"logs": false
},
"ingestion_status_check": {
"metrics": [
{
"category": "$default",
"display_name": "Default",
"checks": [
{
"key": "aws_DynamoDB_AccountMaxReads_max",
"attributes": []
},
{
"key": "aws_DynamoDB_AccountProvisionedReadCapacityUtilization_max",
"attributes": []
}
]
}
]
},
"data_collected": {
"metrics": [
{
@@ -391,4 +409,4 @@
}
]
}
}
}

View File

@@ -7,6 +7,24 @@
"metrics": true,
"logs": false
},
"ingestion_status_check": {
"metrics": [
{
"category": "$default",
"display_name": "Default",
"checks": [
{
"key": "aws_EC2_CPUUtilization_max",
"attributes": []
},
{
"key": "aws_EC2_NetworkIn_max",
"attributes": []
}
]
}
]
},
"data_collected": {
"metrics": [
{
@@ -515,4 +533,4 @@
}
]
}
}
}

View File

@@ -7,6 +7,81 @@
"metrics": true,
"logs": true
},
"ingestion_status_check": {
"metrics": [
{
"category": "overview",
"display_name": "Overview",
"checks": [
{
"key": "aws_ECS_CPUUtilization_max",
"attributes": []
},
{
"key": "aws_ECS_MemoryUtilization_max",
"attributes": []
}
]
},
{
"category": "containerinsights",
"display_name": "Container Insights",
"checks": [
{
"key": "aws_ECS_ContainerInsights_NetworkRxBytes_max",
"attributes": []
},
{
"key": "aws_ECS_ContainerInsights_StorageReadBytes_max",
"attributes": []
}
]
},
{
"category": "enhanced_containerinsights",
"display_name": "Enhanced Container Insights",
"checks": [
{
"key": "aws_ECS_ContainerInsights_ContainerCpuUtilization_max",
"attributes": [
{
"name": "TaskId",
"operator": "EXISTS",
"value": ""
}
]
},
{
"key": "aws_ECS_ContainerInsights_TaskMemoryUtilization_max",
"attributes": [
{
"name": "TaskId",
"operator": "EXISTS",
"value": ""
}
]
}
]
}
],
"logs": [
{
"category": "$default",
"display_name": "Default",
"checks": [
{
"attributes": [
{
"name": "aws.cloudwatch.log_group_name",
"operator": "ILIKE",
"value": "%/ecs/%"
}
]
}
]
}
]
},
"data_collected": {
"metrics": [
{

View File

@@ -7,6 +7,20 @@
"metrics": true,
"logs": false
},
"ingestion_status_check": {
"metrics": [
{
"category": "$default",
"display_name": "Default",
"checks": [
{
"key": "aws_ElastiCache_CacheHitRate_max",
"attributes": []
}
]
}
]
},
"data_collected": {
"metrics":[
{
@@ -1928,7 +1942,7 @@
"unit": "Percent",
"type": "Gauge",
"description": ""
}
}
]
},
"telemetry_collection_strategy": {
@@ -1951,4 +1965,4 @@
}
]
}
}
}

View File

@@ -7,6 +7,37 @@
"metrics": true,
"logs": true
},
"ingestion_status_check": {
"metrics": [
{
"category": "$default",
"display_name": "Default",
"checks": [
{
"key": "aws_Lambda_Invocations_sum",
"attributes": []
}
]
}
],
"logs": [
{
"category": "$default",
"display_name": "Default",
"checks": [
{
"attributes": [
{
"name": "aws.cloudwatch.log_group_name",
"operator": "ILIKE",
"value": "/aws/lambda%"
}
]
}
]
}
]
},
"data_collected": {
"metrics": [
{

View File

@@ -7,6 +7,20 @@
"metrics": true,
"logs": false
},
"ingestion_status_check": {
"metrics": [
{
"category": "$default",
"display_name": "Default",
"checks": [
{
"key": "aws_Kafka_KafkaDataLogsDiskUsed_max",
"attributes": []
}
]
}
]
},
"data_collected": {
"metrics": [
{
@@ -1088,4 +1102,3 @@
]
}
}

View File

@@ -7,6 +7,37 @@
"metrics": true,
"logs": true
},
"ingestion_status_check": {
"metrics": [
{
"category": "$default",
"display_name": "Default",
"checks": [
{
"key": "aws_RDS_CPUUtilization_max",
"attributes": []
}
]
}
],
"logs": [
{
"category": "$default",
"display_name": "Default",
"checks": [
{
"attributes": [
{
"name": "resources.aws.cloudwatch.log_group_name",
"operator": "ILIKE",
"value": "/aws/rds%"
}
]
}
]
}
]
},
"data_collected": {
"metrics": [
{
@@ -800,4 +831,4 @@
}
]
}
}
}

View File

@@ -7,6 +7,20 @@
"metrics": true,
"logs": false
},
"ingestion_status_check": {
"metrics": [
{
"category": "$default",
"display_name": "Default",
"checks": [
{
"key": "aws_SNS_NumberOfMessagesPublished_sum",
"attributes": []
}
]
}
]
},
"data_collected": {
"metrics": [
{
@@ -127,4 +141,4 @@
}
]
}
}
}

View File

@@ -7,6 +7,24 @@
"metrics": true,
"logs": false
},
"ingestion_status_check": {
"metrics": [
{
"category": "$default",
"display_name": "Default",
"checks": [
{
"key": "aws_SQS_SentMessageSize_max",
"attributes": []
},
{
"key": "aws_SQS_NumberOfMessagesSent_sum",
"attributes": []
}
]
}
]
},
"data_collected": {
"metrics": [
{
@@ -247,4 +265,4 @@
}
]
}
}
}

View File

@@ -0,0 +1 @@
<svg id="f2f04349-8aee-4413-84c9-a9053611b319" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"><defs><linearGradient id="ad4c4f96-09aa-4f91-ba10-5cb8ad530f74" x1="9" y1="15.83" x2="9" y2="5.79" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#b3b3b3" /><stop offset="0.26" stop-color="#c1c1c1" /><stop offset="1" stop-color="#e6e6e6" /></linearGradient></defs><title>Icon-storage-86</title><path d="M.5,5.79h17a0,0,0,0,1,0,0v9.48a.57.57,0,0,1-.57.57H1.07a.57.57,0,0,1-.57-.57V5.79A0,0,0,0,1,.5,5.79Z" fill="url(#ad4c4f96-09aa-4f91-ba10-5cb8ad530f74)" /><path d="M1.07,2.17H16.93a.57.57,0,0,1,.57.57V5.79a0,0,0,0,1,0,0H.5a0,0,0,0,1,0,0V2.73A.57.57,0,0,1,1.07,2.17Z" fill="#37c2b1" /><path d="M2.81,6.89H15.18a.27.27,0,0,1,.26.27v1.4a.27.27,0,0,1-.26.27H2.81a.27.27,0,0,1-.26-.27V7.16A.27.27,0,0,1,2.81,6.89Z" fill="#fff" /><path d="M2.82,9.68H15.19a.27.27,0,0,1,.26.27v1.41a.27.27,0,0,1-.26.27H2.82a.27.27,0,0,1-.26-.27V10A.27.27,0,0,1,2.82,9.68Z" fill="#37c2b1" /><path d="M2.82,12.5H15.19a.27.27,0,0,1,.26.27v1.41a.27.27,0,0,1-.26.27H2.82a.27.27,0,0,1-.26-.27V12.77A.27.27,0,0,1,2.82,12.5Z" fill="#258277" /></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,293 @@
{
"id": "blobstorage",
"title": "Blob Storage",
"icon": "file://icon.svg",
"overview": "file://overview.md",
"supported_signals": {
"metrics": true,
"logs": true
},
"ingestion_status_check": {
"metrics": [
{
"category": "$default",
"display_name": "Default",
"checks": [
{
"key": "placeholder",
"attributes": []
}
]
},
{
"category": "transactions",
"display_name": "Transactions",
"checks": [
{
"key": "placeholder",
"attributes": []
}
]
}
],
"logs": [
{
"category": "$default",
"display_name": "Default",
"checks": [
{
"attributes": [
{
"name": "placeholder",
"operator": "ILIKE",
"value": "%/ecs/%"
}
]
}
]
}
]
},
"data_collected": {
"metrics": [
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "placeholder_metric_1",
"unit": "Percent",
"type": "Gauge",
"description": ""
}
],
"logs": [
{
"name": "placeholder_log_1",
"path": "placeholder.path.value",
"type": "string"
},
{
"name": "placeholder_log_1",
"path": "placeholder.path.value",
"type": "string"
},
{
"name": "placeholder_log_1",
"path": "placeholder.path.value",
"type": "string"
},
{
"name": "placeholder_log_1",
"path": "placeholder.path.value",
"type": "string"
}
]
},
"telemetry_collection_strategy": {
"azure_metrics": [
{
"category_type": "metrics",
"name": "Capacity"
},
{
"category_type": "metrics",
"name": "Transaction"
}
],
"azure_logs": [
{
"category_type": "logs",
"name": "StorageRead"
},
{
"category_type": "logs",
"name": "StorageWrite"
},
{
"category_type": "logs",
"name": "StorageDelete"
}
]
},
"assets": {
"dashboards": [
{
"id": "overview",
"title": "Blob Storage Overview",
"description": "Overview of Blob Storage",
"definition": "file://assets/dashboards/overview.json"
}
]
}
}

View File

@@ -0,0 +1,2 @@
Monitor Azure Blob Storage with SigNoz
Collect key Blob Storage metrics and view them with an out of the box dashboard.

View File

@@ -0,0 +1 @@
<svg id="f2f04349-8aee-4413-84c9-a9053611b319" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"><defs><linearGradient id="ad4c4f96-09aa-4f91-ba10-5cb8ad530f74" x1="9" y1="15.83" x2="9" y2="5.79" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#b3b3b3" /><stop offset="0.26" stop-color="#c1c1c1" /><stop offset="1" stop-color="#e6e6e6" /></linearGradient></defs><title>Icon-storage-86</title><path d="M.5,5.79h17a0,0,0,0,1,0,0v9.48a.57.57,0,0,1-.57.57H1.07a.57.57,0,0,1-.57-.57V5.79A0,0,0,0,1,.5,5.79Z" fill="url(#ad4c4f96-09aa-4f91-ba10-5cb8ad530f74)" /><path d="M1.07,2.17H16.93a.57.57,0,0,1,.57.57V5.79a0,0,0,0,1,0,0H.5a0,0,0,0,1,0,0V2.73A.57.57,0,0,1,1.07,2.17Z" fill="#37c2b1" /><path d="M2.81,6.89H15.18a.27.27,0,0,1,.26.27v1.4a.27.27,0,0,1-.26.27H2.81a.27.27,0,0,1-.26-.27V7.16A.27.27,0,0,1,2.81,6.89Z" fill="#fff" /><path d="M2.82,9.68H15.19a.27.27,0,0,1,.26.27v1.41a.27.27,0,0,1-.26.27H2.82a.27.27,0,0,1-.26-.27V10A.27.27,0,0,1,2.82,9.68Z" fill="#37c2b1" /><path d="M2.82,12.5H15.19a.27.27,0,0,1,.26.27v1.41a.27.27,0,0,1-.26.27H2.82a.27.27,0,0,1-.26-.27V12.77A.27.27,0,0,1,2.82,12.5Z" fill="#258277" /></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

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