Compare commits

...

49 Commits

Author SHA1 Message Date
SagarRajput-7
d6fd19b363 Merge branch 'variable-update-queue' into dashboard-variable-test-cases 2024-12-17 17:53:49 +05:30
SagarRajput-7
e50a773fa9 feat: added API limiting to reduce unnecessary api call for dashboard variables (#6609)
* feat: added API limiting to reduce unneccesary api call for dashboard variables

* feat: fixed dropdown open triggering the api calls for single-select and misc
2024-12-17 17:53:03 +05:30
SagarRajput-7
cb91fee7c3 Merge branch 'develop' into variable-update-queue 2024-12-17 17:52:11 +05:30
Vibhu Pandey
14fbb1fcda fix(flag): add missing flag back (#6647) 2024-12-17 01:12:31 +05:30
Vikrant Gupta
96da21df05 fix: correlation time stamps for APM to traces and logs (#6632) 2024-12-17 00:00:15 +05:30
Vikrant Gupta
8608f02263 fix: timerange selected for traces to logs (#6634) 2024-12-16 22:35:25 +05:30
Vikrant Gupta
2701ae5c34 fix: unable to remove query tags from the beginning when similar tags present (#6645)
* fix: unable to remove query tags from the begining

* fix: focus only when the filters dropdown is opened

* fix: used auto focus prop
2024-12-16 22:25:37 +05:30
Vibhu Pandey
951593b0a3 feat(licenses): deprecate licenses v2 (#6626)
deprecate licenses v2
2024-12-16 10:23:23 +00:00
Srikanth Chekuri
e6766023dd chore: use tag attributes v2 table (#6616) 2024-12-16 09:59:16 +00:00
Srikanth Chekuri
bef5b96c5c chore: add queries with tag attributes v1 (#6643) 2024-12-16 13:04:55 +05:30
Shaheer Kochai
b29359dee0 fix: improve traces table ux by making the table scrollable (#6423)
* fix: improve traces table ux by making the table scrollable

* chore: remove explicit scroll.x and modify the existing scroll.x from true to max-content

* fix(Traces Explorer): make trace explorer card full-width when collapsed, resizable when expanded
2024-12-16 07:00:40 +00:00
Amlan Kumar Nandy
9a1cd65b73 feat: add functionality to export dashboard as json from listing page 2024-12-16 12:10:21 +05:30
SagarRajput-7
351178ef34 Merge branch 'limiting-api-via-keys' into dashboard-variable-test-cases 2024-12-16 12:03:20 +05:30
SagarRajput-7
4b6e934510 Merge branch 'variable-update-queue' into limiting-api-via-keys 2024-12-16 12:03:14 +05:30
SagarRajput-7
99fb8c2a64 Merge branch 'develop' into variable-update-queue 2024-12-16 12:03:05 +05:30
Amlan Kumar Nandy
8ab0c066d6 Merge branch 'develop' into dashboard-list-export-json 2024-12-16 11:39:18 +05:30
Shaheer Kochai
b333aa3775 Feat: Timezone picker feature (#6474)
* feat: time picker hint and timezone picker UI with basic functionality + helper to get timezones

* feat: add support for esc keypress to close the timezone picker

* chore: add the selected timezone as url param and close timezone picker on select

* fix: overall improvement + add searchIndex to timezone

* feat: timezone preferences UI

* chore: improve timezone utils

* chore: change timezone item from div to button

* feat: display timezone in timepicker input

* chore: fix the typo

* fix: don't focus on time picker when timezone is clicked

* fix: fix the issue of timezone breaking for browser and utc timezones

* fix: display the timezone in timepicker hint 'You are at'

* feat: timezone basic functionality (#6492)

* chore: change div to fragment + change type to any as the ESLint complains otherwise

* chore: manage etc timezone filtering with an arg

* chore: update timezone wrapper class name

* fix: add timezone support to downloaded logs

* feat: add current timezone to dashboard list and configure metadata modal

* fix: add pencil icon next to timezone hint + change the copy to Current timezone

* fix: properly handle the escape button behavior for timezone picker

* chore: replace @vvo/tzdb with native Intl API for timezones

* feat: lightmode for timezone picker and timezone adaptation components

* fix: use normald tz in browser timezone

* fix: timezone picker lightmode fixes

* feat: display selected time range in 12 hour format

* chore: remove unnecessary optional chaining

* fix: fix the typo in css variable

* chore: add em dash and change icon for timezone hint in date/time picker

* chore: move pen line icon to the right of timezone offset

* fix: fix the failing tests

* feat: handle switching off the timezone adaptation
2024-12-16 11:27:20 +05:30
amlannandy
8a3319cdf5 chore: address comments 2024-12-16 11:24:57 +05:30
amlannandy
d09c4d947e feat: update unit test 2024-12-16 11:24:57 +05:30
amlannandy
2508e6f9f1 feat: update unit test 2024-12-16 11:24:57 +05:30
amlannandy
1b8213653a feat: update tests 2024-12-16 11:24:57 +05:30
amlannandy
b499b10333 feat: add unit test 2024-12-16 11:24:57 +05:30
amlannandy
b35b975798 chore: address comments 2024-12-16 11:24:57 +05:30
amlannandy
715f8a2363 feat: address comments 2024-12-16 11:24:57 +05:30
amlannandy
8d1c4491b7 feat: add functionality to export dashboard as json from listing page 2024-12-16 11:24:57 +05:30
dependabot[bot]
e3caa6a8f5 chore(deps): bump golang.org/x/crypto from 0.27.0 to 0.31.0 (#6638)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.27.0 to 0.31.0.
- [Commits](https://github.com/golang/crypto/compare/v0.27.0...v0.31.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-14 04:29:25 +00:00
Vibhu Pandey
a1059ed949 feat(signoz-ingestion-key): rename signoz-access-token to signoz-ingestion-key (#6633) 2024-12-13 18:18:12 +05:30
SagarRajput-7
8c46de8eac feat: added table column and row logic for the new api response structure for producer overview (#6433)
* feat: added table column and row logic for the new api response structure for prodcure overview

* feat: fixed typo in function name

* feat: consumed new 2 table merging logic for producer latency overview

* feat: added 3 digit precision to 'ingestion_rate' and 'byte_rate'in consumer overview
2024-12-13 11:25:39 +05:30
SagarRajput-7
ae98aaad2d feat: added more test on graph utilities 2024-12-12 16:52:50 +05:30
SagarRajput-7
23d808af08 feat: refactor code 2024-12-12 11:13:49 +05:30
SagarRajput-7
c991ee6239 feat: added test for checkAPIInvocation 2024-12-12 11:04:55 +05:30
SagarRajput-7
f098518faa feat: add jest test cases for new logic's utils, functions and processors - dashboardVariables 2024-12-12 09:53:47 +05:30
Nityananda Gohain
0fbfb6b22b fix: fix count aggregate attribute column name (#6613)
* fix: fix count aggregate attribute column name

* fix: add test for column as well

* fix: use new schema in threshold rule enrichment
2024-12-11 13:37:25 +05:30
SagarRajput-7
421d355e29 feat: fixed dropdown open triggering the api calls for single-select and misc 2024-12-11 13:18:56 +05:30
Nityananda Gohain
b25df66381 fix: migration for ingestion dashboard (#6610)
* fix: migration for ingestion dashboard

* fix: remove redundant uuid

* Update pkg/query-service/app/dashboards/model.go

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2024-12-10 18:49:21 +05:30
Nityananda Gohain
32fa5a403c fix: update default alert ch queries for traces and logs (#6552)
* fix: update default alert ch queries for traces and logs

* fix: use service.name instead of peer.service

* fix: add link to docs

* fix: add alias for service name
2024-12-10 18:36:02 +05:30
Nityananda Gohain
f9d4cf19e9 fix: smartTraceAlgo for new schema (#6602) 2024-12-10 13:43:51 +05:30
Amlan Kumar Nandy
81775c7d55 fix: resolve unnecessary refetching of graphs on service details page 2024-12-10 12:00:55 +05:30
amlannandy
8d2666004b fix: resolve the unneccessary refetching of graphs on service details page 2024-12-10 11:39:26 +05:30
SagarRajput-7
eb75e636e8 feat: added API limiting to reduce unneccesary api call for dashboard variables 2024-12-10 11:20:34 +05:30
SagarRajput-7
f121240c82 Merge branch 'develop' into variable-update-queue 2024-12-10 11:18:28 +05:30
Shivanshu Raj Shrivastava
51baf7f8d3 feat: add byte rate to producer API (#6579)
* fix: fix partition check for topic throughput
2024-12-09 15:18:41 +00:00
Amlan Kumar Nandy
31a2926375 chore: replace the 'Get started' CTA with 'New source' 2024-12-09 14:34:18 +05:30
amlannandy
8c6225185d chore: replace the 'Get started' CTA with 'New source' 2024-12-09 14:00:41 +05:30
deepakdinesh1123
d4458d65ad fix(docker): otel-collector-migrator command fixed in docker-compose-core.yaml (#6564)
Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2024-12-07 20:15:07 +05:30
Nityananda Gohain
02d8fdb212 fix: enable smart trace detail (#6596) 2024-12-05 09:30:39 +05:30
Prashant Shahi
a383c708e3 Merge pull request #6591 from SigNoz/release/v0.61.x
Release/v0.61.x
2024-12-04 20:49:18 +05:30
SagarRajput-7
a60dbf7f89 Merge branch 'develop' into variable-update-queue 2024-12-04 10:46:17 +05:30
SagarRajput-7
fa4aeae508 feat: updated the logic for variable update queue 2024-12-04 10:19:33 +05:30
409 changed files with 6062 additions and 1345 deletions

View File

@@ -72,7 +72,9 @@ services:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.14}
container_name: otel-migrator
command:
- "sync"
- "--dsn=tcp://clickhouse:9000"
- "--up="
depends_on:
clickhouse:
condition: service_healthy

View File

@@ -41,7 +41,6 @@ type APIHandlerOptions struct {
FluxInterval time.Duration
UseLogsNewSchema bool
UseTraceNewSchema bool
UseLicensesV3 bool
}
type APIHandler struct {
@@ -68,7 +67,6 @@ func NewAPIHandler(opts APIHandlerOptions) (*APIHandler, error) {
FluxInterval: opts.FluxInterval,
UseLogsNewSchema: opts.UseLogsNewSchema,
UseTraceNewSchema: opts.UseTraceNewSchema,
UseLicensesV3: opts.UseLicensesV3,
})
if err != nil {

View File

@@ -84,13 +84,6 @@ func (ah *APIHandler) listLicenses(w http.ResponseWriter, r *http.Request) {
}
func (ah *APIHandler) applyLicense(w http.ResponseWriter, r *http.Request) {
if ah.UseLicensesV3 {
// if the licenses v3 is toggled on then do not apply license in v2 and run the validator!
// TODO: remove after migration to v3 and deprecation from zeus
zap.L().Info("early return from apply license v2 call")
render.Success(w, http.StatusOK, nil)
return
}
var l model.License
if err := json.NewDecoder(r.Body).Decode(&l); err != nil {
@@ -102,7 +95,7 @@ func (ah *APIHandler) applyLicense(w http.ResponseWriter, r *http.Request) {
RespondError(w, model.BadRequest(fmt.Errorf("license key is required")), nil)
return
}
license, apiError := ah.LM().Activate(r.Context(), l.Key)
license, apiError := ah.LM().ActivateV3(r.Context(), l.Key)
if apiError != nil {
RespondError(w, apiError, nil)
return
@@ -265,24 +258,12 @@ func convertLicenseV3ToLicenseV2(licenses []*model.LicenseV3) []model.License {
}
func (ah *APIHandler) listLicensesV2(w http.ResponseWriter, r *http.Request) {
var licenses []model.License
if ah.UseLicensesV3 {
licensesV3, err := ah.LM().GetLicensesV3(r.Context())
if err != nil {
RespondError(w, err, nil)
return
}
licenses = convertLicenseV3ToLicenseV2(licensesV3)
} else {
_licenses, apiError := ah.LM().GetLicenses(r.Context())
if apiError != nil {
RespondError(w, apiError, nil)
return
}
licenses = _licenses
licensesV3, apierr := ah.LM().GetLicensesV3(r.Context())
if apierr != nil {
RespondError(w, apierr, nil)
return
}
licenses := convertLicenseV3ToLicenseV2(licensesV3)
resp := model.Licenses{
TrialStart: -1,

View File

@@ -2,31 +2,32 @@ package api
import (
"net/http"
"go.signoz.io/signoz/ee/query-service/app/db"
"go.signoz.io/signoz/ee/query-service/model"
baseapp "go.signoz.io/signoz/pkg/query-service/app"
basemodel "go.signoz.io/signoz/pkg/query-service/model"
"go.uber.org/zap"
)
func (ah *APIHandler) searchTraces(w http.ResponseWriter, r *http.Request) {
ah.APIHandler.SearchTraces(w, r)
return
if !ah.CheckFeature(basemodel.SmartTraceDetail) {
zap.L().Info("SmartTraceDetail feature is not enabled in this plan")
ah.APIHandler.SearchTraces(w, r)
return
}
searchTracesParams, err := baseapp.ParseSearchTracesParams(r)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, "Error reading params")
return
}
// This is commented since this will be taken care by new trace API
result, err := ah.opts.DataConnector.SearchTraces(r.Context(), searchTracesParams, db.SmartTraceAlgorithm)
if ah.HandleError(w, err, http.StatusBadRequest) {
return
}
// if !ah.CheckFeature(basemodel.SmartTraceDetail) {
// zap.L().Info("SmartTraceDetail feature is not enabled in this plan")
// ah.APIHandler.SearchTraces(w, r)
// return
// }
// searchTracesParams, err := baseapp.ParseSearchTracesParams(r)
// if err != nil {
// RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, "Error reading params")
// return
// }
// result, err := ah.opts.DataConnector.SearchTraces(r.Context(), searchTracesParams, db.SmartTraceAlgorithm)
// if ah.HandleError(w, err, http.StatusBadRequest) {
// return
// }
// ah.WriteJSON(w, r, result)
ah.WriteJSON(w, r, result)
}

View File

@@ -78,7 +78,6 @@ type ServerOptions struct {
GatewayUrl string
UseLogsNewSchema bool
UseTraceNewSchema bool
UseLicensesV3 bool
}
// Server runs HTTP api service
@@ -135,7 +134,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
}
// initiate license manager
lm, err := licensepkg.StartManager("sqlite", localDB, serverOptions.UseLicensesV3)
lm, err := licensepkg.StartManager("sqlite", localDB)
if err != nil {
return nil, err
}
@@ -274,7 +273,6 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
Gateway: gatewayProxy,
UseLogsNewSchema: serverOptions.UseLogsNewSchema,
UseTraceNewSchema: serverOptions.UseTraceNewSchema,
UseLicensesV3: serverOptions.UseLicensesV3,
}
apiHandler, err := api.NewAPIHandler(apiOpts)

View File

@@ -2,18 +2,6 @@ package signozio
type status string
type ActivationResult struct {
Status status `json:"status"`
Data *ActivationResponse `json:"data,omitempty"`
ErrorType string `json:"errorType,omitempty"`
Error string `json:"error,omitempty"`
}
type ActivationResponse struct {
ActivationId string `json:"ActivationId"`
PlanDetails string `json:"PlanDetails"`
}
type ValidateLicenseResponse struct {
Status status `json:"status"`
Data map[string]interface{} `json:"data"`

View File

@@ -10,7 +10,6 @@ import (
"time"
"github.com/pkg/errors"
"go.uber.org/zap"
"go.signoz.io/signoz/ee/query-service/constants"
"go.signoz.io/signoz/ee/query-service/model"
@@ -39,86 +38,6 @@ func init() {
C = New()
}
// ActivateLicense sends key to license.signoz.io and gets activation data
func ActivateLicense(key, siteId string) (*ActivationResponse, *model.ApiError) {
licenseReq := map[string]string{
"key": key,
"siteId": siteId,
}
reqString, _ := json.Marshal(licenseReq)
httpResponse, err := http.Post(C.Prefix+"/licenses/activate", APPLICATION_JSON, bytes.NewBuffer(reqString))
if err != nil {
zap.L().Error("failed to connect to license.signoz.io", zap.Error(err))
return nil, model.BadRequest(fmt.Errorf("unable to connect with license.signoz.io, please check your network connection"))
}
httpBody, err := io.ReadAll(httpResponse.Body)
if err != nil {
zap.L().Error("failed to read activation response from license.signoz.io", zap.Error(err))
return nil, model.BadRequest(fmt.Errorf("failed to read activation response from license.signoz.io"))
}
defer httpResponse.Body.Close()
// read api request result
result := ActivationResult{}
err = json.Unmarshal(httpBody, &result)
if err != nil {
zap.L().Error("failed to marshal activation response from license.signoz.io", zap.Error(err))
return nil, model.InternalError(errors.Wrap(err, "failed to marshal license activation response"))
}
switch httpResponse.StatusCode {
case 200, 201:
return result.Data, nil
case 400, 401:
return nil, model.BadRequest(fmt.Errorf(fmt.Sprintf("failed to activate: %s", result.Error)))
default:
return nil, model.InternalError(fmt.Errorf(fmt.Sprintf("failed to activate: %s", result.Error)))
}
}
// ValidateLicense validates the license key
func ValidateLicense(activationId string) (*ActivationResponse, *model.ApiError) {
validReq := map[string]string{
"activationId": activationId,
}
reqString, _ := json.Marshal(validReq)
response, err := http.Post(C.Prefix+"/licenses/validate", APPLICATION_JSON, bytes.NewBuffer(reqString))
if err != nil {
return nil, model.BadRequest(errors.Wrap(err, "unable to connect with license.signoz.io, please check your network connection"))
}
body, err := io.ReadAll(response.Body)
if err != nil {
return nil, model.BadRequest(errors.Wrap(err, "failed to read validation response from license.signoz.io"))
}
defer response.Body.Close()
switch response.StatusCode {
case 200, 201:
a := ActivationResult{}
err = json.Unmarshal(body, &a)
if err != nil {
return nil, model.BadRequest(errors.Wrap(err, "failed to marshal license validation response"))
}
return a.Data, nil
case 400, 401:
return nil, model.BadRequest(errors.Wrap(fmt.Errorf(string(body)),
"bad request error received from license.signoz.io"))
default:
return nil, model.InternalError(errors.Wrap(fmt.Errorf(string(body)),
"internal error received from license.signoz.io"))
}
}
func ValidateLicenseV3(licenseKey string) (*model.LicenseV3, *model.ApiError) {
// Creating an HTTP client with a timeout for better control

View File

@@ -18,15 +18,13 @@ import (
// Repo is license repo. stores license keys in a secured DB
type Repo struct {
db *sqlx.DB
useLicensesV3 bool
db *sqlx.DB
}
// NewLicenseRepo initiates a new license repo
func NewLicenseRepo(db *sqlx.DB, useLicensesV3 bool) Repo {
func NewLicenseRepo(db *sqlx.DB) Repo {
return Repo{
db: db,
useLicensesV3: useLicensesV3,
db: db,
}
}
@@ -112,26 +110,16 @@ func (r *Repo) GetActiveLicenseV2(ctx context.Context) (*model.License, *basemod
// GetActiveLicense fetches the latest active license from DB.
// If the license is not present, expect a nil license and a nil error in the output.
func (r *Repo) GetActiveLicense(ctx context.Context) (*model.License, *basemodel.ApiError) {
if r.useLicensesV3 {
zap.L().Info("Using licenses v3 for GetActiveLicense")
activeLicenseV3, err := r.GetActiveLicenseV3(ctx)
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf("failed to get active licenses from db: %v", err))
}
if activeLicenseV3 == nil {
return nil, nil
}
activeLicenseV2 := model.ConvertLicenseV3ToLicenseV2(activeLicenseV3)
return activeLicenseV2, nil
}
active, err := r.GetActiveLicenseV2(ctx)
activeLicenseV3, err := r.GetActiveLicenseV3(ctx)
if err != nil {
return nil, err
return nil, basemodel.InternalError(fmt.Errorf("failed to get active licenses from db: %v", err))
}
return active, nil
if activeLicenseV3 == nil {
return nil, nil
}
activeLicenseV2 := model.ConvertLicenseV3ToLicenseV2(activeLicenseV3)
return activeLicenseV2, nil
}
func (r *Repo) GetActiveLicenseV3(ctx context.Context) (*model.LicenseV3, error) {

View File

@@ -51,12 +51,12 @@ type Manager struct {
activeFeatures basemodel.FeatureSet
}
func StartManager(dbType string, db *sqlx.DB, useLicensesV3 bool, features ...basemodel.Feature) (*Manager, error) {
func StartManager(dbType string, db *sqlx.DB, features ...basemodel.Feature) (*Manager, error) {
if LM != nil {
return LM, nil
}
repo := NewLicenseRepo(db, useLicensesV3)
repo := NewLicenseRepo(db)
err := repo.InitDB(dbType)
if err != nil {
@@ -67,32 +67,7 @@ func StartManager(dbType string, db *sqlx.DB, useLicensesV3 bool, features ...ba
repo: &repo,
}
if useLicensesV3 {
// get active license from the db
active, err := m.repo.GetActiveLicenseV2(context.Background())
if err != nil {
return m, err
}
// if we have an active license then need to fetch the complete details
if active != nil {
// fetch the new license structure from control plane
licenseV3, apiError := validate.ValidateLicenseV3(active.Key)
if apiError != nil {
return m, apiError
}
// insert the licenseV3 in sqlite db
apiError = m.repo.InsertLicenseV3(context.Background(), licenseV3)
// if the license already exists move ahead.
if apiError != nil && apiError.Typ != model.ErrorConflict {
return m, apiError
}
zap.L().Info("Successfully inserted license from v2 to v3 table")
}
}
if err := m.start(useLicensesV3, features...); err != nil {
if err := m.start(features...); err != nil {
return m, err
}
LM = m
@@ -100,16 +75,8 @@ func StartManager(dbType string, db *sqlx.DB, useLicensesV3 bool, features ...ba
}
// start loads active license in memory and initiates validator
func (lm *Manager) start(useLicensesV3 bool, features ...basemodel.Feature) error {
var err error
if useLicensesV3 {
err = lm.LoadActiveLicenseV3(features...)
} else {
err = lm.LoadActiveLicense(features...)
}
return err
func (lm *Manager) start(features ...basemodel.Feature) error {
return lm.LoadActiveLicenseV3(features...)
}
func (lm *Manager) Stop() {
@@ -117,31 +84,6 @@ func (lm *Manager) Stop() {
<-lm.terminated
}
func (lm *Manager) SetActive(l *model.License, features ...basemodel.Feature) {
lm.mutex.Lock()
defer lm.mutex.Unlock()
if l == nil {
return
}
lm.activeLicense = l
lm.activeFeatures = append(l.FeatureSet, features...)
// set default features
setDefaultFeatures(lm)
err := lm.InitFeatures(lm.activeFeatures)
if err != nil {
zap.L().Panic("Couldn't activate features", zap.Error(err))
}
if !lm.validatorRunning {
// we want to make sure only one validator runs,
// we already have lock() so good to go
lm.validatorRunning = true
go lm.Validator(context.Background())
}
}
func (lm *Manager) SetActiveV3(l *model.LicenseV3, features ...basemodel.Feature) {
lm.mutex.Lock()
defer lm.mutex.Unlock()
@@ -172,29 +114,6 @@ func setDefaultFeatures(lm *Manager) {
lm.activeFeatures = append(lm.activeFeatures, baseconstants.DEFAULT_FEATURE_SET...)
}
// LoadActiveLicense loads the most recent active license
func (lm *Manager) LoadActiveLicense(features ...basemodel.Feature) error {
active, err := lm.repo.GetActiveLicense(context.Background())
if err != nil {
return err
}
if active != nil {
lm.SetActive(active, features...)
} else {
zap.L().Info("No active license found, defaulting to basic plan")
// if no active license is found, we default to basic(free) plan with all default features
lm.activeFeatures = model.BasicPlan
setDefaultFeatures(lm)
err := lm.InitFeatures(lm.activeFeatures)
if err != nil {
zap.L().Error("Couldn't initialize features", zap.Error(err))
return err
}
}
return nil
}
func (lm *Manager) LoadActiveLicenseV3(features ...basemodel.Feature) error {
active, err := lm.repo.GetActiveLicenseV3(context.Background())
if err != nil {
@@ -265,31 +184,6 @@ func (lm *Manager) GetLicensesV3(ctx context.Context) (response []*model.License
return response, nil
}
// Validator validates license after an epoch of time
func (lm *Manager) Validator(ctx context.Context) {
zap.L().Info("Validator started!")
defer close(lm.terminated)
tick := time.NewTicker(validationFrequency)
defer tick.Stop()
lm.Validate(ctx)
for {
select {
case <-lm.done:
return
default:
select {
case <-lm.done:
return
case <-tick.C:
lm.Validate(ctx)
}
}
}
}
// Validator validates license after an epoch of time
func (lm *Manager) ValidatorV3(ctx context.Context) {
zap.L().Info("ValidatorV3 started!")
@@ -315,73 +209,6 @@ func (lm *Manager) ValidatorV3(ctx context.Context) {
}
}
// Validate validates the current active license
func (lm *Manager) Validate(ctx context.Context) (reterr error) {
zap.L().Info("License validation started")
if lm.activeLicense == nil {
return nil
}
defer func() {
lm.mutex.Lock()
lm.lastValidated = time.Now().Unix()
if reterr != nil {
zap.L().Error("License validation completed with error", zap.Error(reterr))
atomic.AddUint64(&lm.failedAttempts, 1)
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_LICENSE_CHECK_FAILED,
map[string]interface{}{"err": reterr.Error()}, "", true, false)
} else {
zap.L().Info("License validation completed with no errors")
}
lm.mutex.Unlock()
}()
response, apiError := validate.ValidateLicense(lm.activeLicense.ActivationId)
if apiError != nil {
zap.L().Error("failed to validate license", zap.Error(apiError.Err))
return apiError.Err
}
if response.PlanDetails == lm.activeLicense.PlanDetails {
// license plan hasnt changed, nothing to do
return nil
}
if response.PlanDetails != "" {
// copy and replace the active license record
l := model.License{
Key: lm.activeLicense.Key,
CreatedAt: lm.activeLicense.CreatedAt,
PlanDetails: response.PlanDetails,
ValidationMessage: lm.activeLicense.ValidationMessage,
ActivationId: lm.activeLicense.ActivationId,
}
if err := l.ParsePlan(); err != nil {
zap.L().Error("failed to parse updated license", zap.Error(err))
return err
}
// updated plan is parsable, check if plan has changed
if lm.activeLicense.PlanDetails != response.PlanDetails {
err := lm.repo.UpdatePlanDetails(ctx, lm.activeLicense.Key, response.PlanDetails)
if err != nil {
// unexpected db write issue but we can let the user continue
// and wait for update to work in next cycle.
zap.L().Error("failed to validate license", zap.Error(err))
}
}
// activate the update license plan
lm.SetActive(&l)
}
return nil
}
func (lm *Manager) RefreshLicense(ctx context.Context) *model.ApiError {
license, apiError := validate.ValidateLicenseV3(lm.activeLicenseV3.Key)
@@ -429,50 +256,6 @@ func (lm *Manager) ValidateV3(ctx context.Context) (reterr error) {
return nil
}
// Activate activates a license key with signoz server
func (lm *Manager) Activate(ctx context.Context, key string) (licenseResponse *model.License, errResponse *model.ApiError) {
defer func() {
if errResponse != nil {
userEmail, err := auth.GetEmailFromJwt(ctx)
if err == nil {
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_LICENSE_ACT_FAILED,
map[string]interface{}{"err": errResponse.Err.Error()}, userEmail, true, false)
}
}
}()
response, apiError := validate.ActivateLicense(key, "")
if apiError != nil {
zap.L().Error("failed to activate license", zap.Error(apiError.Err))
return nil, apiError
}
l := &model.License{
Key: key,
ActivationId: response.ActivationId,
PlanDetails: response.PlanDetails,
}
// parse validity and features from the plan details
err := l.ParsePlan()
if err != nil {
zap.L().Error("failed to activate license", zap.Error(err))
return nil, model.InternalError(err)
}
// store the license before activating it
err = lm.repo.InsertLicense(ctx, l)
if err != nil {
zap.L().Error("failed to activate license", zap.Error(err))
return nil, model.InternalError(err)
}
// license is valid, activate it
lm.SetActive(l)
return l, nil
}
func (lm *Manager) ActivateV3(ctx context.Context, licenseKey string) (licenseResponse *model.LicenseV3, errResponse *model.ApiError) {
defer func() {
if errResponse != nil {

View File

@@ -95,7 +95,6 @@ func main() {
var useLogsNewSchema bool
var useTraceNewSchema bool
var useLicensesV3 bool
var cacheConfigPath, fluxInterval string
var enableQueryServiceLogOTLPExport bool
var preferSpanMetrics bool
@@ -104,10 +103,10 @@ func main() {
var maxOpenConns int
var dialTimeout time.Duration
var gatewayUrl string
var useLicensesV3 bool
flag.BoolVar(&useLogsNewSchema, "use-logs-new-schema", false, "use logs_v2 schema for logs")
flag.BoolVar(&useTraceNewSchema, "use-trace-new-schema", false, "use new schema for traces")
flag.BoolVar(&useLicensesV3, "use-licenses-v3", false, "use licenses_v3 schema for licenses")
flag.StringVar(&promConfigPath, "config", "./config/prometheus.yml", "(prometheus config to read metrics)")
flag.StringVar(&skipTopLvlOpsPath, "skip-top-level-ops", "", "(config file to skip top level operations)")
flag.BoolVar(&disableRules, "rules.disable", false, "(disable rule evaluation)")
@@ -121,6 +120,7 @@ func main() {
flag.BoolVar(&enableQueryServiceLogOTLPExport, "enable.query.service.log.otlp.export", false, "(enable query service log otlp export)")
flag.StringVar(&cluster, "cluster", "cluster", "(cluster name - defaults to 'cluster')")
flag.StringVar(&gatewayUrl, "gateway-url", "", "(url to the gateway)")
flag.BoolVar(&useLicensesV3, "use-licenses-v3", false, "use licenses_v3 schema for licenses")
flag.Parse()
@@ -148,7 +148,6 @@ func main() {
GatewayUrl: gatewayUrl,
UseLogsNewSchema: useLogsNewSchema,
UseTraceNewSchema: useTraceNewSchema,
UseLicensesV3: useLicensesV3,
}
// Read the jwt secret key

View File

@@ -1,10 +1,9 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
MessagingQueueServicePayload,
MessagingQueuesPayloadProps,
} from './getConsumerLagDetails';
} from 'pages/MessagingQueues/MQDetails/MQTables/getConsumerLagDetails';
import { ErrorResponse, SuccessResponse } from 'types/api';
export const getTopicThroughputOverview = async (
props: Omit<MessagingQueueServicePayload, 'variables'>,

View File

@@ -40,7 +40,7 @@
&.custom-time {
input:not(:focus) {
min-width: 240px;
min-width: 280px;
}
}
@@ -119,3 +119,69 @@
color: var(--bg-slate-400) !important;
}
}
.date-time-popover__footer {
border-top: 1px solid var(--bg-ink-200);
padding: 8px 14px;
.timezone-container {
&,
.timezone {
font-family: Inter;
font-size: 12px;
line-height: 16px;
letter-spacing: -0.06px;
}
display: flex;
align-items: center;
color: var(--bg-vanilla-400);
gap: 6px;
.timezone {
display: flex;
align-items: center;
gap: 4px;
border-radius: 2px;
background: rgba(171, 189, 255, 0.04);
cursor: pointer;
padding: 0px 4px;
color: var(--bg-vanilla-100);
border: none;
}
}
}
.timezone-badge {
display: flex;
align-items: center;
justify-content: center;
padding: 0 4px;
border-radius: 2px;
background: rgba(171, 189, 255, 0.04);
color: var(--bg-vanilla-100);
font-size: 12px;
font-weight: 400;
line-height: 16px;
letter-spacing: -0.06px;
cursor: pointer;
}
.lightMode {
.date-time-popover__footer {
border-color: var(--bg-vanilla-400);
}
.timezone-container {
color: var(--bg-ink-400);
&__clock-icon {
stroke: var(--bg-ink-400);
}
.timezone {
color: var(--bg-ink-100);
background: rgb(179 179 179 / 15%);
&__icon {
stroke: var(--bg-ink-100);
}
}
}
.timezone-badge {
color: var(--bg-ink-100);
background: rgb(179 179 179 / 15%);
}
}

View File

@@ -15,11 +15,14 @@ import { isValidTimeFormat } from 'lib/getMinMax';
import { defaultTo, isFunction, noop } from 'lodash-es';
import debounce from 'lodash-es/debounce';
import { CheckCircle, ChevronDown, Clock } from 'lucide-react';
import { useTimezone } from 'providers/Timezone';
import {
ChangeEvent,
Dispatch,
SetStateAction,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { useLocation } from 'react-router-dom';
@@ -28,6 +31,8 @@ import { popupContainer } from 'utils/selectPopupContainer';
import CustomTimePickerPopoverContent from './CustomTimePickerPopoverContent';
const maxAllowedMinTimeInMonths = 6;
type ViewType = 'datetime' | 'timezone';
const DEFAULT_VIEW: ViewType = 'datetime';
interface CustomTimePickerProps {
onSelect: (value: string) => void;
@@ -81,11 +86,42 @@ function CustomTimePicker({
const location = useLocation();
const [isInputFocused, setIsInputFocused] = useState(false);
const [activeView, setActiveView] = useState<ViewType>(DEFAULT_VIEW);
const { timezone, browserTimezone } = useTimezone();
const activeTimezoneOffset = timezone.offset;
const isTimezoneOverridden = useMemo(
() => timezone.offset !== browserTimezone.offset,
[timezone, browserTimezone],
);
const handleViewChange = useCallback(
(newView: 'timezone' | 'datetime'): void => {
if (activeView !== newView) {
setActiveView(newView);
}
setOpen(true);
},
[activeView, setOpen],
);
const [isOpenedFromFooter, setIsOpenedFromFooter] = useState(false);
const getSelectedTimeRangeLabel = (
selectedTime: string,
selectedTimeValue: string,
): string => {
if (selectedTime === 'custom') {
// Convert the date range string to 12-hour format
const dates = selectedTimeValue.split(' - ');
if (dates.length === 2) {
const startDate = dayjs(dates[0], 'DD/MM/YYYY HH:mm');
const endDate = dayjs(dates[1], 'DD/MM/YYYY HH:mm');
return `${startDate.format('DD/MM/YYYY hh:mm A')} - ${endDate.format(
'DD/MM/YYYY hh:mm A',
)}`;
}
return selectedTimeValue;
}
@@ -131,6 +167,7 @@ function CustomTimePicker({
setOpen(newOpen);
if (!newOpen) {
setCustomDTPickerVisible?.(false);
setActiveView('datetime');
}
};
@@ -244,6 +281,7 @@ function CustomTimePicker({
const handleFocus = (): void => {
setIsInputFocused(true);
setActiveView('datetime');
};
const handleBlur = (): void => {
@@ -280,6 +318,10 @@ function CustomTimePicker({
handleGoLive={defaultTo(handleGoLive, noop)}
options={items}
selectedTime={selectedTime}
activeView={activeView}
setActiveView={setActiveView}
setIsOpenedFromFooter={setIsOpenedFromFooter}
isOpenedFromFooter={isOpenedFromFooter}
/>
) : (
content
@@ -316,12 +358,24 @@ function CustomTimePicker({
)
}
suffix={
<ChevronDown
size={14}
onClick={(): void => {
setOpen(!open);
}}
/>
<>
{!!isTimezoneOverridden && activeTimezoneOffset && (
<div
className="timezone-badge"
onClick={(e): void => {
e.stopPropagation();
handleViewChange('timezone');
setIsOpenedFromFooter(false);
}}
>
<span>{activeTimezoneOffset}</span>
</div>
)}
<ChevronDown
size={14}
onClick={(): void => handleViewChange('datetime')}
/>
</>
}
/>
</Popover>

View File

@@ -1,5 +1,6 @@
import './CustomTimePicker.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Button } from 'antd';
import cx from 'classnames';
import ROUTES from 'constants/routes';
@@ -9,10 +10,13 @@ import {
Option,
RelativeDurationSuggestionOptions,
} from 'container/TopNav/DateTimeSelectionV2/config';
import { Clock, PenLine } from 'lucide-react';
import { useTimezone } from 'providers/Timezone';
import { Dispatch, SetStateAction, useMemo } from 'react';
import { useLocation } from 'react-router-dom';
import RangePickerModal from './RangePickerModal';
import TimezonePicker from './TimezonePicker';
interface CustomTimePickerPopoverContentProps {
options: any[];
@@ -26,8 +30,13 @@ interface CustomTimePickerPopoverContentProps {
onSelectHandler: (label: string, value: string) => void;
handleGoLive: () => void;
selectedTime: string;
activeView: 'datetime' | 'timezone';
setActiveView: Dispatch<SetStateAction<'datetime' | 'timezone'>>;
isOpenedFromFooter: boolean;
setIsOpenedFromFooter: Dispatch<SetStateAction<boolean>>;
}
// eslint-disable-next-line sonarjs/cognitive-complexity
function CustomTimePickerPopoverContent({
options,
setIsOpen,
@@ -37,12 +46,18 @@ function CustomTimePickerPopoverContent({
onSelectHandler,
handleGoLive,
selectedTime,
activeView,
setActiveView,
isOpenedFromFooter,
setIsOpenedFromFooter,
}: CustomTimePickerPopoverContentProps): JSX.Element {
const { pathname } = useLocation();
const isLogsExplorerPage = useMemo(() => pathname === ROUTES.LOGS_EXPLORER, [
pathname,
]);
const { timezone } = useTimezone();
const activeTimezoneOffset = timezone.offset;
function getTimeChips(options: Option[]): JSX.Element {
return (
@@ -63,55 +78,99 @@ function CustomTimePickerPopoverContent({
);
}
const handleTimezoneHintClick = (): void => {
setActiveView('timezone');
setIsOpenedFromFooter(true);
};
if (activeView === 'timezone') {
return (
<div className="date-time-popover">
<TimezonePicker
setActiveView={setActiveView}
setIsOpen={setIsOpen}
isOpenedFromFooter={isOpenedFromFooter}
/>
</div>
);
}
return (
<div className="date-time-popover">
<div className="date-time-options">
{isLogsExplorerPage && (
<Button className="data-time-live" type="text" onClick={handleGoLive}>
Live
</Button>
)}
{options.map((option) => (
<Button
type="text"
key={option.label + option.value}
onClick={(): void => {
onSelectHandler(option.label, option.value);
}}
className={cx(
'date-time-options-btn',
customDateTimeVisible
? option.value === 'custom' && 'active'
: selectedTime === option.value && 'active',
)}
>
{option.label}
</Button>
))}
<>
<div className="date-time-popover">
<div className="date-time-options">
{isLogsExplorerPage && (
<Button className="data-time-live" type="text" onClick={handleGoLive}>
Live
</Button>
)}
{options.map((option) => (
<Button
type="text"
key={option.label + option.value}
onClick={(): void => {
onSelectHandler(option.label, option.value);
}}
className={cx(
'date-time-options-btn',
customDateTimeVisible
? option.value === 'custom' && 'active'
: selectedTime === option.value && 'active',
)}
>
{option.label}
</Button>
))}
</div>
<div
className={cx(
'relative-date-time',
selectedTime === 'custom' || customDateTimeVisible
? 'date-picker'
: 'relative-times',
)}
>
{selectedTime === 'custom' || customDateTimeVisible ? (
<RangePickerModal
setCustomDTPickerVisible={setCustomDTPickerVisible}
setIsOpen={setIsOpen}
onCustomDateHandler={onCustomDateHandler}
selectedTime={selectedTime}
/>
) : (
<div className="relative-times-container">
<div className="time-heading">RELATIVE TIMES</div>
<div>{getTimeChips(RelativeDurationSuggestionOptions)}</div>
</div>
)}
</div>
</div>
<div
className={cx(
'relative-date-time',
selectedTime === 'custom' || customDateTimeVisible
? 'date-picker'
: 'relative-times',
)}
>
{selectedTime === 'custom' || customDateTimeVisible ? (
<RangePickerModal
setCustomDTPickerVisible={setCustomDTPickerVisible}
setIsOpen={setIsOpen}
onCustomDateHandler={onCustomDateHandler}
selectedTime={selectedTime}
<div className="date-time-popover__footer">
<div className="timezone-container">
<Clock
color={Color.BG_VANILLA_400}
className="timezone-container__clock-icon"
height={12}
width={12}
/>
) : (
<div className="relative-times-container">
<div className="time-heading">RELATIVE TIMES</div>
<div>{getTimeChips(RelativeDurationSuggestionOptions)}</div>
</div>
)}
<span className="timezone__icon">Current timezone</span>
<div></div>
<button
type="button"
className="timezone"
onClick={handleTimezoneHintClick}
>
<span>{activeTimezoneOffset}</span>
<PenLine
color={Color.BG_VANILLA_100}
className="timezone__icon"
size={10}
/>
</button>
</div>
</div>
</div>
</>
);
}

View File

@@ -3,7 +3,8 @@ import './RangePickerModal.styles.scss';
import { DatePicker } from 'antd';
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
import { LexicalContext } from 'container/TopNav/DateTimeSelectionV2/config';
import dayjs, { Dayjs } from 'dayjs';
import dayjs from 'dayjs';
import { useTimezone } from 'providers/Timezone';
import { Dispatch, SetStateAction } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
@@ -31,7 +32,10 @@ function RangePickerModal(props: RangePickerModalProps): JSX.Element {
(state) => state.globalTime,
);
const disabledDate = (current: Dayjs): boolean => {
// Using any type here because antd's DatePicker expects its own internal Dayjs type
// which conflicts with our project's Dayjs type that has additional plugins (tz, utc etc).
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
const disabledDate = (current: any): boolean => {
const currentDay = dayjs(current);
return currentDay.isAfter(dayjs());
};
@@ -49,16 +53,22 @@ function RangePickerModal(props: RangePickerModalProps): JSX.Element {
}
onCustomDateHandler(date_time, LexicalContext.CUSTOM_DATE_PICKER);
};
const { timezone } = useTimezone();
return (
<div className="custom-date-picker">
<RangePicker
disabledDate={disabledDate}
allowClear
showTime
format="YYYY-MM-DD hh:mm A"
onOk={onModalOkHandler}
// eslint-disable-next-line react/jsx-props-no-spreading
{...(selectedTime === 'custom' && {
defaultValue: [dayjs(minTime / 1000000), dayjs(maxTime / 1000000)],
defaultValue: [
dayjs(minTime / 1000000).tz(timezone.value),
dayjs(maxTime / 1000000).tz(timezone.value),
],
})}
/>
</div>

View File

@@ -0,0 +1,166 @@
// Variables
$font-family: 'Inter';
$item-spacing: 8px;
:root {
--border-color: var(--bg-slate-400);
}
.lightMode {
--border-color: var(--bg-vanilla-400);
}
// Mixins
@mixin text-style-base {
font-family: $font-family;
font-style: normal;
font-weight: 400;
}
@mixin flex-center {
display: flex;
align-items: center;
}
.timezone-picker {
width: 532px;
color: var(--bg-vanilla-400);
font-family: $font-family;
&__search {
@include flex-center;
justify-content: space-between;
padding: 12px 14px;
border-bottom: 1px solid var(--border-color);
}
&__input-container {
@include flex-center;
gap: 6px;
width: -webkit-fill-available;
}
&__input {
@include text-style-base;
width: 100%;
background: transparent;
border: none;
outline: none;
color: var(--bg-vanilla-100);
font-size: 14px;
line-height: 20px;
letter-spacing: -0.07px;
padding: 0;
&.ant-input:focus {
box-shadow: none;
}
&::placeholder {
color: var(--bg-vanilla-400);
}
}
&__esc-key {
@include text-style-base;
font-size: 8px;
color: var(--bg-vanilla-400);
letter-spacing: -0.04px;
border-radius: 2.286px;
border: 1.143px solid var(--bg-ink-200);
border-bottom-width: 2.286px;
background: var(--bg-ink-400);
padding: 0 1px;
}
&__list {
max-height: 310px;
overflow-y: auto;
}
&__item {
@include flex-center;
justify-content: space-between;
padding: 7.5px 6px 7.5px $item-spacing;
margin: 4px $item-spacing;
cursor: pointer;
background: transparent;
border: none;
width: -webkit-fill-available;
color: var(--bg-vanilla-400);
font-family: $font-family;
&:hover,
&.selected {
border-radius: 2px;
background: rgba(171, 189, 255, 0.04);
color: var(--bg-vanilla-100);
}
&.has-divider {
position: relative;
&::after {
content: '';
position: absolute;
bottom: -2px;
left: -$item-spacing;
right: -$item-spacing;
border-bottom: 1px solid var(--border-color);
}
}
}
&__name {
@include text-style-base;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.07px;
}
&__offset {
color: var(--bg-vanilla-100);
font-size: 12px;
line-height: 16px;
letter-spacing: -0.06px;
}
}
.timezone-name-wrapper {
@include flex-center;
gap: 6px;
&__selected-icon {
height: 15px;
width: 15px;
}
}
.lightMode {
.timezone-picker {
&__search {
.search-icon {
stroke: var(--bg-ink-400);
}
}
&__input {
color: var(--bg-ink-100);
}
&__esc-key {
background-color: var(--bg-vanilla-100);
border-color: var(--bg-vanilla-400);
color: var(--bg-ink-400);
}
&__item {
color: var(--bg-ink-400);
}
&__offset {
color: var(--bg-ink-100);
}
}
.timezone-name-wrapper {
&__selected-icon {
.check-icon {
stroke: var(--bg-ink-100);
}
}
}
}

View File

@@ -0,0 +1,201 @@
import './TimezonePicker.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Input } from 'antd';
import cx from 'classnames';
import { TimezonePickerShortcuts } from 'constants/shortcuts/TimezonePickerShortcuts';
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
import { Check, Search } from 'lucide-react';
import { useTimezone } from 'providers/Timezone';
import {
Dispatch,
SetStateAction,
useCallback,
useEffect,
useState,
} from 'react';
import { Timezone, TIMEZONE_DATA } from './timezoneUtils';
interface SearchBarProps {
value: string;
onChange: (value: string) => void;
setIsOpen: Dispatch<SetStateAction<boolean>>;
setActiveView: Dispatch<SetStateAction<'datetime' | 'timezone'>>;
isOpenedFromFooter: boolean;
}
interface TimezoneItemProps {
timezone: Timezone;
isSelected?: boolean;
onClick?: () => void;
}
const ICON_SIZE = 14;
function SearchBar({
value,
onChange,
setIsOpen,
setActiveView,
isOpenedFromFooter = false,
}: SearchBarProps): JSX.Element {
const handleKeyDown = useCallback(
(e: React.KeyboardEvent): void => {
if (e.key === 'Escape') {
if (isOpenedFromFooter) {
setActiveView('datetime');
} else {
setIsOpen(false);
}
}
},
[setActiveView, setIsOpen, isOpenedFromFooter],
);
return (
<div className="timezone-picker__search">
<div className="timezone-picker__input-container">
<Search
color={Color.BG_VANILLA_400}
className="search-icon"
height={ICON_SIZE}
width={ICON_SIZE}
/>
<Input
type="text"
className="timezone-picker__input"
placeholder="Search timezones..."
value={value}
onChange={(e): void => onChange(e.target.value)}
onKeyDown={handleKeyDown}
tabIndex={0}
autoFocus
/>
</div>
<kbd className="timezone-picker__esc-key">esc</kbd>
</div>
);
}
function TimezoneItem({
timezone,
isSelected = false,
onClick,
}: TimezoneItemProps): JSX.Element {
return (
<button
type="button"
className={cx('timezone-picker__item', {
selected: isSelected,
'has-divider': timezone.hasDivider,
})}
onClick={onClick}
>
<div className="timezone-name-wrapper">
<div className="timezone-name-wrapper__selected-icon">
{isSelected && (
<Check
className="check-icon"
color={Color.BG_VANILLA_100}
height={ICON_SIZE}
width={ICON_SIZE}
/>
)}
</div>
<div className="timezone-picker__name">{timezone.name}</div>
</div>
<div className="timezone-picker__offset">{timezone.offset}</div>
</button>
);
}
TimezoneItem.defaultProps = {
isSelected: false,
onClick: undefined,
};
interface TimezonePickerProps {
setActiveView: Dispatch<SetStateAction<'datetime' | 'timezone'>>;
setIsOpen: Dispatch<SetStateAction<boolean>>;
isOpenedFromFooter: boolean;
}
function TimezonePicker({
setActiveView,
setIsOpen,
isOpenedFromFooter,
}: TimezonePickerProps): JSX.Element {
console.log({ isOpenedFromFooter });
const [searchTerm, setSearchTerm] = useState('');
const { timezone, updateTimezone } = useTimezone();
const [selectedTimezone, setSelectedTimezone] = useState<string>(
timezone.name ?? TIMEZONE_DATA[0].name,
);
const getFilteredTimezones = useCallback((searchTerm: string): Timezone[] => {
const normalizedSearch = searchTerm.toLowerCase();
return TIMEZONE_DATA.filter(
(tz) =>
tz.name.toLowerCase().includes(normalizedSearch) ||
tz.offset.toLowerCase().includes(normalizedSearch) ||
tz.searchIndex.toLowerCase().includes(normalizedSearch),
);
}, []);
const handleCloseTimezonePicker = useCallback(() => {
if (isOpenedFromFooter) {
setActiveView('datetime');
} else {
setIsOpen(false);
}
}, [isOpenedFromFooter, setActiveView, setIsOpen]);
const handleTimezoneSelect = useCallback(
(timezone: Timezone) => {
setSelectedTimezone(timezone.name);
updateTimezone(timezone);
handleCloseTimezonePicker();
setIsOpen(false);
},
[handleCloseTimezonePicker, setIsOpen, updateTimezone],
);
// Register keyboard shortcuts
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
useEffect(() => {
registerShortcut(
TimezonePickerShortcuts.CloseTimezonePicker,
handleCloseTimezonePicker,
);
return (): void => {
deregisterShortcut(TimezonePickerShortcuts.CloseTimezonePicker);
};
}, [deregisterShortcut, handleCloseTimezonePicker, registerShortcut]);
return (
<div className="timezone-picker">
<SearchBar
value={searchTerm}
onChange={setSearchTerm}
setIsOpen={setIsOpen}
setActiveView={setActiveView}
isOpenedFromFooter={isOpenedFromFooter}
/>
<div className="timezone-picker__list">
{getFilteredTimezones(searchTerm).map((timezone) => (
<TimezoneItem
key={timezone.value}
timezone={timezone}
isSelected={timezone.name === selectedTimezone}
onClick={(): void => handleTimezoneSelect(timezone)}
/>
))}
</div>
</div>
);
}
export default TimezonePicker;

View File

@@ -0,0 +1,152 @@
import dayjs from 'dayjs';
import timezone from 'dayjs/plugin/timezone';
import utc from 'dayjs/plugin/utc';
dayjs.extend(utc);
dayjs.extend(timezone);
export interface Timezone {
name: string;
value: string;
offset: string;
searchIndex: string;
hasDivider?: boolean;
}
const TIMEZONE_TYPES = {
BROWSER: 'BROWSER',
UTC: 'UTC',
STANDARD: 'STANDARD',
} as const;
type TimezoneType = typeof TIMEZONE_TYPES[keyof typeof TIMEZONE_TYPES];
export const UTC_TIMEZONE: Timezone = {
name: 'Coordinated Universal Time — UTC, GMT',
value: 'UTC',
offset: 'UTC',
searchIndex: 'UTC',
hasDivider: true,
};
const normalizeTimezoneName = (timezone: string): string => {
// https://github.com/tc39/proposal-temporal/issues/1076
if (timezone === 'Asia/Calcutta') {
return 'Asia/Kolkata';
}
return timezone;
};
const formatOffset = (offsetMinutes: number): string => {
if (offsetMinutes === 0) return 'UTC';
const hours = Math.floor(Math.abs(offsetMinutes) / 60);
const minutes = Math.abs(offsetMinutes) % 60;
const sign = offsetMinutes > 0 ? '+' : '-';
return `UTC ${sign} ${hours}${
minutes ? `:${minutes.toString().padStart(2, '0')}` : ':00'
}`;
};
const createTimezoneEntry = (
name: string,
offsetMinutes: number,
type: TimezoneType = TIMEZONE_TYPES.STANDARD,
hasDivider = false,
): Timezone => {
const offset = formatOffset(offsetMinutes);
let value = name;
let displayName = name;
switch (type) {
case TIMEZONE_TYPES.BROWSER:
displayName = `Browser time — ${name}`;
value = name;
break;
case TIMEZONE_TYPES.UTC:
displayName = 'Coordinated Universal Time — UTC, GMT';
value = 'UTC';
break;
case TIMEZONE_TYPES.STANDARD:
displayName = name;
value = name;
break;
default:
console.error(`Invalid timezone type: ${type}`);
}
return {
name: displayName,
value,
offset,
searchIndex: offset.replace(/ /g, ''),
...(hasDivider && { hasDivider }),
};
};
const getOffsetByTimezone = (timezone: string): number => {
const dayjsTimezone = dayjs().tz(timezone);
return dayjsTimezone.utcOffset();
};
export const getBrowserTimezone = (): Timezone => {
const browserTz = dayjs.tz.guess();
const normalizedTz = normalizeTimezoneName(browserTz);
const browserOffset = getOffsetByTimezone(normalizedTz);
return createTimezoneEntry(
normalizedTz,
browserOffset,
TIMEZONE_TYPES.BROWSER,
);
};
const filterAndSortTimezones = (
allTimezones: string[],
browserTzName?: string,
includeEtcTimezones = false,
): Timezone[] =>
allTimezones
.filter((tz) => {
const isNotBrowserTz = tz !== browserTzName;
const isNotEtcTz = includeEtcTimezones || !tz.startsWith('Etc/');
return isNotBrowserTz && isNotEtcTz;
})
.sort((a, b) => a.localeCompare(b))
.map((tz) => {
const normalizedTz = normalizeTimezoneName(tz);
const offset = getOffsetByTimezone(normalizedTz);
return createTimezoneEntry(normalizedTz, offset);
});
const generateTimezoneData = (includeEtcTimezones = false): Timezone[] => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const allTimezones = (Intl as any).supportedValuesOf('timeZone');
const timezones: Timezone[] = [];
// Add browser timezone
const browserTzObject = getBrowserTimezone();
timezones.push(browserTzObject);
// Add UTC timezone with divider
timezones.push(UTC_TIMEZONE);
timezones.push(
...filterAndSortTimezones(
allTimezones,
browserTzObject.value,
includeEtcTimezones,
),
);
return timezones;
};
export const getTimezoneObjectByTimezoneString = (
timezone: string,
): Timezone => {
const utcOffset = getOffsetByTimezone(timezone);
return createTimezoneEntry(timezone, utcOffset);
};
export const TIMEZONE_DATA = generateTimezoneData();

View File

@@ -1,4 +1,5 @@
import {
_adapters,
BarController,
BarElement,
CategoryScale,
@@ -18,8 +19,10 @@ import {
} from 'chart.js';
import annotationPlugin from 'chartjs-plugin-annotation';
import { generateGridTitle } from 'container/GridPanelSwitch/utils';
import dayjs from 'dayjs';
import { useIsDarkMode } from 'hooks/useDarkMode';
import isEqual from 'lodash-es/isEqual';
import { useTimezone } from 'providers/Timezone';
import {
forwardRef,
memo,
@@ -62,6 +65,17 @@ Chart.register(
Tooltip.positioners.custom = TooltipPositionHandler;
// Map of Chart.js time formats to dayjs format strings
const formatMap = {
'HH:mm:ss': 'HH:mm:ss',
'HH:mm': 'HH:mm',
'MM/DD HH:mm': 'MM/DD HH:mm',
'MM/dd HH:mm': 'MM/DD HH:mm',
'MM/DD': 'MM/DD',
'YY-MM': 'YY-MM',
YY: 'YY',
};
const Graph = forwardRef<ToggleGraphProps | undefined, GraphProps>(
(
{
@@ -80,11 +94,13 @@ const Graph = forwardRef<ToggleGraphProps | undefined, GraphProps>(
dragSelectColor,
},
ref,
// eslint-disable-next-line sonarjs/cognitive-complexity
): JSX.Element => {
const nearestDatasetIndex = useRef<null | number>(null);
const chartRef = useRef<HTMLCanvasElement>(null);
const isDarkMode = useIsDarkMode();
const gridTitle = useMemo(() => generateGridTitle(title), [title]);
const { timezone } = useTimezone();
const currentTheme = isDarkMode ? 'dark' : 'light';
const xAxisTimeUnit = useXAxisTimeUnit(data); // Computes the relevant time unit for x axis by analyzing the time stamp data
@@ -112,6 +128,22 @@ const Graph = forwardRef<ToggleGraphProps | undefined, GraphProps>(
return 'rgba(231,233,237,0.8)';
}, [currentTheme]);
// Override Chart.js date adapter to use dayjs with timezone support
useEffect(() => {
_adapters._date.override({
format(time: number | Date, fmt: string) {
const dayjsTime = dayjs(time).tz(timezone.value);
const format = formatMap[fmt as keyof typeof formatMap];
if (!format) {
console.warn(`Missing datetime format for ${fmt}`);
return dayjsTime.format('YYYY-MM-DD HH:mm:ss'); // fallback format
}
return dayjsTime.format(format);
},
});
}, [timezone]);
const buildChart = useCallback(() => {
if (lineChartRef.current !== undefined) {
lineChartRef.current.destroy();
@@ -132,6 +164,7 @@ const Graph = forwardRef<ToggleGraphProps | undefined, GraphProps>(
isStacked,
onClickHandler,
data,
timezone,
);
const chartHasData = hasData(data);
@@ -166,6 +199,7 @@ const Graph = forwardRef<ToggleGraphProps | undefined, GraphProps>(
isStacked,
onClickHandler,
data,
timezone,
name,
type,
]);

View File

@@ -1,5 +1,6 @@
import { Chart, ChartConfiguration, ChartData, Color } from 'chart.js';
import * as chartjsAdapter from 'chartjs-adapter-date-fns';
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
import dayjs from 'dayjs';
import { MutableRefObject } from 'react';
@@ -50,6 +51,7 @@ export const getGraphOptions = (
isStacked: boolean | undefined,
onClickHandler: GraphOnClickHandler | undefined,
data: ChartData,
timezone: Timezone,
// eslint-disable-next-line sonarjs/cognitive-complexity
): CustomChartOptions => ({
animation: {
@@ -97,7 +99,7 @@ export const getGraphOptions = (
callbacks: {
title(context): string | string[] {
const date = dayjs(context[0].parsed.x);
return date.format('MMM DD, YYYY, HH:mm:ss');
return date.tz(timezone.value).format('MMM DD, YYYY, HH:mm:ss');
},
label(context): string | string[] {
let label = context.dataset.label || '';

View File

@@ -8,13 +8,13 @@ import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import { unescapeString } from 'container/LogDetailedView/utils';
import { FontSize } from 'container/OptionsMenu/types';
import dayjs from 'dayjs';
import dompurify from 'dompurify';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useIsDarkMode } from 'hooks/useDarkMode';
// utils
import { FlatLogData } from 'lib/logs/flatLogData';
import { useTimezone } from 'providers/Timezone';
import { useCallback, useMemo, useState } from 'react';
// interfaces
import { IField } from 'types/api/logs/fields';
@@ -174,12 +174,20 @@ function ListLogView({
[selectedFields],
);
const { formatTimezoneAdjustedTimestamp } = useTimezone();
const timestampValue = useMemo(
() =>
typeof flattenLogData.timestamp === 'string'
? dayjs(flattenLogData.timestamp).format('YYYY-MM-DD HH:mm:ss.SSS')
: dayjs(flattenLogData.timestamp / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS'),
[flattenLogData.timestamp],
? formatTimezoneAdjustedTimestamp(
flattenLogData.timestamp,
'YYYY-MM-DD HH:mm:ss.SSS',
)
: formatTimezoneAdjustedTimestamp(
flattenLogData.timestamp / 1e6,
'YYYY-MM-DD HH:mm:ss.SSS',
),
[flattenLogData.timestamp, formatTimezoneAdjustedTimestamp],
);
const logType = getLogIndicatorType(logData);

View File

@@ -6,7 +6,6 @@ import LogDetail from 'components/LogDetail';
import { VIEW_TYPES, VIEWS } from 'components/LogDetail/constants';
import { unescapeString } from 'container/LogDetailedView/utils';
import LogsExplorerContext from 'container/LogsExplorerContext';
import dayjs from 'dayjs';
import dompurify from 'dompurify';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
@@ -14,6 +13,7 @@ import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { FlatLogData } from 'lib/logs/flatLogData';
import { isEmpty, isNumber, isUndefined } from 'lodash-es';
import { useTimezone } from 'providers/Timezone';
import {
KeyboardEvent,
MouseEvent,
@@ -89,16 +89,24 @@ function RawLogView({
attributesText += ' | ';
}
const { formatTimezoneAdjustedTimestamp } = useTimezone();
const text = useMemo(() => {
const date =
typeof data.timestamp === 'string'
? dayjs(data.timestamp)
: dayjs(data.timestamp / 1e6);
? formatTimezoneAdjustedTimestamp(data.timestamp, 'YYYY-MM-DD HH:mm:ss.SSS')
: formatTimezoneAdjustedTimestamp(
data.timestamp / 1e6,
'YYYY-MM-DD HH:mm:ss.SSS',
);
return `${date.format('YYYY-MM-DD HH:mm:ss.SSS')} | ${attributesText} ${
data.body
}`;
}, [data.timestamp, data.body, attributesText]);
return `${date} | ${attributesText} ${data.body}`;
}, [
data.timestamp,
data.body,
attributesText,
formatTimezoneAdjustedTimestamp,
]);
const handleClickExpand = useCallback(() => {
if (activeContextLog || isReadOnly) return;

View File

@@ -5,10 +5,10 @@ import { Typography } from 'antd';
import { ColumnsType } from 'antd/es/table';
import cx from 'classnames';
import { unescapeString } from 'container/LogDetailedView/utils';
import dayjs from 'dayjs';
import dompurify from 'dompurify';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { FlatLogData } from 'lib/logs/flatLogData';
import { useTimezone } from 'providers/Timezone';
import { useMemo } from 'react';
import { FORBID_DOM_PURIFY_TAGS } from 'utils/app';
@@ -44,6 +44,8 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
logs,
]);
const { formatTimezoneAdjustedTimestamp } = useTimezone();
const columns: ColumnsType<Record<string, unknown>> = useMemo(() => {
const fieldColumns: ColumnsType<Record<string, unknown>> = fields
.filter((e) => e.name !== 'id')
@@ -81,8 +83,11 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
render: (field, item): ColumnTypeRender<Record<string, unknown>> => {
const date =
typeof field === 'string'
? dayjs(field).format('YYYY-MM-DD HH:mm:ss.SSS')
: dayjs(field / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS');
? formatTimezoneAdjustedTimestamp(field, 'YYYY-MM-DD HH:mm:ss.SSS')
: formatTimezoneAdjustedTimestamp(
field / 1e6,
'YYYY-MM-DD HH:mm:ss.SSS',
);
return {
children: (
<div className="table-timestamp">
@@ -125,7 +130,15 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
},
...(appendTo === 'end' ? fieldColumns : []),
];
}, [fields, isListViewPanel, appendTo, isDarkMode, linesPerRow, fontSize]);
}, [
fields,
isListViewPanel,
appendTo,
isDarkMode,
linesPerRow,
fontSize,
formatTimezoneAdjustedTimestamp,
]);
return { columns, dataSource: flattenLogData };
};

View File

@@ -1,11 +1,13 @@
import { Typography } from 'antd';
import convertDateToAmAndPm from 'lib/convertDateToAmAndPm';
import getFormattedDate from 'lib/getFormatedDate';
import { useTimezone } from 'providers/Timezone';
function Time({ CreatedOrUpdateTime }: DateProps): JSX.Element {
const { formatTimezoneAdjustedTimestamp } = useTimezone();
const time = new Date(CreatedOrUpdateTime);
const date = getFormattedDate(time);
const timeString = `${date} ${convertDateToAmAndPm(time)}`;
const timeString = formatTimezoneAdjustedTimestamp(
time,
'MM/DD/YYYY hh:mm:ss A (UTC Z)',
);
return <Typography>{timeString}</Typography>;
}

View File

@@ -21,4 +21,5 @@ export enum LOCALSTORAGE {
THEME_ANALYTICS_V1 = 'THEME_ANALYTICS_V1',
LAST_USED_SAVED_VIEWS = 'LAST_USED_SAVED_VIEWS',
SHOW_LOGS_QUICK_FILTERS = 'SHOW_LOGS_QUICK_FILTERS',
PREFERRED_TIMEZONE = 'PREFERRED_TIMEZONE',
}

View File

@@ -0,0 +1,3 @@
export const TimezonePickerShortcuts = {
CloseTimezonePicker: 'escape',
};

View File

@@ -7,6 +7,7 @@ import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import heatmapPlugin from 'lib/uPlotLib/plugins/heatmapPlugin';
import timelinePlugin from 'lib/uPlotLib/plugins/timelinePlugin';
import { useTimezone } from 'providers/Timezone';
import { useMemo, useRef } from 'react';
import { useDispatch } from 'react-redux';
import { UpdateTimeInterval } from 'store/actions';
@@ -48,6 +49,7 @@ function HorizontalTimelineGraph({
const urlQuery = useUrlQuery();
const dispatch = useDispatch();
const { timezone } = useTimezone();
const options: uPlot.Options = useMemo(
() => ({
@@ -116,8 +118,18 @@ function HorizontalTimelineGraph({
}),
]
: [],
tzDate: (timestamp: number): Date =>
uPlot.tzDate(new Date(timestamp * 1e3), timezone.value),
}),
[width, isDarkMode, transformedData.length, urlQuery, dispatch],
[
width,
isDarkMode,
transformedData.length,
urlQuery,
dispatch,
timezone.value,
],
);
return <Uplot data={transformedData} options={options} />;
}

View File

@@ -7,6 +7,7 @@ import {
useGetAlertRuleDetailsTimelineTable,
useTimelineTable,
} from 'pages/AlertDetails/hooks';
import { useTimezone } from 'providers/Timezone';
import { HTMLAttributes, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { AlertRuleTimelineTableResponse } from 'types/api/alerts/def';
@@ -41,6 +42,8 @@ function TimelineTable(): JSX.Element {
const { t } = useTranslation('common');
const { formatTimezoneAdjustedTimestamp } = useTimezone();
if (isError || !isValidRuleId || !ruleId) {
return <div>{t('something_went_wrong')}</div>;
}
@@ -64,6 +67,7 @@ function TimelineTable(): JSX.Element {
filters,
labels: labels ?? {},
setFilters,
formatTimezoneAdjustedTimestamp,
})}
onRow={handleRowClick}
dataSource={timelineData}

View File

@@ -8,6 +8,7 @@ import ClientSideQBSearch, {
import { ConditionalAlertPopover } from 'container/AlertHistory/AlertPopover/AlertPopover';
import { transformKeyValuesToAttributeValuesMap } from 'container/QueryBuilder/filters/utils';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { TimestampInput } from 'hooks/useTimezoneFormatter/useTimezoneFormatter';
import { Search } from 'lucide-react';
import AlertLabels, {
AlertLabelsProps,
@@ -16,7 +17,6 @@ import AlertState from 'pages/AlertDetails/AlertHeader/AlertState/AlertState';
import { useMemo } from 'react';
import { AlertRuleTimelineTableResponse } from 'types/api/alerts/def';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { formatEpochTimestamp } from 'utils/timeUtils';
const transformLabelsToQbKeys = (
labels: AlertRuleTimelineTableResponse['labels'],
@@ -74,10 +74,15 @@ export const timelineTableColumns = ({
filters,
labels,
setFilters,
formatTimezoneAdjustedTimestamp,
}: {
filters: TagFilter;
labels: AlertLabelsProps['labels'];
setFilters: (filters: TagFilter) => void;
formatTimezoneAdjustedTimestamp: (
input: TimestampInput,
format?: string,
) => string;
}): ColumnsType<AlertRuleTimelineTableResponse> => [
{
title: 'STATE',
@@ -106,7 +111,9 @@ export const timelineTableColumns = ({
dataIndex: 'unixMilli',
width: 200,
render: (value): JSX.Element => (
<div className="alert-rule__created-at">{formatEpochTimestamp(value)}</div>
<div className="alert-rule__created-at">
{formatTimezoneAdjustedTimestamp(value, 'MMM D, YYYY ⎯ HH:mm:ss')}
</div>
),
},
{

View File

@@ -17,14 +17,15 @@ import getAll from 'api/errors/getAll';
import getErrorCounts from 'api/errors/getErrorCounts';
import { ResizeTable } from 'components/ResizeTable';
import ROUTES from 'constants/routes';
import dayjs from 'dayjs';
import { useNotifications } from 'hooks/useNotifications';
import useResourceAttribute from 'hooks/useResourceAttribute';
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
import { TimestampInput } from 'hooks/useTimezoneFormatter/useTimezoneFormatter';
import useUrlQuery from 'hooks/useUrlQuery';
import createQueryParams from 'lib/createQueryParams';
import history from 'lib/history';
import { isUndefined } from 'lodash-es';
import { useTimezone } from 'providers/Timezone';
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useQueries } from 'react-query';
@@ -155,8 +156,16 @@ function AllErrors(): JSX.Element {
}
}, [data?.error, data?.payload, t, notifications]);
const getDateValue = (value: string): JSX.Element => (
<Typography>{dayjs(value).format('DD/MM/YYYY HH:mm:ss A')}</Typography>
const getDateValue = (
value: string,
formatTimezoneAdjustedTimestamp: (
input: TimestampInput,
format?: string,
) => string,
): JSX.Element => (
<Typography>
{formatTimezoneAdjustedTimestamp(value, 'DD/MM/YYYY hh:mm:ss A')}
</Typography>
);
const filterIcon = useCallback(() => <SearchOutlined />, []);
@@ -283,6 +292,8 @@ function AllErrors(): JSX.Element {
[filterIcon, filterDropdownWrapper],
);
const { formatTimezoneAdjustedTimestamp } = useTimezone();
const columns: ColumnsType<Exception> = [
{
title: 'Exception Type',
@@ -342,7 +353,8 @@ function AllErrors(): JSX.Element {
dataIndex: 'lastSeen',
width: 80,
key: 'lastSeen',
render: getDateValue,
render: (value): JSX.Element =>
getDateValue(value, formatTimezoneAdjustedTimestamp),
sorter: true,
defaultSortOrder: getDefaultOrder(
getUpdatedParams,
@@ -355,7 +367,8 @@ function AllErrors(): JSX.Element {
dataIndex: 'firstSeen',
width: 80,
key: 'firstSeen',
render: getDateValue,
render: (value): JSX.Element =>
getDateValue(value, formatTimezoneAdjustedTimestamp),
sorter: true,
defaultSortOrder: getDefaultOrder(
getUpdatedParams,

View File

@@ -10,6 +10,7 @@ import getAxes from 'lib/uPlotLib/utils/getAxes';
import { getUplotChartDataForAnomalyDetection } from 'lib/uPlotLib/utils/getUplotChartData';
import { getYAxisScaleForAnomalyDetection } from 'lib/uPlotLib/utils/getYAxisScale';
import { LineChart } from 'lucide-react';
import { useTimezone } from 'providers/Timezone';
import { useEffect, useRef, useState } from 'react';
import uPlot from 'uplot';
@@ -148,10 +149,12 @@ function AnomalyAlertEvaluationView({
]
: [];
const { timezone } = useTimezone();
const options = {
width: dimensions.width,
height: dimensions.height - 36,
plugins: [bandsPlugin, tooltipPlugin(isDarkMode)],
plugins: [bandsPlugin, tooltipPlugin(isDarkMode, timezone.value)],
focus: {
alpha: 0.3,
},
@@ -256,6 +259,8 @@ function AnomalyAlertEvaluationView({
show: true,
},
axes: getAxes(isDarkMode, yAxisUnit),
tzDate: (timestamp: number): Date =>
uPlot.tzDate(new Date(timestamp * 1e3), timezone.value),
};
const handleSearch = (searchText: string): void => {

View File

@@ -1,8 +1,10 @@
import { themeColors } from 'constants/theme';
import dayjs from 'dayjs';
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
const tooltipPlugin = (
isDarkMode: boolean,
timezone: string,
): { hooks: { init: (u: any) => void } } => {
let tooltip: HTMLDivElement;
const tooltipLeftOffset = 10;
@@ -17,7 +19,7 @@ const tooltipPlugin = (
return value.toFixed(3);
}
if (value instanceof Date) {
return value.toLocaleString();
return dayjs(value).tz(timezone).format('MM/DD/YYYY, h:mm:ss A');
}
if (value == null) {
return 'N/A';

View File

@@ -117,7 +117,7 @@ export const logAlertDefaults: AlertDef = {
chQueries: {
A: {
name: 'A',
query: `select \ntoStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 30 MINUTE) AS interval, \ntoFloat64(count()) as value \nFROM signoz_logs.distributed_logs_v2 \nWHERE timestamp BETWEEN {{.start_timestamp_nano}} AND {{.end_timestamp_nano}} \nGROUP BY interval;\n\n-- available variables:\n-- \t{{.start_timestamp_nano}}\n-- \t{{.end_timestamp_nano}}\n\n-- required columns (or alias):\n-- \tvalue\n-- \tinterval`,
query: `select \ntoStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 30 MINUTE) AS interval, \ntoFloat64(count()) as value \nFROM signoz_logs.distributed_logs_v2 \nWHERE timestamp BETWEEN {{.start_timestamp_nano}} AND {{.end_timestamp_nano}} \nAND ts_bucket_start BETWEEN {{.start_timestamp}} - 1800 AND {{.end_timestamp}} \nGROUP BY interval;\n\n-- Please check docs here https://signoz.io/docs/userguide/logs_clickhouse_queries/\n\n-- available variables:\n-- \t{{.start_timestamp_nano}}\n-- \t{{.end_timestamp_nano}}\n-- \t{{.start_timestamp}}\n-- \t{{.end_timestamp}}\n\n-- required columns (or alias):\n-- \tvalue\n-- \tinterval`,
legend: '',
disabled: false,
},
@@ -149,7 +149,7 @@ export const traceAlertDefaults: AlertDef = {
chQueries: {
A: {
name: 'A',
query: `SELECT \n\ttoStartOfInterval(timestamp, INTERVAL 1 MINUTE) AS interval, \n\tstringTagMap['peer.service'] AS op_name, \n\ttoFloat64(avg(durationNano)) AS value \nFROM signoz_traces.distributed_signoz_index_v2 \nWHERE stringTagMap['peer.service']!='' \nAND timestamp BETWEEN {{.start_datetime}} AND {{.end_datetime}} \nGROUP BY (op_name, interval);\n\n-- available variables:\n-- \t{{.start_datetime}}\n-- \t{{.end_datetime}}\n\n-- required column alias:\n-- \tvalue\n-- \tinterval`,
query: `SELECT \n\ttoStartOfInterval(timestamp, INTERVAL 1 MINUTE) AS interval, \n\tresource_string_service$$name AS \`service.name\`, \n\ttoFloat64(avg(duration_nano)) AS value \nFROM signoz_traces.distributed_signoz_index_v3 \nWHERE resource_string_service$$name !='' \nAND timestamp BETWEEN {{.start_datetime}} AND {{.end_datetime}} \nAND ts_bucket_start BETWEEN {{.start_timestamp}} - 1800 AND {{.end_timestamp}} \nGROUP BY (\`service.name\`, interval);\n\n-- Please check docs here https://signoz.io/docs/userguide/writing-clickhouse-traces-query/\n\n-- available variables:\n-- \t{{.start_datetime}}\n-- \t{{.end_datetime}}\n-- \t{{.start_timestamp}}\n-- \t{{.end_timestamp}}\n\n-- required column alias:\n-- \tvalue\n-- \tinterval`,
legend: '',
disabled: false,
},

View File

@@ -6,12 +6,12 @@ import getNextPrevId from 'api/errors/getNextPrevId';
import Editor from 'components/Editor';
import { ResizeTable } from 'components/ResizeTable';
import { getNanoSeconds } from 'container/AllError/utils';
import dayjs from 'dayjs';
import { useNotifications } from 'hooks/useNotifications';
import createQueryParams from 'lib/createQueryParams';
import history from 'lib/history';
import { isUndefined } from 'lodash-es';
import { urlKey } from 'pages/ErrorDetails/utils';
import { useTimezone } from 'providers/Timezone';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useQuery } from 'react-query';
@@ -103,8 +103,6 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
}
};
const timeStamp = dayjs(errorDetail.timestamp);
const data: { key: string; value: string }[] = Object.keys(errorDetail)
.filter((e) => !keyToExclude.includes(e))
.map((key) => ({
@@ -136,6 +134,8 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data]);
const { formatTimezoneAdjustedTimestamp } = useTimezone();
return (
<>
<Typography>{errorDetail.exceptionType}</Typography>
@@ -145,7 +145,12 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
<EventContainer>
<div>
<Typography>Event {errorDetail.errorId}</Typography>
<Typography>{timeStamp.format('MMM DD YYYY hh:mm:ss A')}</Typography>
<Typography>
{formatTimezoneAdjustedTimestamp(
errorDetail.timestamp,
'DD/MM/YYYY hh:mm:ss A (UTC Z)',
)}
</Typography>
</div>
<div>
<Space align="end" direction="horizontal">

View File

@@ -25,6 +25,7 @@ import getTimeString from 'lib/getTimeString';
import history from 'lib/history';
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { useTimezone } from 'providers/Timezone';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
@@ -35,6 +36,7 @@ import { AlertDef } from 'types/api/alerts/def';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { GlobalReducer } from 'types/reducer/globalTime';
import uPlot from 'uplot';
import { getGraphType } from 'utils/getGraphType';
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
import { getTimeRange } from 'utils/getTimeRange';
@@ -201,6 +203,8 @@ function ChartPreview({
[dispatch, location.pathname, urlQuery],
);
const { timezone } = useTimezone();
const options = useMemo(
() =>
getUPlotChartOptions({
@@ -236,6 +240,9 @@ function ChartPreview({
softMax: null,
softMin: null,
panelType: graphType,
tzDate: (timestamp: number) =>
uPlot.tzDate(new Date(timestamp * 1e3), timezone.value),
timezone: timezone.value,
}),
[
yAxisUnit,
@@ -250,6 +257,7 @@ function ChartPreview({
optionName,
alertDef?.condition.targetUnit,
graphType,
timezone.value,
],
);

View File

@@ -4,6 +4,7 @@ import { Popover, Typography } from 'antd';
import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils';
import dayjs from 'dayjs';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useTimezone } from 'providers/Timezone';
import { useEffect } from 'react';
import { toFixed } from 'utils/toFixed';
@@ -32,13 +33,17 @@ function Span(props: SpanLengthProps): JSX.Element {
const isDarkMode = useIsDarkMode();
const { time, timeUnitName } = convertTimeToRelevantUnit(inMsCount);
const { timezone } = useTimezone();
useEffect(() => {
document.documentElement.scrollTop = document.documentElement.clientHeight;
document.documentElement.scrollLeft = document.documentElement.clientWidth;
}, []);
const getContent = (): JSX.Element => {
const timeStamp = dayjs(startTime).format('h:mm:ss:SSS A');
const timeStamp = dayjs(startTime)
.tz(timezone.value)
.format('h:mm:ss:SSS A (UTC Z)');
const startTimeInMs = startTime - globalStart;
return (
<div>

View File

@@ -31,7 +31,7 @@ import { AxiosError } from 'axios';
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
import Tags from 'components/Tags/Tags';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import dayjs, { Dayjs } from 'dayjs';
import dayjs from 'dayjs';
import { useGetAllIngestionsKeys } from 'hooks/IngestionKeys/useGetAllIngestionKeys';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { useNotifications } from 'hooks/useNotifications';
@@ -51,6 +51,7 @@ import {
Trash2,
X,
} from 'lucide-react';
import { useTimezone } from 'providers/Timezone';
import { ChangeEvent, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useMutation } from 'react-query';
@@ -70,7 +71,10 @@ const { Option } = Select;
const BYTES = 1073741824;
export const disabledDate = (current: Dayjs): boolean =>
// Using any type here because antd's DatePicker expects its own internal Dayjs type
// which conflicts with our project's Dayjs type that has additional plugins (tz, utc etc).
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
export const disabledDate = (current: any): boolean =>
// Disable all dates before today
current && current < dayjs().endOf('day');
@@ -393,8 +397,11 @@ function MultiIngestionSettings(): JSX.Element {
const gbToBytes = (gb: number): number => Math.round(gb * 1024 ** 3);
const getFormattedTime = (date: string): string =>
dayjs(date).format('MMM DD,YYYY, hh:mm a');
const getFormattedTime = (
date: string,
formatTimezoneAdjustedTimestamp: (date: string, format: string) => string,
): string =>
formatTimezoneAdjustedTimestamp(date, 'MMM DD,YYYY, hh:mm a (UTC Z)');
const showDeleteLimitModal = (
APIKey: IngestionKeyProps,
@@ -544,17 +551,27 @@ function MultiIngestionSettings(): JSX.Element {
}
};
const { formatTimezoneAdjustedTimestamp } = useTimezone();
const columns: AntDTableProps<IngestionKeyProps>['columns'] = [
{
title: 'Ingestion Key',
key: 'ingestion-key',
// eslint-disable-next-line sonarjs/cognitive-complexity
render: (APIKey: IngestionKeyProps): JSX.Element => {
const createdOn = getFormattedTime(APIKey.created_at);
const createdOn = getFormattedTime(
APIKey.created_at,
formatTimezoneAdjustedTimestamp,
);
const formattedDateAndTime =
APIKey && APIKey?.expires_at && getFormattedTime(APIKey?.expires_at);
APIKey &&
APIKey?.expires_at &&
getFormattedTime(APIKey?.expires_at, formatTimezoneAdjustedTimestamp);
const updatedOn = getFormattedTime(APIKey?.updated_at);
const updatedOn = getFormattedTime(
APIKey?.updated_at,
formatTimezoneAdjustedTimestamp,
);
const limits: { [key: string]: LimitProps } = {};

View File

@@ -1,8 +1,20 @@
import { Typography } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import { ResizeTable } from 'components/ResizeTable';
import { useTimezone } from 'providers/Timezone';
import { useTranslation } from 'react-i18next';
import { License } from 'types/api/licenses/def';
function ValidityColumn({ value }: { value: string }): JSX.Element {
const { formatTimezoneAdjustedTimestamp } = useTimezone();
return (
<Typography>
{formatTimezoneAdjustedTimestamp(value, 'YYYY-MM-DD HH:mm:ss (UTC Z)')}
</Typography>
);
}
function ListLicenses({ licenses }: ListLicensesProps): JSX.Element {
const { t } = useTranslation(['licenses']);
@@ -23,12 +35,14 @@ function ListLicenses({ licenses }: ListLicensesProps): JSX.Element {
title: t('column_valid_from'),
dataIndex: 'ValidFrom',
key: 'valid from',
render: (value: string): JSX.Element => ValidityColumn({ value }),
width: 80,
},
{
title: t('column_valid_until'),
dataIndex: 'ValidUntil',
key: 'valid until',
render: (value: string): JSX.Element => ValidityColumn({ value }),
width: 80,
},
];

View File

@@ -867,7 +867,7 @@
.configure-metadata-root {
.ant-modal-content {
width: 400px;
width: 500px;
flex-shrink: 0;
border-radius: 4px;
border: 1px solid var(--Slate-500, #161922);
@@ -1039,7 +1039,6 @@
display: flex;
justify-content: space-between;
align-items: center;
width: 336px;
padding: 0px 0px 0px 14.634px;
.left {

View File

@@ -27,6 +27,8 @@ import { AxiosError } from 'axios';
import cx from 'classnames';
import { ENTITY_VERSION_V4 } from 'constants/app';
import ROUTES from 'constants/routes';
import { sanitizeDashboardData } from 'container/NewDashboard/DashboardDescription';
import { downloadObjectAsJson } from 'container/NewDashboard/DashboardDescription/utils';
import { Base64Icons } from 'container/NewDashboard/DashboardSettings/General/utils';
import dayjs from 'dayjs';
import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
@@ -44,6 +46,7 @@ import {
EllipsisVertical,
Expand,
ExternalLink,
FileJson,
Github,
HdmiPort,
LayoutGrid,
@@ -57,6 +60,7 @@ import {
// see more: https://github.com/lucide-icons/lucide/issues/94
import { handleContactSupport } from 'pages/Integrations/utils';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useTimezone } from 'providers/Timezone';
import {
ChangeEvent,
Key,
@@ -66,12 +70,18 @@ import {
useRef,
useState,
} from 'react';
import { Layout } from 'react-grid-layout';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { generatePath, Link } from 'react-router-dom';
import { useCopyToClipboard } from 'react-use';
import { AppState } from 'store/reducers';
import { Dashboard } from 'types/api/dashboard/getAll';
import {
Dashboard,
IDashboardVariable,
WidgetRow,
Widgets,
} from 'types/api/dashboard/getAll';
import AppReducer from 'types/reducer/app';
import { isCloudUser } from 'utils/app';
@@ -260,6 +270,11 @@ function DashboardsList(): JSX.Element {
isLocked: !!e.isLocked || false,
lastUpdatedBy: e.updated_by,
image: e.data.image || Base64Icons[0],
variables: e.data.variables,
widgets: e.data.widgets,
layout: e.data.layout,
panelMap: e.data.panelMap,
version: e.data.version,
refetchDashboardList,
})) || [];
@@ -343,31 +358,13 @@ function DashboardsList(): JSX.Element {
}
}, [state.error, state.value, t, notifications]);
const { formatTimezoneAdjustedTimestamp } = useTimezone();
function getFormattedTime(dashboard: Dashboard, option: string): string {
const timeOptions: Intl.DateTimeFormatOptions = {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
};
const formattedTime = new Date(get(dashboard, option, '')).toLocaleTimeString(
'en-US',
timeOptions,
return formatTimezoneAdjustedTimestamp(
get(dashboard, option, ''),
'MMM D, YYYY ⎯ hh:mm:ss A (UTC Z)',
);
const dateOptions: Intl.DateTimeFormatOptions = {
month: 'short',
day: 'numeric',
year: 'numeric',
};
const formattedDate = new Date(get(dashboard, option, '')).toLocaleDateString(
'en-US',
dateOptions,
);
// Combine time and date
return `${formattedDate}${formattedTime}`;
}
const onLastUpdated = (time: string): string => {
@@ -410,31 +407,11 @@ function DashboardsList(): JSX.Element {
title: 'Dashboards',
key: 'dashboard',
render: (dashboard: Data, _, index): JSX.Element => {
const timeOptions: Intl.DateTimeFormatOptions = {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
};
const formattedTime = new Date(dashboard.createdAt).toLocaleTimeString(
'en-US',
timeOptions,
const formattedDateAndTime = formatTimezoneAdjustedTimestamp(
dashboard.createdAt,
'MMM D, YYYY ⎯ hh:mm:ss A (UTC Z)',
);
const dateOptions: Intl.DateTimeFormatOptions = {
month: 'short',
day: 'numeric',
year: 'numeric',
};
const formattedDate = new Date(dashboard.createdAt).toLocaleDateString(
'en-US',
dateOptions,
);
// Combine time and date
const formattedDateAndTime = `${formattedDate}${formattedTime}`;
const getLink = (): string => `${ROUTES.ALL_DASHBOARD}/${dashboard.id}`;
const onClickHandler = (event: React.MouseEvent<HTMLElement>): void => {
@@ -450,6 +427,15 @@ function DashboardsList(): JSX.Element {
});
};
const handleJsonExport = (event: React.MouseEvent<HTMLElement>): void => {
event.stopPropagation();
event.preventDefault();
downloadObjectAsJson(
sanitizeDashboardData({ ...dashboard, title: dashboard.name }),
dashboard.name,
);
};
return (
<div className="dashboard-list-item" onClick={onClickHandler}>
<div className="title-with-action">
@@ -523,6 +509,14 @@ function DashboardsList(): JSX.Element {
>
Copy Link
</Button>
<Button
type="text"
className="action-btn"
icon={<FileJson size={12} />}
onClick={handleJsonExport}
>
Export JSON
</Button>
</section>
<section className="section-2">
<DeleteButton
@@ -541,6 +535,7 @@ function DashboardsList(): JSX.Element {
<EllipsisVertical
className="dashboard-action-icon"
size={14}
data-testid="dashboard-action-icon"
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
@@ -1105,6 +1100,11 @@ export interface Data {
isLocked: boolean;
id: string;
image?: string;
widgets?: Array<WidgetRow | Widgets>;
layout?: Layout[];
panelMap?: Record<string, { widgets: Layout[]; collapsed: boolean }>;
variables: Record<string, IDashboardVariable>;
version?: string;
}
export default DashboardsList;

View File

@@ -8,10 +8,12 @@ import { useResizeObserver } from 'hooks/useDimensions';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { useTimezone } from 'providers/Timezone';
import { useMemo, useRef } from 'react';
import { useQueries, UseQueryResult } from 'react-query';
import { SuccessResponse } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import uPlot from 'uplot';
import {
getHostQueryPayload,
@@ -73,6 +75,8 @@ function NodeMetrics({
[queries],
);
const { timezone } = useTimezone();
const options = useMemo(
() =>
queries.map(({ data }, idx) =>
@@ -86,6 +90,9 @@ function NodeMetrics({
minTimeScale: start,
maxTimeScale: end,
verticalLineTimestamp,
tzDate: (timestamp: number) =>
uPlot.tzDate(new Date(timestamp * 1e3), timezone.value),
timezone: timezone.value,
}),
),
[
@@ -96,6 +103,7 @@ function NodeMetrics({
start,
verticalLineTimestamp,
end,
timezone.value,
],
);

View File

@@ -8,10 +8,12 @@ import { useResizeObserver } from 'hooks/useDimensions';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { useTimezone } from 'providers/Timezone';
import { useMemo, useRef } from 'react';
import { useQueries, UseQueryResult } from 'react-query';
import { SuccessResponse } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import uPlot from 'uplot';
import { getPodQueryPayload, podWidgetInfo } from './constants';
@@ -60,6 +62,7 @@ function PodMetrics({
() => queries.map(({ data }) => getUPlotChartData(data?.payload)),
[queries],
);
const { timezone } = useTimezone();
const options = useMemo(
() =>
@@ -74,9 +77,20 @@ function PodMetrics({
minTimeScale: start,
maxTimeScale: end,
verticalLineTimestamp,
tzDate: (timestamp: number) =>
uPlot.tzDate(new Date(timestamp * 1e3), timezone.value),
timezone: timezone.value,
}),
),
[queries, isDarkMode, dimensions, start, verticalLineTimestamp, end],
[
queries,
isDarkMode,
dimensions,
start,
end,
verticalLineTimestamp,
timezone.value,
],
);
const renderCardContent = (

View File

@@ -11,7 +11,8 @@ import ROUTES from 'constants/routes';
import dompurify from 'dompurify';
import { isEmpty } from 'lodash-es';
import { ArrowDownToDot, ArrowUpFromDot, Ellipsis } from 'lucide-react';
import { useMemo, useState } from 'react';
import { useTimezone } from 'providers/Timezone';
import React, { useMemo, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { FORBID_DOM_PURIFY_TAGS } from 'utils/app';
@@ -68,6 +69,8 @@ export function TableViewActions(
const [isOpen, setIsOpen] = useState<boolean>(false);
const { formatTimezoneAdjustedTimestamp } = useTimezone();
if (record.field === 'body') {
const parsedBody = recursiveParseJSON(fieldData.value);
if (!isEmpty(parsedBody)) {
@@ -100,33 +103,44 @@ export function TableViewActions(
);
}
let cleanTimestamp: string;
if (record.field === 'timestamp') {
cleanTimestamp = fieldData.value.replace(/^["']|["']$/g, '');
}
const renderFieldContent = (): JSX.Element => {
const commonStyles: React.CSSProperties = {
color: Color.BG_SIENNA_400,
whiteSpace: 'pre-wrap',
tabSize: 4,
};
switch (record.field) {
case 'body':
return <span style={commonStyles} dangerouslySetInnerHTML={bodyHtml} />;
case 'timestamp':
return (
<span style={commonStyles}>
{formatTimezoneAdjustedTimestamp(
cleanTimestamp,
'MM/DD/YYYY, HH:mm:ss.SSS (UTC Z)',
)}
</span>
);
default:
return (
<span style={commonStyles}>{removeEscapeCharacters(fieldData.value)}</span>
);
}
};
return (
<div className={cx('value-field', isOpen ? 'open-popover' : '')}>
{record.field === 'body' ? (
<CopyClipboardHOC entityKey={fieldFilterKey} textToCopy={textToCopy}>
<span
style={{
color: Color.BG_SIENNA_400,
whiteSpace: 'pre-wrap',
tabSize: 4,
}}
dangerouslySetInnerHTML={bodyHtml}
/>
</CopyClipboardHOC>
) : (
<CopyClipboardHOC entityKey={fieldFilterKey} textToCopy={textToCopy}>
<span
style={{
color: Color.BG_SIENNA_400,
whiteSpace: 'pre-wrap',
tabSize: 4,
}}
>
{removeEscapeCharacters(fieldData.value)}
</span>
</CopyClipboardHOC>
)}
<CopyClipboardHOC entityKey={fieldFilterKey} textToCopy={textToCopy}>
{renderFieldContent()}
</CopyClipboardHOC>
{!isListViewPanel && (
<span className="action-btn">
<Tooltip title="Filter for value">

View File

@@ -50,6 +50,7 @@ import {
} from 'lodash-es';
import { Sliders } from 'lucide-react';
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
import { useTimezone } from 'providers/Timezone';
import {
memo,
MutableRefObject,
@@ -669,13 +670,19 @@ function LogsExplorerViews({
setIsLoadingQueries,
]);
const { timezone } = useTimezone();
const flattenLogData = useMemo(
() =>
logs.map((log) => {
const timestamp =
typeof log.timestamp === 'string'
? dayjs(log.timestamp).format('YYYY-MM-DD HH:mm:ss.SSS')
: dayjs(log.timestamp / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS');
? dayjs(log.timestamp)
.tz(timezone.value)
.format('YYYY-MM-DD HH:mm:ss.SSS')
: dayjs(log.timestamp / 1e6)
.tz(timezone.value)
.format('YYYY-MM-DD HH:mm:ss.SSS');
return FlatLogData({
timestamp,
@@ -683,7 +690,7 @@ function LogsExplorerViews({
...omit(log, 'timestamp', 'body'),
});
}),
[logs],
[logs, timezone.value],
);
return (

View File

@@ -7,6 +7,7 @@ import { rest } from 'msw';
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
import { QueryBuilderProvider } from 'providers/QueryBuilder';
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
import TimezoneProvider from 'providers/Timezone';
import { I18nextProvider } from 'react-i18next';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
@@ -91,17 +92,19 @@ const renderer = (): RenderResult =>
<I18nextProvider i18n={i18n}>
<MockQueryClientProvider>
<QueryBuilderProvider>
<VirtuosoMockContext.Provider
value={{ viewportHeight: 300, itemHeight: 100 }}
>
<LogsExplorerViews
selectedView={SELECTED_VIEWS.SEARCH}
showFrequencyChart
setIsLoadingQueries={(): void => {}}
listQueryKeyRef={{ current: {} }}
chartQueryKeyRef={{ current: {} }}
/>
</VirtuosoMockContext.Provider>
<TimezoneProvider>
<VirtuosoMockContext.Provider
value={{ viewportHeight: 300, itemHeight: 100 }}
>
<LogsExplorerViews
selectedView={SELECTED_VIEWS.SEARCH}
showFrequencyChart
setIsLoadingQueries={(): void => {}}
listQueryKeyRef={{ current: {} }}
chartQueryKeyRef={{ current: {} }}
/>
</VirtuosoMockContext.Provider>
</TimezoneProvider>
</QueryBuilderProvider>
</MockQueryClientProvider>
</I18nextProvider>

View File

@@ -15,6 +15,7 @@ import { useLogsData } from 'hooks/useLogsData';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { FlatLogData } from 'lib/logs/flatLogData';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { useTimezone } from 'providers/Timezone';
import {
Dispatch,
HTMLAttributes,
@@ -76,7 +77,12 @@ function LogsPanelComponent({
});
};
const columns = getLogPanelColumnsList(widget.selectedLogFields);
const { formatTimezoneAdjustedTimestamp } = useTimezone();
const columns = getLogPanelColumnsList(
widget.selectedLogFields,
formatTimezoneAdjustedTimestamp,
);
const dataLength =
queryResponse.data?.payload?.data?.newResult?.data?.result[0]?.list?.length;

View File

@@ -1,6 +1,7 @@
import { ColumnsType } from 'antd/es/table';
import { Typography } from 'antd/lib';
import { OPERATORS } from 'constants/queryBuilder';
import { TimestampInput } from 'hooks/useTimezoneFormatter/useTimezoneFormatter';
// import Typography from 'antd/es/typography/Typography';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { ReactNode } from 'react';
@@ -13,18 +14,31 @@ import { v4 as uuid } from 'uuid';
export const getLogPanelColumnsList = (
selectedLogFields: Widgets['selectedLogFields'],
formatTimezoneAdjustedTimestamp: (
input: TimestampInput,
format?: string,
) => string,
): ColumnsType<RowData> => {
const initialColumns: ColumnsType<RowData> = [];
const columns: ColumnsType<RowData> =
selectedLogFields?.map((field: IField) => {
const { name } = field;
return {
title: name,
dataIndex: name,
key: name,
width: name === 'body' ? 350 : 100,
render: (value: ReactNode): JSX.Element => {
if (name === 'timestamp') {
return (
<Typography.Text>
{formatTimezoneAdjustedTimestamp(value as string)}
</Typography.Text>
);
}
if (name === 'body') {
return (
<Typography.Paragraph ellipsis={{ rows: 1 }} data-testid={name}>

View File

@@ -145,41 +145,50 @@ function Application(): JSX.Element {
[servicename, topLevelOperations],
);
const operationPerSecWidget = getWidgetQueryBuilder({
query: {
queryType: EQueryType.QUERY_BUILDER,
promql: [],
builder: operationPerSec({
servicename,
tagFilterItems,
topLevelOperations: topLevelOperationsRoute,
const operationPerSecWidget = useMemo(
() =>
getWidgetQueryBuilder({
query: {
queryType: EQueryType.QUERY_BUILDER,
promql: [],
builder: operationPerSec({
servicename,
tagFilterItems,
topLevelOperations: topLevelOperationsRoute,
}),
clickhouse_sql: [],
id: uuid(),
},
title: GraphTitle.RATE_PER_OPS,
panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: 'ops',
id: SERVICE_CHART_ID.rps,
}),
clickhouse_sql: [],
id: uuid(),
},
title: GraphTitle.RATE_PER_OPS,
panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: 'ops',
id: SERVICE_CHART_ID.rps,
});
[servicename, tagFilterItems, topLevelOperationsRoute],
);
const errorPercentageWidget = getWidgetQueryBuilder({
query: {
queryType: EQueryType.QUERY_BUILDER,
promql: [],
builder: errorPercentage({
servicename,
tagFilterItems,
topLevelOperations: topLevelOperationsRoute,
const errorPercentageWidget = useMemo(
() =>
getWidgetQueryBuilder({
query: {
queryType: EQueryType.QUERY_BUILDER,
promql: [],
builder: errorPercentage({
servicename,
tagFilterItems,
topLevelOperations: topLevelOperationsRoute,
}),
clickhouse_sql: [],
id: uuid(),
},
title: GraphTitle.ERROR_PERCENTAGE,
panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: '%',
id: SERVICE_CHART_ID.errorPercentage,
fillSpans: true,
}),
clickhouse_sql: [],
id: uuid(),
},
title: GraphTitle.ERROR_PERCENTAGE,
panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: '%',
id: SERVICE_CHART_ID.errorPercentage,
});
[servicename, tagFilterItems, topLevelOperationsRoute],
);
const stepInterval = useMemo(
() =>
@@ -214,12 +223,11 @@ function Application(): JSX.Element {
apmToTraceQuery: Query,
isViewLogsClicked?: boolean,
): (() => void) => (): void => {
const currentTime = timestamp;
const endTime = timestamp + stepInterval;
console.log(endTime, stepInterval);
const endTime = timestamp;
const startTime = timestamp - stepInterval;
const urlParams = new URLSearchParams(search);
urlParams.set(QueryParams.startTime, currentTime.toString());
urlParams.set(QueryParams.startTime, startTime.toString());
urlParams.set(QueryParams.endTime, endTime.toString());
urlParams.delete(QueryParams.relativeTime);
const avialableParams = routeConfig[ROUTES.TRACE];

View File

@@ -53,24 +53,28 @@ function ServiceOverview({
[isSpanMetricEnable, queries],
);
const latencyWidget = getWidgetQueryBuilder({
query: {
queryType: EQueryType.QUERY_BUILDER,
promql: [],
builder: latency({
servicename,
tagFilterItems,
isSpanMetricEnable,
topLevelOperationsRoute,
const latencyWidget = useMemo(
() =>
getWidgetQueryBuilder({
query: {
queryType: EQueryType.QUERY_BUILDER,
promql: [],
builder: latency({
servicename,
tagFilterItems,
isSpanMetricEnable,
topLevelOperationsRoute,
}),
clickhouse_sql: [],
id: uuid(),
},
title: GraphTitle.LATENCY,
panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: 'ns',
id: SERVICE_CHART_ID.latency,
}),
clickhouse_sql: [],
id: uuid(),
},
title: GraphTitle.LATENCY,
panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: 'ns',
id: SERVICE_CHART_ID.latency,
});
[isSpanMetricEnable, servicename, tagFilterItems, topLevelOperationsRoute],
);
const isQueryEnabled =
!topLevelOperationsIsLoading && topLevelOperationsRoute.length > 0;

View File

@@ -65,11 +65,11 @@ export function onViewTracePopupClick({
stepInterval,
}: OnViewTracePopupClickProps): VoidFunction {
return (): void => {
const currentTime = timestamp;
const endTime = timestamp + (stepInterval || 60);
const endTime = timestamp;
const startTime = timestamp - (stepInterval || 60);
const urlParams = new URLSearchParams(window.location.search);
urlParams.set(QueryParams.startTime, currentTime.toString());
urlParams.set(QueryParams.startTime, startTime.toString());
urlParams.set(QueryParams.endTime, endTime.toString());
urlParams.delete(QueryParams.relativeTime);
const avialableParams = routeConfig[ROUTES.TRACE];

View File

@@ -0,0 +1,96 @@
.timezone-adaption {
padding: 16px;
background: var(--bg-ink-400);
border: 1px solid var(--bg-ink-500);
border-radius: 4px;
&__header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
&__title {
color: var(--bg-vanilla-300);
font-size: 14px;
font-weight: 500;
margin: 0;
}
&__description {
color: var(--bg-vanilla-400);
font-size: 14px;
line-height: 20px;
margin: 0 0 12px 0;
}
&__note {
display: flex;
align-items: center;
justify-content: space-between;
padding: 7.5px 12px;
background: rgba(78, 116, 248, 0.1);
border: 1px solid rgba(78, 116, 248, 0.1);
border-radius: 4px;
}
&__bullet {
color: var(--bg-robin-400);
font-size: 16px;
line-height: 20px;
}
&__note-text-container {
display: flex;
align-items: center;
gap: 10px;
}
&__note-text {
display: flex;
align-items: center;
gap: 4px;
color: var(--bg-robin-400);
font-size: 14px;
line-height: 20px;
}
&__note-text-overridden {
display: flex;
align-items: center;
padding: 0 2px;
background: rgba(171, 189, 255, 0.04);
border-radius: 2px;
font-size: 12px;
line-height: 16px;
color: var(--bg-vanilla-100);
}
&__clear-override {
display: flex;
align-items: center;
gap: 6px;
background: transparent;
border: none;
padding: 0;
color: var(--bg-robin-300);
font-size: 12px;
line-height: 16px; /* 133.333% */
letter-spacing: 0.12px;
cursor: pointer;
}
}
.lightMode {
.timezone-adaption {
background-color: var(--bg-vanilla-100);
border-color: var(--bg-vanilla-300);
&__title {
color: var(--text-ink-300);
}
&__description {
color: var(--text-ink-400);
}
}
}

View File

@@ -0,0 +1,82 @@
import './TimezoneAdaptation.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Switch } from 'antd';
import { Delete } from 'lucide-react';
import { useTimezone } from 'providers/Timezone';
import { useMemo } from 'react';
function TimezoneAdaptation(): JSX.Element {
const {
timezone,
browserTimezone,
updateTimezone,
isAdaptationEnabled,
setIsAdaptationEnabled,
} = useTimezone();
const isTimezoneOverridden = useMemo(
() => timezone.offset !== browserTimezone.offset,
[timezone, browserTimezone],
);
const getSwitchStyles = (): React.CSSProperties => ({
backgroundColor:
isAdaptationEnabled && isTimezoneOverridden ? Color.BG_AMBER_400 : undefined,
});
const handleOverrideClear = (): void => {
updateTimezone(browserTimezone);
};
return (
<div className="timezone-adaption">
<div className="timezone-adaption__header">
<h2 className="timezone-adaption__title">Adapt to my timezone</h2>
<Switch
checked={isAdaptationEnabled}
onChange={setIsAdaptationEnabled}
style={getSwitchStyles()}
/>
</div>
<p className="timezone-adaption__description">
Adapt the timestamps shown in the SigNoz console to my active timezone.
</p>
<div className="timezone-adaption__note">
<div className="timezone-adaption__note-text-container">
<span className="timezone-adaption__bullet"></span>
<span className="timezone-adaption__note-text">
{isTimezoneOverridden ? (
<>
Your current timezone is overridden to
<span className="timezone-adaption__note-text-overridden">
{timezone.offset}
</span>
</>
) : (
<>
You can override the timezone adaption for any view with the time
picker.
</>
)}
</span>
</div>
{!!isTimezoneOverridden && (
<button
type="button"
className="timezone-adaption__clear-override"
onClick={handleOverrideClear}
>
<Delete height={12} width={12} color={Color.BG_ROBIN_300} />
Clear override
</button>
)}
</div>
</div>
);
}
export default TimezoneAdaptation;

View File

@@ -7,6 +7,7 @@ import { LogOut, Moon, Sun } from 'lucide-react';
import { useState } from 'react';
import Password from './Password';
import TimezoneAdaptation from './TimezoneAdaptation/TimezoneAdaptation';
import UserInfo from './UserInfo';
function MySettings(): JSX.Element {
@@ -78,6 +79,8 @@ function MySettings(): JSX.Element {
<Password />
</div>
<TimezoneAdaptation />
<Button
className="flexBtn"
onClick={(): void => Logout()}

View File

@@ -65,7 +65,7 @@ interface DashboardDescriptionProps {
handle: FullScreenHandle;
}
function sanitizeDashboardData(
export function sanitizeDashboardData(
selectedData: DashboardData,
): Omit<DashboardData, 'uuid'> {
if (!selectedData?.variables) {

View File

@@ -1,9 +1,15 @@
import { Row } from 'antd';
import { isNull } from 'lodash-es';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { memo, useEffect, useState } from 'react';
import { memo, useEffect, useRef, useState } from 'react';
import { IDashboardVariable } from 'types/api/dashboard/getAll';
import {
buildDependencies,
buildDependencyGraph,
buildParentDependencyGraph,
onUpdateVariableNode,
VariableGraph,
} from './util';
import VariableItem from './VariableItem';
function DashboardVariableSelection(): JSX.Element | null {
@@ -21,6 +27,12 @@ function DashboardVariableSelection(): JSX.Element | null {
const [variablesTableData, setVariablesTableData] = useState<any>([]);
const [dependencyData, setDependencyData] = useState<{
order: string[];
graph: VariableGraph;
parentDependencyGraph: VariableGraph;
} | null>(null);
useEffect(() => {
if (variables) {
const tableRowData = [];
@@ -43,35 +55,28 @@ function DashboardVariableSelection(): JSX.Element | null {
}
}, [variables]);
const onVarChanged = (name: string): void => {
/**
* this function takes care of adding the dependent variables to current update queue and removing
* the updated variable name from the queue
*/
const dependentVariables = variablesTableData
?.map((variable: any) => {
if (variable.type === 'QUERY') {
const re = new RegExp(`\\{\\{\\s*?\\.${name}\\s*?\\}\\}`); // regex for `{{.var}}`
const queryValue = variable.queryValue || '';
const dependVarReMatch = queryValue.match(re);
if (dependVarReMatch !== null && dependVarReMatch.length > 0) {
return variable.name;
}
}
return null;
})
.filter((val: string | null) => !isNull(val));
setVariablesToGetUpdated((prev) => [
...prev.filter((v) => v !== name),
...dependentVariables,
]);
};
const initializationRef = useRef(false);
useEffect(() => {
if (variablesTableData.length > 0 && !initializationRef.current) {
const depGrp = buildDependencies(variablesTableData);
const { order, graph } = buildDependencyGraph(depGrp);
const parentDependencyGraph = buildParentDependencyGraph(graph);
setDependencyData({
order,
graph,
parentDependencyGraph,
});
initializationRef.current = true;
}
}, [variablesTableData]);
const onValueUpdate = (
name: string,
id: string,
value: IDashboardVariable['selectedValue'],
allSelected: boolean,
isMountedCall?: boolean,
// eslint-disable-next-line sonarjs/cognitive-complexity
): void => {
if (id) {
@@ -111,7 +116,18 @@ function DashboardVariableSelection(): JSX.Element | null {
});
}
onVarChanged(name);
if (dependencyData && !isMountedCall) {
const updatedVariables: string[] = [];
onUpdateVariableNode(
name,
dependencyData.graph,
dependencyData.order,
(node) => updatedVariables.push(node),
);
setVariablesToGetUpdated(updatedVariables.filter((v) => v !== name));
} else if (isMountedCall) {
setVariablesToGetUpdated((prev) => prev.filter((v) => v !== name));
}
}
};
@@ -139,6 +155,7 @@ function DashboardVariableSelection(): JSX.Element | null {
onValueUpdate={onValueUpdate}
variablesToGetUpdated={variablesToGetUpdated}
setVariablesToGetUpdated={setVariablesToGetUpdated}
dependencyData={dependencyData}
/>
))}
</Row>

View File

@@ -24,7 +24,7 @@ import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParse
import sortValues from 'lib/dashbaordVariables/sortVariableValues';
import { debounce, isArray, isString } from 'lodash-es';
import map from 'lodash-es/map';
import { ChangeEvent, memo, useEffect, useMemo, useState } from 'react';
import { ChangeEvent, memo, useEffect, useMemo, useRef, useState } from 'react';
import { useQuery } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
@@ -35,12 +35,15 @@ import { popupContainer } from 'utils/selectPopupContainer';
import { variablePropsToPayloadVariables } from '../utils';
import { SelectItemStyle } from './styles';
import { areArraysEqual } from './util';
import {
areArraysEqual,
checkAPIInvocation,
onUpdateVariableNode,
VariableGraph,
} from './util';
const ALL_SELECT_VALUE = '__ALL__';
const variableRegexPattern = /\{\{\s*?\.([^\s}]+)\s*?\}\}/g;
enum ToggleTagValue {
Only = 'Only',
All = 'All',
@@ -54,9 +57,15 @@ interface VariableItemProps {
id: string,
arg1: IDashboardVariable['selectedValue'],
allSelected: boolean,
isMountedCall?: boolean,
) => void;
variablesToGetUpdated: string[];
setVariablesToGetUpdated: React.Dispatch<React.SetStateAction<string[]>>;
dependencyData: {
order: string[];
graph: VariableGraph;
parentDependencyGraph: VariableGraph;
} | null;
}
const getSelectValue = (
@@ -79,6 +88,7 @@ function VariableItem({
onValueUpdate,
variablesToGetUpdated,
setVariablesToGetUpdated,
dependencyData,
}: VariableItemProps): JSX.Element {
const [optionsData, setOptionsData] = useState<(string | number | boolean)[]>(
[],
@@ -88,60 +98,29 @@ function VariableItem({
(state) => state.globalTime,
);
// logic to detect if its a rerender or a new render/mount
const isMounted = useRef(false);
useEffect(() => {
if (variableData.allSelected && variableData.type === 'QUERY') {
setVariablesToGetUpdated((prev) => {
const variablesQueue = [...prev.filter((v) => v !== variableData.name)];
if (variableData.name) {
variablesQueue.push(variableData.name);
}
return variablesQueue;
});
isMounted.current = true;
}, []);
const validVariableUpdate = (): boolean => {
if (!variableData.name) {
return false;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [minTime, maxTime]);
if (!isMounted.current) {
// variableData.name is present as the top element or next in the queue - variablesToGetUpdated
return Boolean(
variablesToGetUpdated.length &&
variablesToGetUpdated[0] === variableData.name,
);
}
return variablesToGetUpdated.includes(variableData.name);
};
const [errorMessage, setErrorMessage] = useState<null | string>(null);
const getDependentVariables = (queryValue: string): string[] => {
const matches = queryValue.match(variableRegexPattern);
// Extract variable names from the matches array without {{ . }}
return matches
? matches.map((match) => match.replace(variableRegexPattern, '$1'))
: [];
};
const getQueryKey = (variableData: IDashboardVariable): string[] => {
let dependentVariablesStr = '';
const dependentVariables = getDependentVariables(
variableData.queryValue || '',
);
const variableName = variableData.name || '';
dependentVariables?.forEach((element) => {
const [, variable] =
Object.entries(existingVariables).find(
([, value]) => value.name === element,
) || [];
dependentVariablesStr += `${element}${variable?.selectedValue}`;
});
const variableKey = dependentVariablesStr.replace(/\s/g, '');
// added this time dependency for variables query as API respects the passed time range now
return [
REACT_QUERY_KEY.DASHBOARD_BY_ID,
variableName,
variableKey,
`${minTime}`,
`${maxTime}`,
];
};
// eslint-disable-next-line sonarjs/cognitive-complexity
const getOptions = (variablesRes: VariableResponseProps | null): void => {
if (variablesRes && variableData.type === 'QUERY') {
@@ -184,9 +163,7 @@ function VariableItem({
if (
variableData.type === 'QUERY' &&
variableData.name &&
(variablesToGetUpdated.includes(variableData.name) ||
valueNotInList ||
variableData.allSelected)
(validVariableUpdate() || valueNotInList || variableData.allSelected)
) {
let value = variableData.selectedValue;
let allSelected = false;
@@ -200,7 +177,16 @@ function VariableItem({
}
if (variableData && variableData?.name && variableData?.id) {
onValueUpdate(variableData.name, variableData.id, value, allSelected);
onValueUpdate(
variableData.name,
variableData.id,
value,
allSelected,
isMounted.current,
);
setVariablesToGetUpdated((prev) =>
prev.filter((name) => name !== variableData.name),
);
}
}
@@ -224,36 +210,75 @@ function VariableItem({
}
};
const { isLoading } = useQuery(getQueryKey(variableData), {
enabled: variableData && variableData.type === 'QUERY',
queryFn: () =>
dashboardVariablesQuery({
query: variableData.queryValue || '',
variables: variablePropsToPayloadVariables(existingVariables),
}),
refetchOnWindowFocus: false,
onSuccess: (response) => {
getOptions(response.payload);
},
onError: (error: {
details: {
error: string;
};
}) => {
const { details } = error;
if (details.error) {
let message = details.error;
if (details.error.includes('Syntax error:')) {
message =
'Please make sure query is valid and dependent variables are selected';
const { isLoading } = useQuery(
[
REACT_QUERY_KEY.DASHBOARD_BY_ID,
variableData.name || '',
`${minTime}`,
`${maxTime}`,
],
{
enabled:
variableData &&
variableData.type === 'QUERY' &&
checkAPIInvocation(
variablesToGetUpdated,
variableData,
dependencyData?.parentDependencyGraph,
),
queryFn: () =>
dashboardVariablesQuery({
query: variableData.queryValue || '',
variables: variablePropsToPayloadVariables(existingVariables),
}),
refetchOnWindowFocus: false,
onSuccess: (response) => {
getOptions(response.payload);
if (
dependencyData?.parentDependencyGraph[variableData.name || ''].length === 0
) {
const updatedVariables: string[] = [];
onUpdateVariableNode(
variableData.name || '',
dependencyData.graph,
dependencyData.order,
(node) => updatedVariables.push(node),
);
setVariablesToGetUpdated((prev) => [
...prev,
...updatedVariables.filter((v) => v !== variableData.name),
]);
}
setErrorMessage(message);
}
},
onError: (error: {
details: {
error: string;
};
}) => {
const { details } = error;
if (details.error) {
let message = details.error;
if (details.error.includes('Syntax error:')) {
message =
'Please make sure query is valid and dependent variables are selected';
}
setErrorMessage(message);
}
},
},
});
);
const handleChange = (value: string | string[]): void => {
// if value is equal to selected value then return
if (
value === variableData.selectedValue ||
(Array.isArray(value) &&
Array.isArray(variableData.selectedValue) &&
areArraysEqual(value, variableData.selectedValue))
) {
return;
}
if (variableData.name) {
if (
value === ALL_SELECT_VALUE ||

View File

@@ -0,0 +1,241 @@
import {
buildDependencies,
buildDependencyGraph,
buildParentDependencyGraph,
checkAPIInvocation,
onUpdateVariableNode,
VariableGraph,
} from '../util';
import {
buildDependenciesMock,
buildGraphMock,
checkAPIInvocationMock,
onUpdateVariableNodeMock,
} from './mock';
describe('dashboardVariables - utilities and processors', () => {
describe('onUpdateVariableNode', () => {
const { graph, topologicalOrder } = onUpdateVariableNodeMock;
const testCases = [
{
scenario: 'root element',
nodeToUpdate: 'deployment_environment',
expected: [
'deployment_environment',
'service_name',
'endpoint',
'http_status_code',
],
},
{
scenario: 'middle child',
nodeToUpdate: 'k8s_node_name',
expected: ['k8s_node_name', 'k8s_namespace_name'],
},
{
scenario: 'leaf element',
nodeToUpdate: 'http_status_code',
expected: ['http_status_code'],
},
{
scenario: 'node not in graph',
nodeToUpdate: 'unknown',
expected: [],
},
{
scenario: 'node not in topological order',
nodeToUpdate: 'unknown',
expected: [],
},
];
test.each(testCases)(
'should update variable node when $scenario',
({ nodeToUpdate, expected }) => {
const updatedVariables: string[] = [];
const callback = (node: string): void => {
updatedVariables.push(node);
};
onUpdateVariableNode(nodeToUpdate, graph, topologicalOrder, callback);
expect(updatedVariables).toEqual(expected);
},
);
it('should return empty array when topological order is empty', () => {
const updatedVariables: string[] = [];
onUpdateVariableNode('http_status_code', graph, [], (node) =>
updatedVariables.push(node),
);
expect(updatedVariables).toEqual([]);
});
});
describe('checkAPIInvocation', () => {
const {
variablesToGetUpdated,
variableData,
parentDependencyGraph,
} = checkAPIInvocationMock;
const mockRootElement = {
name: 'deployment_environment',
key: '036a47cd-9ffc-47de-9f27-0329198964a8',
id: '036a47cd-9ffc-47de-9f27-0329198964a8',
modificationUUID: '5f71b591-f583-497c-839d-6a1590c3f60f',
selectedValue: 'production',
type: 'QUERY',
// ... other properties omitted for brevity
} as any;
describe('edge cases', () => {
it('should return false when variableData is empty', () => {
expect(
checkAPIInvocation(
variablesToGetUpdated,
variableData,
parentDependencyGraph,
),
).toBeFalsy();
});
it('should return true when parentDependencyGraph is empty', () => {
expect(
checkAPIInvocation(variablesToGetUpdated, variableData, {}),
).toBeTruthy();
});
});
describe('variable sequences', () => {
it('should return true for valid sequence', () => {
expect(
checkAPIInvocation(
['k8s_node_name', 'k8s_namespace_name'],
variableData,
parentDependencyGraph,
),
).toBeTruthy();
});
it('should return false for invalid sequence', () => {
expect(
checkAPIInvocation(
['k8s_cluster_name', 'k8s_node_name', 'k8s_namespace_name'],
variableData,
parentDependencyGraph,
),
).toBeFalsy();
});
it('should return false when variableData is not in sequence', () => {
expect(
checkAPIInvocation(
['deployment_environment', 'service_name', 'endpoint'],
variableData,
parentDependencyGraph,
),
).toBeFalsy();
});
});
describe('root element behavior', () => {
it('should return true for valid root element sequence', () => {
expect(
checkAPIInvocation(
[
'deployment_environment',
'service_name',
'endpoint',
'http_status_code',
],
mockRootElement,
parentDependencyGraph,
),
).toBeTruthy();
});
it('should return true for empty variablesToGetUpdated array', () => {
expect(
checkAPIInvocation([], mockRootElement, parentDependencyGraph),
).toBeTruthy();
});
});
});
describe('Graph Building Utilities', () => {
const { graph } = buildGraphMock;
const { variables } = buildDependenciesMock;
describe('buildParentDependencyGraph', () => {
it('should build parent dependency graph with correct relationships', () => {
const expected = {
deployment_environment: [],
service_name: ['deployment_environment'],
endpoint: ['deployment_environment', 'service_name'],
http_status_code: ['endpoint'],
k8s_cluster_name: [],
k8s_node_name: ['k8s_cluster_name'],
k8s_namespace_name: ['k8s_cluster_name', 'k8s_node_name'],
environment: [],
};
expect(buildParentDependencyGraph(graph)).toEqual(expected);
});
it('should handle empty graph', () => {
expect(buildParentDependencyGraph({})).toEqual({});
});
});
describe('buildDependencyGraph', () => {
it('should build complete dependency graph with correct structure and order', () => {
const expected = {
graph: {
deployment_environment: ['service_name', 'endpoint'],
service_name: ['endpoint'],
endpoint: ['http_status_code'],
http_status_code: [],
k8s_cluster_name: ['k8s_node_name', 'k8s_namespace_name'],
k8s_node_name: ['k8s_namespace_name'],
k8s_namespace_name: [],
environment: [],
},
order: [
'deployment_environment',
'k8s_cluster_name',
'environment',
'service_name',
'k8s_node_name',
'endpoint',
'k8s_namespace_name',
'http_status_code',
],
};
expect(buildDependencyGraph(graph)).toEqual(expected);
});
});
describe('buildDependencies', () => {
it('should build dependency map from variables array', () => {
const expected: VariableGraph = {
deployment_environment: ['service_name', 'endpoint'],
service_name: ['endpoint'],
endpoint: ['http_status_code'],
http_status_code: [],
k8s_cluster_name: ['k8s_node_name', 'k8s_namespace_name'],
k8s_node_name: ['k8s_namespace_name'],
k8s_namespace_name: [],
environment: [],
};
expect(buildDependencies(variables)).toEqual(expected);
});
it('should handle empty variables array', () => {
expect(buildDependencies([])).toEqual({});
});
});
});
});

View File

@@ -0,0 +1,251 @@
/* eslint-disable sonarjs/no-duplicate-string */
export const checkAPIInvocationMock = {
variablesToGetUpdated: [],
variableData: {
name: 'k8s_node_name',
key: '4d71d385-beaf-4434-8dbf-c62be68049fc',
allSelected: false,
customValue: '',
description: '',
id: '4d71d385-beaf-4434-8dbf-c62be68049fc',
modificationUUID: '77233d3c-96d7-4ccb-aa9d-11b04d563068',
multiSelect: false,
order: 6,
queryValue:
"SELECT JSONExtractString(labels, 'k8s_node_name') AS k8s_node_name\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'k8s_node_cpu_time' AND JSONExtractString(labels, 'k8s_cluster_name') = {{.k8s_cluster_name}}\nGROUP BY k8s_node_name",
selectedValue: 'gke-signoz-saas-si-consumer-bsc-e2sd4-a6d430fa-gvm2',
showALLOption: false,
sort: 'DISABLED',
textboxValue: '',
type: 'QUERY',
},
parentDependencyGraph: {
deployment_environment: [],
service_name: ['deployment_environment'],
endpoint: ['deployment_environment', 'service_name'],
http_status_code: ['endpoint'],
k8s_cluster_name: [],
environment: [],
k8s_node_name: ['k8s_cluster_name'],
k8s_namespace_name: ['k8s_cluster_name', 'k8s_node_name'],
},
} as any;
export const onUpdateVariableNodeMock = {
nodeToUpdate: 'deployment_environment',
graph: {
deployment_environment: ['service_name', 'endpoint'],
service_name: ['endpoint'],
endpoint: ['http_status_code'],
http_status_code: [],
k8s_cluster_name: ['k8s_node_name', 'k8s_namespace_name'],
environment: [],
k8s_node_name: ['k8s_namespace_name'],
k8s_namespace_name: [],
},
topologicalOrder: [
'deployment_environment',
'k8s_cluster_name',
'environment',
'service_name',
'k8s_node_name',
'endpoint',
'k8s_namespace_name',
'http_status_code',
],
callback: jest.fn(),
};
export const buildGraphMock = {
graph: {
deployment_environment: ['service_name', 'endpoint'],
service_name: ['endpoint'],
endpoint: ['http_status_code'],
http_status_code: [],
k8s_cluster_name: ['k8s_node_name', 'k8s_namespace_name'],
environment: [],
k8s_node_name: ['k8s_namespace_name'],
k8s_namespace_name: [],
},
};
export const buildDependenciesMock = {
variables: [
{
key: '036a47cd-9ffc-47de-9f27-0329198964a8',
name: 'deployment_environment',
allSelected: false,
customValue: '',
description: '',
id: '036a47cd-9ffc-47de-9f27-0329198964a8',
modificationUUID: '5f71b591-f583-497c-839d-6a1590c3f60f',
multiSelect: false,
order: 0,
queryValue:
"SELECT DISTINCT JSONExtractString(labels, 'deployment_environment') AS deployment_environment\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'signoz_calls_total'",
selectedValue: 'production',
showALLOption: false,
sort: 'DISABLED',
textboxValue: '',
type: 'QUERY',
},
{
key: 'eed5c917-1860-4c7e-bf6d-a05b97bafbc9',
name: 'service_name',
allSelected: true,
customValue: '',
description: '',
id: 'eed5c917-1860-4c7e-bf6d-a05b97bafbc9',
modificationUUID: '85db928b-ac9b-4e9f-b274-791112102fdf',
multiSelect: true,
order: 1,
queryValue:
"SELECT DISTINCT JSONExtractString(labels, 'service_name') FROM signoz_metrics.distributed_time_series_v4_1day\n WHERE metric_name = 'signoz_calls_total' and JSONExtractString(labels, 'deployment_environment') = {{.deployment_environment}}",
selectedValue: ['otelgateway'],
showALLOption: true,
sort: 'ASC',
textboxValue: '',
type: 'QUERY',
},
{
key: '4022d3c1-e845-4952-8984-78f25f575c7a',
name: 'endpoint',
allSelected: true,
customValue: '',
description: '',
id: '4022d3c1-e845-4952-8984-78f25f575c7a',
modificationUUID: 'c0107fa1-ebb7-4dd3-aa9d-6ba08ecc594d',
multiSelect: true,
order: 2,
queryValue:
"SELECT DISTINCT JSONExtractString(labels, 'operation') FROM signoz_metrics.distributed_time_series_v4_1day\n WHERE metric_name = 'signoz_calls_total' AND JSONExtractString(labels, 'service_name') IN {{.service_name}} and JSONExtractString(labels, 'deployment_environment') = {{.deployment_environment}}",
selectedValue: [
'//v1/traces',
'/logs/heroku',
'/logs/json',
'/logs/vector',
'/v1/logs',
'/v1/metrics',
'/v1/traces',
'SELECT',
'exporter/signozkafka/logs',
'exporter/signozkafka/metrics',
'exporter/signozkafka/traces',
'extension/signozkeyauth/Authenticate',
'get',
'hmget',
'opentelemetry.proto.collector.logs.v1.LogsService/Export',
'opentelemetry.proto.collector.metrics.v1.MetricsService/Export',
'opentelemetry.proto.collector.trace.v1.TraceService/Export',
'processor/signozlimiter/LogsProcessed',
'processor/signozlimiter/MetricsProcessed',
'processor/signozlimiter/TracesProcessed',
'receiver/otlp/LogsReceived',
'receiver/otlp/MetricsReceived',
'receiver/otlp/TraceDataReceived',
'receiver/signozhttplog/heroku/LogsReceived',
'receiver/signozhttplog/json/LogsReceived',
'receiver/signozhttplog/vector/LogsReceived',
'redis.dial',
'redis.pipeline eval',
'sadd',
'set',
'sismember',
],
showALLOption: true,
sort: 'ASC',
textboxValue: '',
type: 'QUERY',
},
{
key: '5e8a3cd9-3cd9-42df-a76c-79471a0f75bd',
name: 'http_status_code',
customValue: '',
description: '',
id: '5e8a3cd9-3cd9-42df-a76c-79471a0f75bd',
modificationUUID: '9a4021cc-a80a-4f15-8899-78892b763ca7',
multiSelect: true,
order: 3,
queryValue:
"SELECT DISTINCT JSONExtractString(labels, 'http_status_code') FROM signoz_metrics.distributed_time_series_v4_1day\n WHERE metric_name = 'signoz_calls_total' AND JSONExtractString(labels, 'operation') IN {{.endpoint}}",
showALLOption: true,
sort: 'ASC',
textboxValue: '',
type: 'QUERY',
selectedValue: ['', '200', '301', '400', '401', '405', '415', '429'],
allSelected: true,
},
{
key: '48e9aa64-05ca-41c2-a1bd-6c8aeca659f1',
name: 'k8s_cluster_name',
allSelected: false,
customValue: 'test-1,\ntest-2,\ntest-3',
description: '',
id: '48e9aa64-05ca-41c2-a1bd-6c8aeca659f1',
modificationUUID: '44722322-368c-4613-bb7f-d0b12867d57a',
multiSelect: false,
order: 4,
queryValue:
"SELECT JSONExtractString(labels, 'k8s_cluster_name') AS k8s_cluster_name\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'k8s_node_cpu_time'\nGROUP BY k8s_cluster_name",
selectedValue: 'saasmonitor-cluster',
showALLOption: false,
sort: 'DISABLED',
textboxValue: '',
type: 'QUERY',
},
{
key: '3ea18ba2-30cf-4220-b03b-720b5eaf35f8',
name: 'environment',
allSelected: false,
customValue: '',
description: '',
id: '3ea18ba2-30cf-4220-b03b-720b5eaf35f8',
modificationUUID: '9f76cb06-1b9f-460f-a174-0b210bb3cf93',
multiSelect: false,
order: 5,
queryValue:
"SELECT DISTINCT JSONExtractString(labels, 'deployment_environment') AS environment\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'signoz_calls_total'",
selectedValue: 'production',
showALLOption: false,
sort: 'DISABLED',
textboxValue: '',
type: 'QUERY',
},
{
key: '4d71d385-beaf-4434-8dbf-c62be68049fc',
name: 'k8s_node_name',
allSelected: false,
customValue: '',
description: '',
id: '4d71d385-beaf-4434-8dbf-c62be68049fc',
modificationUUID: '77233d3c-96d7-4ccb-aa9d-11b04d563068',
multiSelect: false,
order: 6,
queryValue:
"SELECT JSONExtractString(labels, 'k8s_node_name') AS k8s_node_name\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'k8s_node_cpu_time' AND JSONExtractString(labels, 'k8s_cluster_name') = {{.k8s_cluster_name}}\nGROUP BY k8s_node_name",
selectedValue: 'gke-signoz-saas-si-consumer-bsc-e2sd4-a6d430fa-gvm2',
showALLOption: false,
sort: 'DISABLED',
textboxValue: '',
type: 'QUERY',
},
{
key: '937ecbae-b24b-4d6d-8cc4-5d5b8d53569b',
name: 'k8s_namespace_name',
customValue: '',
description: '',
id: '937ecbae-b24b-4d6d-8cc4-5d5b8d53569b',
modificationUUID: '8ad2442d-8b4d-4c64-848e-af847d1d0eec',
multiSelect: false,
order: 7,
queryValue:
"SELECT JSONExtractString(labels, 'k8s_namespace_name') AS k8s_namespace_name\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'k8s_pod_cpu_time' AND JSONExtractString(labels, 'k8s_cluster_name') = {{.k8s_cluster_name}} AND JSONExtractString(labels, 'k8s_node_name') IN {{.k8s_node_name}}\nGROUP BY k8s_namespace_name",
showALLOption: false,
sort: 'DISABLED',
textboxValue: '',
type: 'QUERY',
selectedValue: 'saasmonitor',
allSelected: false,
},
] as any,
};

View File

@@ -1,3 +1,4 @@
import { isEmpty } from 'lodash-es';
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
export function areArraysEqual(
@@ -29,3 +30,159 @@ export const convertVariablesToDbFormat = (
result[id] = obj;
return result;
}, {});
const getDependentVariables = (queryValue: string): string[] => {
const variableRegexPattern = /\{\{\s*?\.([^\s}]+)\s*?\}\}/g;
const matches = queryValue.match(variableRegexPattern);
// Extract variable names from the matches array without {{ . }}
return matches
? matches.map((match) => match.replace(variableRegexPattern, '$1'))
: [];
};
export type VariableGraph = Record<string, string[]>;
export const buildDependencies = (
variables: IDashboardVariable[],
): VariableGraph => {
console.log('buildDependencies', variables);
const graph: VariableGraph = {};
// Initialize empty arrays for all variables first
variables.forEach((variable) => {
if (variable.name) {
graph[variable.name] = [];
}
});
// For each QUERY variable, add it as a dependent to its referenced variables
variables.forEach((variable) => {
if (variable.type === 'QUERY' && variable.name) {
const dependentVariables = getDependentVariables(variable.queryValue || '');
// For each referenced variable, add the current query as a dependent
dependentVariables.forEach((referencedVar) => {
if (graph[referencedVar]) {
graph[referencedVar].push(variable.name as string);
} else {
graph[referencedVar] = [variable.name as string];
}
});
}
});
return graph;
};
// Function to build the dependency graph
export const buildDependencyGraph = (
dependencies: VariableGraph,
): { order: string[]; graph: VariableGraph } => {
const inDegree: Record<string, number> = {};
const adjList: VariableGraph = {};
// Initialize in-degree and adjacency list
Object.keys(dependencies).forEach((node) => {
if (!inDegree[node]) inDegree[node] = 0;
if (!adjList[node]) adjList[node] = [];
dependencies[node].forEach((child) => {
if (!inDegree[child]) inDegree[child] = 0;
inDegree[child]++;
adjList[node].push(child);
});
});
// Topological sort using Kahn's Algorithm
const queue: string[] = Object.keys(inDegree).filter(
(node) => inDegree[node] === 0,
);
const topologicalOrder: string[] = [];
while (queue.length > 0) {
const current = queue.shift();
if (current === undefined) {
break;
}
topologicalOrder.push(current);
adjList[current].forEach((neighbor) => {
inDegree[neighbor]--;
if (inDegree[neighbor] === 0) queue.push(neighbor);
});
}
if (topologicalOrder.length !== Object.keys(dependencies).length) {
throw new Error('Cycle detected in the dependency graph!');
}
return { order: topologicalOrder, graph: adjList };
};
export const onUpdateVariableNode = (
nodeToUpdate: string,
graph: VariableGraph,
topologicalOrder: string[],
callback: (node: string) => void,
): void => {
const visited = new Set<string>();
// Start processing from the node to update
topologicalOrder.forEach((node) => {
if (node === nodeToUpdate || visited.has(node)) {
visited.add(node);
callback(node);
(graph[node] || []).forEach((child) => {
visited.add(child);
});
}
});
};
export const buildParentDependencyGraph = (
graph: VariableGraph,
): VariableGraph => {
const parentGraph: VariableGraph = {};
// Initialize empty arrays for all nodes
Object.keys(graph).forEach((node) => {
parentGraph[node] = [];
});
// For each node and its children in the original graph
Object.entries(graph).forEach(([node, children]) => {
// For each child, add the current node as its parent
children.forEach((child) => {
parentGraph[child].push(node);
});
});
return parentGraph;
};
export const checkAPIInvocation = (
variablesToGetUpdated: string[],
variableData: IDashboardVariable,
parentDependencyGraph?: VariableGraph,
): boolean => {
if (isEmpty(variableData.name)) {
return false;
}
if (isEmpty(parentDependencyGraph)) {
return true;
}
// if no dependency then true
const haveDependency =
parentDependencyGraph?.[variableData.name || '']?.length > 0;
if (!haveDependency) {
return true;
}
// if variable is in the list and has dependency then check if its the top element in the queue then true else false
return (
variablesToGetUpdated.length > 0 &&
variablesToGetUpdated[0] === variableData.name
);
};

View File

@@ -48,7 +48,7 @@ builder.Services.AddOpenTelemetry()
otlpOptions.Protocol = OtlpExportProtocol.Grpc;
//SigNoz Cloud account Ingestion key
string headerKey = "signoz-access-token";
string headerKey = "signoz-ingestion-key";
string headerValue = "{{SIGNOZ_INGESTION_KEY}}";
string formattedHeader = $"{headerKey}={headerValue}";

View File

@@ -48,7 +48,7 @@ builder.Services.AddOpenTelemetry()
otlpOptions.Protocol = OtlpExportProtocol.Grpc;
//SigNoz Cloud account Ingestion key
string headerKey = "signoz-access-token";
string headerKey = "signoz-ingestion-key";
string headerValue = "{{SIGNOZ_INGESTION_KEY}}";
string formattedHeader = $"{headerKey}={headerValue}";

View File

@@ -68,7 +68,7 @@ exporters:
tls:
insecure: false
headers:
"signoz-access-token": "{{SIGNOZ_INGESTION_KEY}}"
"signoz-ingestion-key": "{{SIGNOZ_INGESTION_KEY}}"
logging:
verbosity: normal
service:

View File

@@ -48,7 +48,7 @@ builder.Services.AddOpenTelemetry()
otlpOptions.Protocol = OtlpExportProtocol.Grpc;
//SigNoz Cloud account Ingestion key
string headerKey = "signoz-access-token";
string headerKey = "signoz-ingestion-key";
string headerValue = "{{SIGNOZ_INGESTION_KEY}}";
string formattedHeader = $"{headerKey}={headerValue}";

View File

@@ -69,7 +69,7 @@ exporters:
tls:
insecure: false
headers:
"signoz-access-token": "{{SIGNOZ_INGESTION_KEY}}"
"signoz-ingestion-key": "{{SIGNOZ_INGESTION_KEY}}"
logging:
verbosity: normal
service:

View File

@@ -48,7 +48,7 @@ builder.Services.AddOpenTelemetry()
otlpOptions.Protocol = OtlpExportProtocol.Grpc;
//SigNoz Cloud account Ingestion key
string headerKey = "signoz-access-token";
string headerKey = "signoz-ingestion-key";
string headerValue = "{{SIGNOZ_INGESTION_KEY}}";
string formattedHeader = $"{headerKey}={headerValue}";

View File

@@ -67,7 +67,7 @@ exporters:
tls:
insecure: false
headers:
"signoz-access-token": "{{SIGNOZ_INGESTION_KEY}}"
"signoz-ingestion-key": "{{SIGNOZ_INGESTION_KEY}}"
logging:
verbosity: normal
service:

View File

@@ -48,7 +48,7 @@ builder.Services.AddOpenTelemetry()
otlpOptions.Protocol = OtlpExportProtocol.Grpc;
//SigNoz Cloud account Ingestion key
string headerKey = "signoz-access-token";
string headerKey = "signoz-ingestion-key";
string headerValue = "{{SIGNOZ_INGESTION_KEY}}";
string formattedHeader = $"{headerKey}={headerValue}";

View File

@@ -68,7 +68,7 @@ exporters:
tls:
insecure: false
headers:
"signoz-access-token": "{{SIGNOZ_INGESTION_KEY}}"
"signoz-ingestion-key": "{{SIGNOZ_INGESTION_KEY}}"
logging:
verbosity: normal
service:

View File

@@ -37,7 +37,7 @@ builder.Services.AddOpenTelemetry()
otlpOptions.Protocol = OtlpExportProtocol.Grpc;
//SigNoz Cloud account Ingestion key
string headerKey = "signoz-access-token";
string headerKey = "signoz-ingestion-key";
string headerValue = "{{SIGNOZ_INGESTION_KEY}}";
string formattedHeader = $"{headerKey}={headerValue}";

View File

@@ -53,7 +53,7 @@ config :opentelemetry, :processors,
%{
endpoints: ["https://ingest.{{REGION}}.signoz.cloud:443"],
headers: [
{"signoz-access-token", {{SIGNOZ_ACCESS_TOKEN}} }
{"signoz-ingestion-key", {{SIGNOZ_ACCESS_TOKEN}} }
]
}
}

View File

@@ -53,7 +53,7 @@ config :opentelemetry, :processors,
%{
endpoints: ["https://ingest.{{REGION}}.signoz.cloud:443"],
headers: [
{"signoz-access-token", {{SIGNOZ_ACCESS_TOKEN}} }
{"signoz-ingestion-key", {{SIGNOZ_ACCESS_TOKEN}} }
]
}
}

View File

@@ -66,7 +66,7 @@ exporters:
tls:
insecure: false
headers:
"signoz-access-token": "{{SIGNOZ_INGESTION_KEY}}"
"signoz-ingestion-key": "{{SIGNOZ_INGESTION_KEY}}"
logging:
verbosity: normal
service:

View File

@@ -53,7 +53,7 @@ config :opentelemetry, :processors,
%{
endpoints: ["https://ingest.{{REGION}}.signoz.cloud:443"],
headers: [
{"signoz-access-token", {{SIGNOZ_ACCESS_TOKEN}} }
{"signoz-ingestion-key", {{SIGNOZ_ACCESS_TOKEN}} }
]
}
}

View File

@@ -66,7 +66,7 @@ exporters:
tls:
insecure: false
headers:
"signoz-access-token": "{{SIGNOZ_INGESTION_KEY}}"
"signoz-ingestion-key": "{{SIGNOZ_INGESTION_KEY}}"
logging:
verbosity: normal
service:

View File

@@ -53,7 +53,7 @@ config :opentelemetry, :processors,
%{
endpoints: ["https://ingest.{{REGION}}.signoz.cloud:443"],
headers: [
{"signoz-access-token", {{SIGNOZ_ACCESS_TOKEN}} }
{"signoz-ingestion-key", {{SIGNOZ_ACCESS_TOKEN}} }
]
}
}

View File

@@ -66,7 +66,7 @@ exporters:
tls:
insecure: false
headers:
"signoz-access-token": "{{SIGNOZ_INGESTION_KEY}}"
"signoz-ingestion-key": "{{SIGNOZ_INGESTION_KEY}}"
logging:
verbosity: normal
service:

View File

@@ -53,7 +53,7 @@ config :opentelemetry, :processors,
%{
endpoints: ["https://ingest.{{REGION}}.signoz.cloud:443"],
headers: [
{"signoz-access-token", {{SIGNOZ_ACCESS_TOKEN}} }
{"signoz-ingestion-key", {{SIGNOZ_ACCESS_TOKEN}} }
]
}
}

View File

@@ -66,7 +66,7 @@ exporters:
tls:
insecure: false
headers:
"signoz-access-token": "{{SIGNOZ_INGESTION_KEY}}"
"signoz-ingestion-key": "{{SIGNOZ_INGESTION_KEY}}"
logging:
verbosity: normal
service:

View File

@@ -53,7 +53,7 @@ config :opentelemetry, :processors,
%{
endpoints: ["https://ingest.{{REGION}}.signoz.cloud:443"],
headers: [
{"signoz-access-token", {{SIGNOZ_ACCESS_TOKEN}} }
{"signoz-ingestion-key", {{SIGNOZ_ACCESS_TOKEN}} }
]
}
}

View File

@@ -75,7 +75,7 @@ exporters:
tls:
insecure: false
headers:
"signoz-access-token": "{{SIGNOZ_INGESTION_KEY}}"
"signoz-ingestion-key": "{{SIGNOZ_INGESTION_KEY}}"
logging:
verbosity: normal
service:

View File

@@ -134,7 +134,7 @@ From VMs, there are two ways to send data to SigNoz Cloud.
The run command must have some environment variables to send data to SigNoz cloud. The run command:
```bash
SERVICE_NAME={{MYAPP}} INSECURE_MODE=false OTEL_EXPORTER_OTLP_HEADERS=signoz-access-token={{SIGNOZ_INGESTION_KEY}} OTEL_EXPORTER_OTLP_ENDPOINT=ingest.{{REGION}}.signoz.cloud:443 go run main.go
SERVICE_NAME={{MYAPP}} INSECURE_MODE=false OTEL_EXPORTER_OTLP_HEADERS=signoz-ingestion-key={{SIGNOZ_INGESTION_KEY}} OTEL_EXPORTER_OTLP_ENDPOINT=ingest.{{REGION}}.signoz.cloud:443 go run main.go
```
If you want to update your `service_name`, you can modify the `SERVICE_NAME` variable.

View File

@@ -128,7 +128,7 @@ Set the environment variables in your Dockerfile.
# Set environment variables
ENV SERVICE_NAME={{MYAPP}} \
INSECURE_MODE=false \
OTEL_EXPORTER_OTLP_HEADERS="signoz-access-token=b{{SIGNOZ_INGESTION_KEY}}" \
OTEL_EXPORTER_OTLP_HEADERS="signoz-ingestion-key=b{{SIGNOZ_INGESTION_KEY}}" \
OTEL_EXPORTER_OTLP_ENDPOINT=ingest.{{REGION}}.signoz.cloud:443
...
```

View File

@@ -2,5 +2,5 @@
To run your Go Gin application, use the below command :
```bash
SERVICE_NAME={{MYAPP}} INSECURE_MODE=false OTEL_EXPORTER_OTLP_HEADERS=signoz-access-token={{SIGNOZ_INGESTION_KEY}} OTEL_EXPORTER_OTLP_ENDPOINT=ingest.{{REGION}}.signoz.cloud:443 go run main.go
SERVICE_NAME={{MYAPP}} INSECURE_MODE=false OTEL_EXPORTER_OTLP_HEADERS=signoz-ingestion-key={{SIGNOZ_INGESTION_KEY}} OTEL_EXPORTER_OTLP_ENDPOINT=ingest.{{REGION}}.signoz.cloud:443 go run main.go
```

View File

@@ -66,7 +66,7 @@ exporters:
tls:
insecure: false
headers:
"signoz-access-token": "{{SIGNOZ_INGESTION_KEY}}"
"signoz-ingestion-key": "{{SIGNOZ_INGESTION_KEY}}"
logging:
verbosity: normal
service:

View File

@@ -2,5 +2,5 @@
To run your Go Gin application, use the below command :
```bash
SERVICE_NAME={{MYAPP}} INSECURE_MODE=false OTEL_EXPORTER_OTLP_HEADERS=signoz-access-token={{SIGNOZ_INGESTION_KEY}} OTEL_EXPORTER_OTLP_ENDPOINT=ingest.{{REGION}}.signoz.cloud:443 go run main.go
SERVICE_NAME={{MYAPP}} INSECURE_MODE=false OTEL_EXPORTER_OTLP_HEADERS=signoz-ingestion-key={{SIGNOZ_INGESTION_KEY}} OTEL_EXPORTER_OTLP_ENDPOINT=ingest.{{REGION}}.signoz.cloud:443 go run main.go
```

View File

@@ -69,7 +69,7 @@ exporters:
tls:
insecure: false
headers:
"signoz-access-token": "{{SIGNOZ_INGESTION_KEY}}"
"signoz-ingestion-key": "{{SIGNOZ_INGESTION_KEY}}"
logging:
verbosity: normal
service:

View File

@@ -2,5 +2,5 @@
To run your Go Gin application, use the below command :
```bash
SERVICE_NAME={{MYAPP}} INSECURE_MODE=false OTEL_EXPORTER_OTLP_HEADERS=signoz-access-token={{SIGNOZ_INGESTION_KEY}} OTEL_EXPORTER_OTLP_ENDPOINT=ingest.{{REGION}}.signoz.cloud:443 go run main.go
SERVICE_NAME={{MYAPP}} INSECURE_MODE=false OTEL_EXPORTER_OTLP_HEADERS=signoz-ingestion-key={{SIGNOZ_INGESTION_KEY}} OTEL_EXPORTER_OTLP_ENDPOINT=ingest.{{REGION}}.signoz.cloud:443 go run main.go
```

View File

@@ -66,7 +66,7 @@ exporters:
tls:
insecure: false
headers:
"signoz-access-token": "{{SIGNOZ_INGESTION_KEY}}"
"signoz-ingestion-key": "{{SIGNOZ_INGESTION_KEY}}"
logging:
verbosity: normal
service:

View File

@@ -2,5 +2,5 @@
To run your Go Gin application, use the below command :
```bash
SERVICE_NAME={{MYAPP}} INSECURE_MODE=false OTEL_EXPORTER_OTLP_HEADERS=signoz-access-token={{SIGNOZ_INGESTION_KEY}} OTEL_EXPORTER_OTLP_ENDPOINT=ingest.{{REGION}}.signoz.cloud:443 go run main.go
SERVICE_NAME={{MYAPP}} INSECURE_MODE=false OTEL_EXPORTER_OTLP_HEADERS=signoz-ingestion-key={{SIGNOZ_INGESTION_KEY}} OTEL_EXPORTER_OTLP_ENDPOINT=ingest.{{REGION}}.signoz.cloud:443 go run main.go
```

View File

@@ -64,7 +64,7 @@ exporters:
tls:
insecure: false
headers:
"signoz-access-token": "{{SIGNOZ_INGESTION_KEY}}"
"signoz-ingestion-key": "{{SIGNOZ_INGESTION_KEY}}"
logging:
verbosity: normal
service:

View File

@@ -22,7 +22,7 @@
var (
serviceName = "{{MYAPP}}")
collectorURL = "https://ingest.{{REGION}}.signoz.cloud:443"
headers="signoz-access-token={{SIGNOZ_INGESTION_KEY}}"
headers="signoz-ingestion-key={{SIGNOZ_INGESTION_KEY}}"
insecure = os.Getenv("INSECURE_MODE")
)
```

View File

@@ -18,7 +18,7 @@ RUN wget https://github.com/open-telemetry/opentelemetry-java-instrumentation/re
# Open and update the configuration file
RUN sed -i 's/\(JAVA_OPTS=".*\)/\1 -javaagent:\/opt\/jboss-eap-7.1\/opentelemetry-javaagent.jar \
-Dotel.exporter.otlp.endpoint=https:\/\/ingest.{{REGION}}.signoz.cloud:443 \
-Dotel.exporter.otlp.headers="signoz-access-token={{SIGNOZ_INGESTION_KEY}}" \
-Dotel.exporter.otlp.headers="signoz-ingestion-key={{SIGNOZ_INGESTION_KEY}}" \
-Dotel.resource.attributes="service.name={{MYAPP}}"/' /opt/jboss-eap-7.1/bin/standalone.conf
...
```

View File

@@ -10,7 +10,7 @@ Update `JAVA_OPTS` environment variable with configurations required to send dat
```bash
JAVA_OPTS="-javaagent:/<path>/opentelemetry-javaagent.jar
-Dotel.exporter.otlp.endpoint=https://ingest.{{REGION}}.signoz.cloud:443
-Dotel.exporter.otlp.headers="signoz-access-token={{SIGNOZ_INGESTION_KEY}}"
-Dotel.exporter.otlp.headers="signoz-ingestion-key={{SIGNOZ_INGESTION_KEY}}"
-Dotel.resource.attributes="service.name={{MYAPP}}""
```

View File

@@ -66,7 +66,7 @@ exporters:
tls:
insecure: false
headers:
"signoz-access-token": "{{SIGNOZ_INGESTION_KEY}}"
"signoz-ingestion-key": "{{SIGNOZ_INGESTION_KEY}}"
logging:
verbosity: normal
service:

View File

@@ -10,7 +10,7 @@ Update `JAVA_OPTS` environment variable with configurations required to send dat
```bash
JAVA_OPTS="-javaagent:/<path>/opentelemetry-javaagent.jar
-Dotel.exporter.otlp.endpoint=https://ingest.{{REGION}}.signoz.cloud:443
-Dotel.exporter.otlp.headers="signoz-access-token={{SIGNOZ_INGESTION_KEY}}"
-Dotel.exporter.otlp.headers="signoz-ingestion-key={{SIGNOZ_INGESTION_KEY}}"
-Dotel.resource.attributes="service.name={{MYAPP}}""
```

View File

@@ -66,7 +66,7 @@ exporters:
tls:
insecure: false
headers:
"signoz-access-token": "{{SIGNOZ_INGESTION_KEY}}"
"signoz-ingestion-key": "{{SIGNOZ_INGESTION_KEY}}"
logging:
verbosity: normal
service:

View File

@@ -10,7 +10,7 @@ Update `JAVA_OPTS` environment variable with configurations required to send dat
```bash
JAVA_OPTS="-javaagent:/<path>/opentelemetry-javaagent.jar
-Dotel.exporter.otlp.endpoint=https://ingest.{{REGION}}.signoz.cloud:443
-Dotel.exporter.otlp.headers="signoz-access-token={{SIGNOZ_INGESTION_KEY}}"
-Dotel.exporter.otlp.headers="signoz-ingestion-key={{SIGNOZ_INGESTION_KEY}}"
-Dotel.resource.attributes="service.name={{MYAPP}}""
```

View File

@@ -66,7 +66,7 @@ exporters:
tls:
insecure: false
headers:
"signoz-access-token": "{{SIGNOZ_INGESTION_KEY}}"
"signoz-ingestion-key": "{{SIGNOZ_INGESTION_KEY}}"
logging:
verbosity: normal
service:

View File

@@ -10,7 +10,7 @@ Update `JAVA_OPTS` environment variable with configurations required to send dat
```bash
JAVA_OPTS="-javaagent:/<path>/opentelemetry-javaagent.jar
-Dotel.exporter.otlp.endpoint=https://ingest.{{REGION}}.signoz.cloud:443
-Dotel.exporter.otlp.headers="signoz-access-token={{SIGNOZ_INGESTION_KEY}}"
-Dotel.exporter.otlp.headers="signoz-ingestion-key={{SIGNOZ_INGESTION_KEY}}"
-Dotel.resource.attributes="service.name={{MYAPP}}""
```

View File

@@ -66,7 +66,7 @@ exporters:
tls:
insecure: false
headers:
"signoz-access-token": "{{SIGNOZ_INGESTION_KEY}}"
"signoz-ingestion-key": "{{SIGNOZ_INGESTION_KEY}}"
logging:
verbosity: normal
service:

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