Merge branch 'develop' into chore/send-language-service-as-list

This commit is contained in:
Vishal Sharma 2024-03-28 21:40:57 +05:30 committed by GitHub
commit 6ac938f2a6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
454 changed files with 20548 additions and 3080 deletions

View File

@ -133,7 +133,7 @@ services:
# - ./data/clickhouse-3/:/var/lib/clickhouse/ # - ./data/clickhouse-3/:/var/lib/clickhouse/
alertmanager: alertmanager:
image: signoz/alertmanager:0.23.4 image: signoz/alertmanager:0.23.5
volumes: volumes:
- ./data/alertmanager:/data - ./data/alertmanager:/data
command: command:

View File

@ -54,7 +54,7 @@ services:
alertmanager: alertmanager:
container_name: signoz-alertmanager container_name: signoz-alertmanager
image: signoz/alertmanager:0.23.4 image: signoz/alertmanager:0.23.5
volumes: volumes:
- ./data/alertmanager:/data - ./data/alertmanager:/data
depends_on: depends_on:

View File

@ -149,7 +149,7 @@ services:
# - ./user_scripts:/var/lib/clickhouse/user_scripts/ # - ./user_scripts:/var/lib/clickhouse/user_scripts/
alertmanager: alertmanager:
image: signoz/alertmanager:${ALERTMANAGER_TAG:-0.23.4} image: signoz/alertmanager:${ALERTMANAGER_TAG:-0.23.5}
container_name: signoz-alertmanager container_name: signoz-alertmanager
volumes: volumes:
- ./data/alertmanager:/data - ./data/alertmanager:/data

View File

@ -10,6 +10,7 @@ import (
"go.signoz.io/signoz/ee/query-service/license" "go.signoz.io/signoz/ee/query-service/license"
"go.signoz.io/signoz/ee/query-service/usage" "go.signoz.io/signoz/ee/query-service/usage"
baseapp "go.signoz.io/signoz/pkg/query-service/app" baseapp "go.signoz.io/signoz/pkg/query-service/app"
"go.signoz.io/signoz/pkg/query-service/app/integrations"
"go.signoz.io/signoz/pkg/query-service/app/logparsingpipeline" "go.signoz.io/signoz/pkg/query-service/app/logparsingpipeline"
"go.signoz.io/signoz/pkg/query-service/cache" "go.signoz.io/signoz/pkg/query-service/cache"
baseint "go.signoz.io/signoz/pkg/query-service/interfaces" baseint "go.signoz.io/signoz/pkg/query-service/interfaces"
@ -31,6 +32,7 @@ type APIHandlerOptions struct {
UsageManager *usage.Manager UsageManager *usage.Manager
FeatureFlags baseint.FeatureLookup FeatureFlags baseint.FeatureLookup
LicenseManager *license.Manager LicenseManager *license.Manager
IntegrationsController *integrations.Controller
LogsParsingPipelineController *logparsingpipeline.LogParsingPipelineController LogsParsingPipelineController *logparsingpipeline.LogParsingPipelineController
Cache cache.Cache Cache cache.Cache
// Querier Influx Interval // Querier Influx Interval
@ -56,6 +58,7 @@ func NewAPIHandler(opts APIHandlerOptions) (*APIHandler, error) {
AppDao: opts.AppDao, AppDao: opts.AppDao,
RuleManager: opts.RulesManager, RuleManager: opts.RulesManager,
FeatureFlags: opts.FeatureFlags, FeatureFlags: opts.FeatureFlags,
IntegrationsController: opts.IntegrationsController,
LogsParsingPipelineController: opts.LogsParsingPipelineController, LogsParsingPipelineController: opts.LogsParsingPipelineController,
Cache: opts.Cache, Cache: opts.Cache,
FluxInterval: opts.FluxInterval, FluxInterval: opts.FluxInterval,

View File

@ -74,7 +74,7 @@ func (ah *APIHandler) registerUser(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close() defer r.Body.Close()
requestBody, err := io.ReadAll(r.Body) requestBody, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
zap.S().Errorf("received no input in api\n", err) zap.L().Error("received no input in api", zap.Error(err))
RespondError(w, model.BadRequest(err), nil) RespondError(w, model.BadRequest(err), nil)
return return
} }
@ -82,7 +82,7 @@ func (ah *APIHandler) registerUser(w http.ResponseWriter, r *http.Request) {
err = json.Unmarshal(requestBody, &req) err = json.Unmarshal(requestBody, &req)
if err != nil { if err != nil {
zap.S().Errorf("received invalid user registration request", zap.Error(err)) zap.L().Error("received invalid user registration request", zap.Error(err))
RespondError(w, model.BadRequest(fmt.Errorf("failed to register user")), nil) RespondError(w, model.BadRequest(fmt.Errorf("failed to register user")), nil)
return return
} }
@ -90,13 +90,13 @@ func (ah *APIHandler) registerUser(w http.ResponseWriter, r *http.Request) {
// get invite object // get invite object
invite, err := baseauth.ValidateInvite(ctx, req) invite, err := baseauth.ValidateInvite(ctx, req)
if err != nil { if err != nil {
zap.S().Errorf("failed to validate invite token", err) zap.L().Error("failed to validate invite token", zap.Error(err))
RespondError(w, model.BadRequest(err), nil) RespondError(w, model.BadRequest(err), nil)
return return
} }
if invite == nil { if invite == nil {
zap.S().Errorf("failed to validate invite token: it is either empty or invalid", err) zap.L().Error("failed to validate invite token: it is either empty or invalid", zap.Error(err))
RespondError(w, model.BadRequest(basemodel.ErrSignupFailed{}), nil) RespondError(w, model.BadRequest(basemodel.ErrSignupFailed{}), nil)
return return
} }
@ -104,7 +104,7 @@ func (ah *APIHandler) registerUser(w http.ResponseWriter, r *http.Request) {
// get auth domain from email domain // get auth domain from email domain
domain, apierr := ah.AppDao().GetDomainByEmail(ctx, invite.Email) domain, apierr := ah.AppDao().GetDomainByEmail(ctx, invite.Email)
if apierr != nil { if apierr != nil {
zap.S().Errorf("failed to get domain from email", apierr) zap.L().Error("failed to get domain from email", zap.Error(apierr))
RespondError(w, model.InternalError(basemodel.ErrSignupFailed{}), nil) RespondError(w, model.InternalError(basemodel.ErrSignupFailed{}), nil)
} }
@ -205,24 +205,24 @@ func (ah *APIHandler) receiveGoogleAuth(w http.ResponseWriter, r *http.Request)
ctx := context.Background() ctx := context.Background()
if !ah.CheckFeature(model.SSO) { if !ah.CheckFeature(model.SSO) {
zap.S().Errorf("[receiveGoogleAuth] sso requested but feature unavailable %s in org domain %s", model.SSO) zap.L().Error("[receiveGoogleAuth] sso requested but feature unavailable in org domain")
http.Redirect(w, r, fmt.Sprintf("%s?ssoerror=%s", redirectUri, "feature unavailable, please upgrade your billing plan to access this feature"), http.StatusMovedPermanently) http.Redirect(w, r, fmt.Sprintf("%s?ssoerror=%s", redirectUri, "feature unavailable, please upgrade your billing plan to access this feature"), http.StatusMovedPermanently)
return return
} }
q := r.URL.Query() q := r.URL.Query()
if errType := q.Get("error"); errType != "" { if errType := q.Get("error"); errType != "" {
zap.S().Errorf("[receiveGoogleAuth] failed to login with google auth", q.Get("error_description")) zap.L().Error("[receiveGoogleAuth] failed to login with google auth", zap.String("error", errType), zap.String("error_description", q.Get("error_description")))
http.Redirect(w, r, fmt.Sprintf("%s?ssoerror=%s", redirectUri, "failed to login through SSO "), http.StatusMovedPermanently) http.Redirect(w, r, fmt.Sprintf("%s?ssoerror=%s", redirectUri, "failed to login through SSO "), http.StatusMovedPermanently)
return return
} }
relayState := q.Get("state") relayState := q.Get("state")
zap.S().Debug("[receiveGoogleAuth] relay state received", zap.String("state", relayState)) zap.L().Debug("[receiveGoogleAuth] relay state received", zap.String("state", relayState))
parsedState, err := url.Parse(relayState) parsedState, err := url.Parse(relayState)
if err != nil || relayState == "" { if err != nil || relayState == "" {
zap.S().Errorf("[receiveGoogleAuth] failed to process response - invalid response from IDP", err, r) zap.L().Error("[receiveGoogleAuth] failed to process response - invalid response from IDP", zap.Error(err), zap.Any("request", r))
handleSsoError(w, r, redirectUri) handleSsoError(w, r, redirectUri)
return return
} }
@ -244,14 +244,14 @@ func (ah *APIHandler) receiveGoogleAuth(w http.ResponseWriter, r *http.Request)
identity, err := callbackHandler.HandleCallback(r) identity, err := callbackHandler.HandleCallback(r)
if err != nil { if err != nil {
zap.S().Errorf("[receiveGoogleAuth] failed to process HandleCallback ", domain.String(), zap.Error(err)) zap.L().Error("[receiveGoogleAuth] failed to process HandleCallback ", zap.String("domain", domain.String()), zap.Error(err))
handleSsoError(w, r, redirectUri) handleSsoError(w, r, redirectUri)
return return
} }
nextPage, err := ah.AppDao().PrepareSsoRedirect(ctx, redirectUri, identity.Email) nextPage, err := ah.AppDao().PrepareSsoRedirect(ctx, redirectUri, identity.Email)
if err != nil { if err != nil {
zap.S().Errorf("[receiveGoogleAuth] failed to generate redirect URI after successful login ", domain.String(), zap.Error(err)) zap.L().Error("[receiveGoogleAuth] failed to generate redirect URI after successful login ", zap.String("domain", domain.String()), zap.Error(err))
handleSsoError(w, r, redirectUri) handleSsoError(w, r, redirectUri)
return return
} }
@ -266,14 +266,14 @@ func (ah *APIHandler) receiveSAML(w http.ResponseWriter, r *http.Request) {
ctx := context.Background() ctx := context.Background()
if !ah.CheckFeature(model.SSO) { if !ah.CheckFeature(model.SSO) {
zap.S().Errorf("[receiveSAML] sso requested but feature unavailable %s in org domain %s", model.SSO) zap.L().Error("[receiveSAML] sso requested but feature unavailable in org domain")
http.Redirect(w, r, fmt.Sprintf("%s?ssoerror=%s", redirectUri, "feature unavailable, please upgrade your billing plan to access this feature"), http.StatusMovedPermanently) http.Redirect(w, r, fmt.Sprintf("%s?ssoerror=%s", redirectUri, "feature unavailable, please upgrade your billing plan to access this feature"), http.StatusMovedPermanently)
return return
} }
err := r.ParseForm() err := r.ParseForm()
if err != nil { if err != nil {
zap.S().Errorf("[receiveSAML] failed to process response - invalid response from IDP", err, r) zap.L().Error("[receiveSAML] failed to process response - invalid response from IDP", zap.Error(err), zap.Any("request", r))
handleSsoError(w, r, redirectUri) handleSsoError(w, r, redirectUri)
return return
} }
@ -281,11 +281,11 @@ func (ah *APIHandler) receiveSAML(w http.ResponseWriter, r *http.Request) {
// the relay state is sent when a login request is submitted to // the relay state is sent when a login request is submitted to
// Idp. // Idp.
relayState := r.FormValue("RelayState") relayState := r.FormValue("RelayState")
zap.S().Debug("[receiveML] relay state", zap.String("relayState", relayState)) zap.L().Debug("[receiveML] relay state", zap.String("relayState", relayState))
parsedState, err := url.Parse(relayState) parsedState, err := url.Parse(relayState)
if err != nil || relayState == "" { if err != nil || relayState == "" {
zap.S().Errorf("[receiveSAML] failed to process response - invalid response from IDP", err, r) zap.L().Error("[receiveSAML] failed to process response - invalid response from IDP", zap.Error(err), zap.Any("request", r))
handleSsoError(w, r, redirectUri) handleSsoError(w, r, redirectUri)
return return
} }
@ -302,34 +302,34 @@ func (ah *APIHandler) receiveSAML(w http.ResponseWriter, r *http.Request) {
sp, err := domain.PrepareSamlRequest(parsedState) sp, err := domain.PrepareSamlRequest(parsedState)
if err != nil { if err != nil {
zap.S().Errorf("[receiveSAML] failed to prepare saml request for domain (%s): %v", domain.String(), err) zap.L().Error("[receiveSAML] failed to prepare saml request for domain", zap.String("domain", domain.String()), zap.Error(err))
handleSsoError(w, r, redirectUri) handleSsoError(w, r, redirectUri)
return return
} }
assertionInfo, err := sp.RetrieveAssertionInfo(r.FormValue("SAMLResponse")) assertionInfo, err := sp.RetrieveAssertionInfo(r.FormValue("SAMLResponse"))
if err != nil { if err != nil {
zap.S().Errorf("[receiveSAML] failed to retrieve assertion info from saml response for organization (%s): %v", domain.String(), err) zap.L().Error("[receiveSAML] failed to retrieve assertion info from saml response", zap.String("domain", domain.String()), zap.Error(err))
handleSsoError(w, r, redirectUri) handleSsoError(w, r, redirectUri)
return return
} }
if assertionInfo.WarningInfo.InvalidTime { if assertionInfo.WarningInfo.InvalidTime {
zap.S().Errorf("[receiveSAML] expired saml response for organization (%s): %v", domain.String(), err) zap.L().Error("[receiveSAML] expired saml response", zap.String("domain", domain.String()), zap.Error(err))
handleSsoError(w, r, redirectUri) handleSsoError(w, r, redirectUri)
return return
} }
email := assertionInfo.NameID email := assertionInfo.NameID
if email == "" { if email == "" {
zap.S().Errorf("[receiveSAML] invalid email in the SSO response (%s)", domain.String()) zap.L().Error("[receiveSAML] invalid email in the SSO response", zap.String("domain", domain.String()))
handleSsoError(w, r, redirectUri) handleSsoError(w, r, redirectUri)
return return
} }
nextPage, err := ah.AppDao().PrepareSsoRedirect(ctx, redirectUri, email) nextPage, err := ah.AppDao().PrepareSsoRedirect(ctx, redirectUri, email)
if err != nil { if err != nil {
zap.S().Errorf("[receiveSAML] failed to generate redirect URI after successful login ", domain.String(), zap.Error(err)) zap.L().Error("[receiveSAML] failed to generate redirect URI after successful login ", zap.String("domain", domain.String()), zap.Error(err))
handleSsoError(w, r, redirectUri) handleSsoError(w, r, redirectUri)
return return
} }

View File

@ -12,6 +12,20 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
) )
type DayWiseBreakdown struct {
Type string `json:"type"`
Breakdown []DayWiseData `json:"breakdown"`
}
type DayWiseData struct {
Timestamp int64 `json:"timestamp"`
Count float64 `json:"count"`
Size float64 `json:"size"`
UnitPrice float64 `json:"unitPrice"`
Quantity float64 `json:"quantity"`
Total float64 `json:"total"`
}
type tierBreakdown struct { type tierBreakdown struct {
UnitPrice float64 `json:"unitPrice"` UnitPrice float64 `json:"unitPrice"`
Quantity float64 `json:"quantity"` Quantity float64 `json:"quantity"`
@ -21,9 +35,10 @@ type tierBreakdown struct {
} }
type usageResponse struct { type usageResponse struct {
Type string `json:"type"` Type string `json:"type"`
Unit string `json:"unit"` Unit string `json:"unit"`
Tiers []tierBreakdown `json:"tiers"` Tiers []tierBreakdown `json:"tiers"`
DayWiseBreakdown DayWiseBreakdown `json:"dayWiseBreakdown"`
} }
type details struct { type details struct {
@ -176,7 +191,7 @@ func (ah *APIHandler) listLicensesV2(w http.ResponseWriter, r *http.Request) {
url := fmt.Sprintf("%s/trial?licenseKey=%s", constants.LicenseSignozIo, currentActiveLicenseKey) url := fmt.Sprintf("%s/trial?licenseKey=%s", constants.LicenseSignozIo, currentActiveLicenseKey)
req, err := http.NewRequest("GET", url, nil) req, err := http.NewRequest("GET", url, nil)
if err != nil { if err != nil {
zap.S().Error("Error while creating request for trial details", err) zap.L().Error("Error while creating request for trial details", zap.Error(err))
// If there is an error in fetching trial details, we will still return the license details // If there is an error in fetching trial details, we will still return the license details
// to avoid blocking the UI // to avoid blocking the UI
ah.Respond(w, resp) ah.Respond(w, resp)
@ -185,7 +200,7 @@ func (ah *APIHandler) listLicensesV2(w http.ResponseWriter, r *http.Request) {
req.Header.Add("X-SigNoz-SecretKey", constants.LicenseAPIKey) req.Header.Add("X-SigNoz-SecretKey", constants.LicenseAPIKey)
trialResp, err := hClient.Do(req) trialResp, err := hClient.Do(req)
if err != nil { if err != nil {
zap.S().Error("Error while fetching trial details", err) zap.L().Error("Error while fetching trial details", zap.Error(err))
// If there is an error in fetching trial details, we will still return the license details // If there is an error in fetching trial details, we will still return the license details
// to avoid incorrectly blocking the UI // to avoid incorrectly blocking the UI
ah.Respond(w, resp) ah.Respond(w, resp)
@ -196,7 +211,7 @@ func (ah *APIHandler) listLicensesV2(w http.ResponseWriter, r *http.Request) {
trialRespBody, err := io.ReadAll(trialResp.Body) trialRespBody, err := io.ReadAll(trialResp.Body)
if err != nil || trialResp.StatusCode != http.StatusOK { if err != nil || trialResp.StatusCode != http.StatusOK {
zap.S().Error("Error while fetching trial details", err) zap.L().Error("Error while fetching trial details", zap.Error(err))
// If there is an error in fetching trial details, we will still return the license details // If there is an error in fetching trial details, we will still return the license details
// to avoid incorrectly blocking the UI // to avoid incorrectly blocking the UI
ah.Respond(w, resp) ah.Respond(w, resp)
@ -207,7 +222,7 @@ func (ah *APIHandler) listLicensesV2(w http.ResponseWriter, r *http.Request) {
var trialRespData model.SubscriptionServerResp var trialRespData model.SubscriptionServerResp
if err := json.Unmarshal(trialRespBody, &trialRespData); err != nil { if err := json.Unmarshal(trialRespBody, &trialRespData); err != nil {
zap.S().Error("Error while decoding trial details", err) zap.L().Error("Error while decoding trial details", zap.Error(err))
// If there is an error in fetching trial details, we will still return the license details // If there is an error in fetching trial details, we will still return the license details
// to avoid incorrectly blocking the UI // to avoid incorrectly blocking the UI
ah.Respond(w, resp) ah.Respond(w, resp)

View File

@ -18,14 +18,14 @@ import (
func (ah *APIHandler) queryRangeMetricsV2(w http.ResponseWriter, r *http.Request) { func (ah *APIHandler) queryRangeMetricsV2(w http.ResponseWriter, r *http.Request) {
if !ah.CheckFeature(basemodel.CustomMetricsFunction) { if !ah.CheckFeature(basemodel.CustomMetricsFunction) {
zap.S().Info("CustomMetricsFunction feature is not enabled in this plan") zap.L().Info("CustomMetricsFunction feature is not enabled in this plan")
ah.APIHandler.QueryRangeMetricsV2(w, r) ah.APIHandler.QueryRangeMetricsV2(w, r)
return return
} }
metricsQueryRangeParams, apiErrorObj := parser.ParseMetricQueryRangeParams(r) metricsQueryRangeParams, apiErrorObj := parser.ParseMetricQueryRangeParams(r)
if apiErrorObj != nil { if apiErrorObj != nil {
zap.S().Errorf(apiErrorObj.Err.Error()) zap.L().Error("Error in parsing metric query params", zap.Error(apiErrorObj.Err))
RespondError(w, apiErrorObj, nil) RespondError(w, apiErrorObj, nil)
return return
} }

View File

@ -43,8 +43,8 @@ func (ah *APIHandler) createPAT(w http.ResponseWriter, r *http.Request) {
return return
} }
pat := model.PAT{ pat := model.PAT{
Name: req.Name, Name: req.Name,
Role: req.Role, Role: req.Role,
ExpiresAt: req.ExpiresInDays, ExpiresAt: req.ExpiresInDays,
} }
err = validatePATRequest(pat) err = validatePATRequest(pat)
@ -65,7 +65,7 @@ func (ah *APIHandler) createPAT(w http.ResponseWriter, r *http.Request) {
pat.ExpiresAt = time.Now().Unix() + (pat.ExpiresAt * 24 * 60 * 60) pat.ExpiresAt = time.Now().Unix() + (pat.ExpiresAt * 24 * 60 * 60)
} }
zap.S().Debugf("Got Create PAT request: %+v", pat) zap.L().Info("Got Create PAT request", zap.Any("pat", pat))
var apierr basemodel.BaseApiError var apierr basemodel.BaseApiError
if pat, apierr = ah.AppDao().CreatePAT(ctx, pat); apierr != nil { if pat, apierr = ah.AppDao().CreatePAT(ctx, pat); apierr != nil {
RespondError(w, apierr, nil) RespondError(w, apierr, nil)
@ -115,7 +115,7 @@ func (ah *APIHandler) updatePAT(w http.ResponseWriter, r *http.Request) {
req.UpdatedByUserID = user.Id req.UpdatedByUserID = user.Id
id := mux.Vars(r)["id"] id := mux.Vars(r)["id"]
req.UpdatedAt = time.Now().Unix() req.UpdatedAt = time.Now().Unix()
zap.S().Debugf("Got Update PAT request: %+v", req) zap.L().Info("Got Update PAT request", zap.Any("pat", req))
var apierr basemodel.BaseApiError var apierr basemodel.BaseApiError
if apierr = ah.AppDao().UpdatePAT(ctx, req, id); apierr != nil { if apierr = ah.AppDao().UpdatePAT(ctx, req, id); apierr != nil {
RespondError(w, apierr, nil) RespondError(w, apierr, nil)
@ -135,7 +135,7 @@ func (ah *APIHandler) getPATs(w http.ResponseWriter, r *http.Request) {
}, nil) }, nil)
return return
} }
zap.S().Infof("Get PATs for user: %+v", user.Id) zap.L().Info("Get PATs for user", zap.String("user_id", user.Id))
pats, apierr := ah.AppDao().ListPATs(ctx) pats, apierr := ah.AppDao().ListPATs(ctx)
if apierr != nil { if apierr != nil {
RespondError(w, apierr, nil) RespondError(w, apierr, nil)
@ -156,7 +156,7 @@ func (ah *APIHandler) revokePAT(w http.ResponseWriter, r *http.Request) {
return return
} }
zap.S().Debugf("Revoke PAT with id: %+v", id) zap.L().Info("Revoke PAT with id", zap.String("id", id))
if apierr := ah.AppDao().RevokePAT(ctx, id, user.Id); apierr != nil { if apierr := ah.AppDao().RevokePAT(ctx, id, user.Id); apierr != nil {
RespondError(w, apierr, nil) RespondError(w, apierr, nil)
return return

View File

@ -15,7 +15,7 @@ import (
func (ah *APIHandler) searchTraces(w http.ResponseWriter, r *http.Request) { func (ah *APIHandler) searchTraces(w http.ResponseWriter, r *http.Request) {
if !ah.CheckFeature(basemodel.SmartTraceDetail) { if !ah.CheckFeature(basemodel.SmartTraceDetail) {
zap.S().Info("SmartTraceDetail feature is not enabled in this plan") zap.L().Info("SmartTraceDetail feature is not enabled in this plan")
ah.APIHandler.SearchTraces(w, r) ah.APIHandler.SearchTraces(w, r)
return return
} }
@ -26,7 +26,7 @@ func (ah *APIHandler) searchTraces(w http.ResponseWriter, r *http.Request) {
} }
spanLimit, err := strconv.Atoi(constants.SpanLimitStr) spanLimit, err := strconv.Atoi(constants.SpanLimitStr)
if err != nil { if err != nil {
zap.S().Error("Error during strconv.Atoi() on SPAN_LIMIT env variable: ", err) zap.L().Error("Error during strconv.Atoi() on SPAN_LIMIT env variable", zap.Error(err))
return return
} }
result, err := ah.opts.DataConnector.SearchTraces(r.Context(), traceId, spanId, levelUpInt, levelDownInt, spanLimit, db.SmartTraceAlgorithm) result, err := ah.opts.DataConnector.SearchTraces(r.Context(), traceId, spanId, levelUpInt, levelDownInt, spanLimit, db.SmartTraceAlgorithm)

View File

@ -22,7 +22,7 @@ import (
func (r *ClickhouseReader) GetMetricResultEE(ctx context.Context, query string) ([]*basemodel.Series, string, error) { func (r *ClickhouseReader) GetMetricResultEE(ctx context.Context, query string) ([]*basemodel.Series, string, error) {
defer utils.Elapsed("GetMetricResult")() defer utils.Elapsed("GetMetricResult")()
zap.S().Infof("Executing metric result query: %s", query) zap.L().Info("Executing metric result query: ", zap.String("query", query))
var hash string var hash string
// If getSubTreeSpans function is used in the clickhouse query // If getSubTreeSpans function is used in the clickhouse query
@ -38,9 +38,8 @@ func (r *ClickhouseReader) GetMetricResultEE(ctx context.Context, query string)
} }
rows, err := r.conn.Query(ctx, query) rows, err := r.conn.Query(ctx, query)
zap.S().Debug(query)
if err != nil { if err != nil {
zap.S().Debug("Error in processing query: ", err) zap.L().Error("Error in processing query", zap.Error(err))
return nil, "", fmt.Errorf("error in processing query") return nil, "", fmt.Errorf("error in processing query")
} }
@ -117,7 +116,7 @@ func (r *ClickhouseReader) GetMetricResultEE(ctx context.Context, query string)
groupAttributes[colName] = fmt.Sprintf("%v", reflect.ValueOf(v).Elem().Int()) groupAttributes[colName] = fmt.Sprintf("%v", reflect.ValueOf(v).Elem().Int())
} }
default: default:
zap.S().Errorf("invalid var found in metric builder query result", v, colName) zap.L().Error("invalid var found in metric builder query result", zap.Any("var", v), zap.String("colName", colName))
} }
} }
sort.Strings(groupBy) sort.Strings(groupBy)
@ -140,7 +139,7 @@ func (r *ClickhouseReader) GetMetricResultEE(ctx context.Context, query string)
} }
// err = r.conn.Exec(ctx, "DROP TEMPORARY TABLE IF EXISTS getSubTreeSpans"+hash) // err = r.conn.Exec(ctx, "DROP TEMPORARY TABLE IF EXISTS getSubTreeSpans"+hash)
// if err != nil { // if err != nil {
// zap.S().Error("Error in dropping temporary table: ", err) // zap.L().Error("Error in dropping temporary table: ", err)
// return nil, err // return nil, err
// } // }
if hash == "" { if hash == "" {
@ -152,7 +151,7 @@ func (r *ClickhouseReader) GetMetricResultEE(ctx context.Context, query string)
func (r *ClickhouseReader) getSubTreeSpansCustomFunction(ctx context.Context, query string, hash string) (string, string, error) { func (r *ClickhouseReader) getSubTreeSpansCustomFunction(ctx context.Context, query string, hash string) (string, string, error) {
zap.S().Debugf("Executing getSubTreeSpans function") zap.L().Debug("Executing getSubTreeSpans function")
// str1 := `select fromUnixTimestamp64Milli(intDiv( toUnixTimestamp64Milli ( timestamp ), 100) * 100) AS interval, toFloat64(count()) as count from (select timestamp, spanId, parentSpanId, durationNano from getSubTreeSpans(select * from signoz_traces.signoz_index_v2 where serviceName='frontend' and name='/driver.DriverService/FindNearest' and traceID='00000000000000004b0a863cb5ed7681') where name='FindDriverIDs' group by interval order by interval asc;` // str1 := `select fromUnixTimestamp64Milli(intDiv( toUnixTimestamp64Milli ( timestamp ), 100) * 100) AS interval, toFloat64(count()) as count from (select timestamp, spanId, parentSpanId, durationNano from getSubTreeSpans(select * from signoz_traces.signoz_index_v2 where serviceName='frontend' and name='/driver.DriverService/FindNearest' and traceID='00000000000000004b0a863cb5ed7681') where name='FindDriverIDs' group by interval order by interval asc;`
@ -162,28 +161,28 @@ func (r *ClickhouseReader) getSubTreeSpansCustomFunction(ctx context.Context, qu
err := r.conn.Exec(ctx, "DROP TABLE IF EXISTS getSubTreeSpans"+hash) err := r.conn.Exec(ctx, "DROP TABLE IF EXISTS getSubTreeSpans"+hash)
if err != nil { if err != nil {
zap.S().Error("Error in dropping temporary table: ", err) zap.L().Error("Error in dropping temporary table", zap.Error(err))
return query, hash, err return query, hash, err
} }
// Create temporary table to store the getSubTreeSpans() results // Create temporary table to store the getSubTreeSpans() results
zap.S().Debugf("Creating temporary table getSubTreeSpans%s", hash) zap.L().Debug("Creating temporary table getSubTreeSpans", zap.String("hash", hash))
err = r.conn.Exec(ctx, "CREATE TABLE IF NOT EXISTS "+"getSubTreeSpans"+hash+" (timestamp DateTime64(9) CODEC(DoubleDelta, LZ4), traceID FixedString(32) CODEC(ZSTD(1)), spanID String CODEC(ZSTD(1)), parentSpanID String CODEC(ZSTD(1)), rootSpanID String CODEC(ZSTD(1)), serviceName LowCardinality(String) CODEC(ZSTD(1)), name LowCardinality(String) CODEC(ZSTD(1)), rootName LowCardinality(String) CODEC(ZSTD(1)), durationNano UInt64 CODEC(T64, ZSTD(1)), kind Int8 CODEC(T64, ZSTD(1)), tagMap Map(LowCardinality(String), String) CODEC(ZSTD(1)), events Array(String) CODEC(ZSTD(2))) ENGINE = MergeTree() ORDER BY (timestamp)") err = r.conn.Exec(ctx, "CREATE TABLE IF NOT EXISTS "+"getSubTreeSpans"+hash+" (timestamp DateTime64(9) CODEC(DoubleDelta, LZ4), traceID FixedString(32) CODEC(ZSTD(1)), spanID String CODEC(ZSTD(1)), parentSpanID String CODEC(ZSTD(1)), rootSpanID String CODEC(ZSTD(1)), serviceName LowCardinality(String) CODEC(ZSTD(1)), name LowCardinality(String) CODEC(ZSTD(1)), rootName LowCardinality(String) CODEC(ZSTD(1)), durationNano UInt64 CODEC(T64, ZSTD(1)), kind Int8 CODEC(T64, ZSTD(1)), tagMap Map(LowCardinality(String), String) CODEC(ZSTD(1)), events Array(String) CODEC(ZSTD(2))) ENGINE = MergeTree() ORDER BY (timestamp)")
if err != nil { if err != nil {
zap.S().Error("Error in creating temporary table: ", err) zap.L().Error("Error in creating temporary table", zap.Error(err))
return query, hash, err return query, hash, err
} }
var getSpansSubQueryDBResponses []model.GetSpansSubQueryDBResponse var getSpansSubQueryDBResponses []model.GetSpansSubQueryDBResponse
getSpansSubQuery := subtreeInput getSpansSubQuery := subtreeInput
// Execute the subTree query // Execute the subTree query
zap.S().Debugf("Executing subTree query: %s", getSpansSubQuery) zap.L().Debug("Executing subTree query", zap.String("query", getSpansSubQuery))
err = r.conn.Select(ctx, &getSpansSubQueryDBResponses, getSpansSubQuery) err = r.conn.Select(ctx, &getSpansSubQueryDBResponses, getSpansSubQuery)
// zap.S().Info(getSpansSubQuery) // zap.L().Info(getSpansSubQuery)
if err != nil { if err != nil {
zap.S().Debug("Error in processing sql query: ", err) zap.L().Error("Error in processing sql query", zap.Error(err))
return query, hash, fmt.Errorf("Error in processing sql query") return query, hash, fmt.Errorf("Error in processing sql query")
} }
@ -196,16 +195,16 @@ func (r *ClickhouseReader) getSubTreeSpansCustomFunction(ctx context.Context, qu
if len(getSpansSubQueryDBResponses) == 0 { if len(getSpansSubQueryDBResponses) == 0 {
return query, hash, fmt.Errorf("No spans found for the given query") return query, hash, fmt.Errorf("No spans found for the given query")
} }
zap.S().Debugf("Executing query to fetch all the spans from the same TraceID: %s", modelQuery) zap.L().Debug("Executing query to fetch all the spans from the same TraceID: ", zap.String("modelQuery", modelQuery))
err = r.conn.Select(ctx, &searchScanResponses, modelQuery, getSpansSubQueryDBResponses[0].TraceID) err = r.conn.Select(ctx, &searchScanResponses, modelQuery, getSpansSubQueryDBResponses[0].TraceID)
if err != nil { if err != nil {
zap.S().Debug("Error in processing sql query: ", err) zap.L().Error("Error in processing sql query", zap.Error(err))
return query, hash, fmt.Errorf("Error in processing sql query") return query, hash, fmt.Errorf("Error in processing sql query")
} }
// Process model to fetch the spans // Process model to fetch the spans
zap.S().Debugf("Processing model to fetch the spans") zap.L().Debug("Processing model to fetch the spans")
searchSpanResponses := []basemodel.SearchSpanResponseItem{} searchSpanResponses := []basemodel.SearchSpanResponseItem{}
for _, item := range searchScanResponses { for _, item := range searchScanResponses {
var jsonItem basemodel.SearchSpanResponseItem var jsonItem basemodel.SearchSpanResponseItem
@ -218,17 +217,17 @@ func (r *ClickhouseReader) getSubTreeSpansCustomFunction(ctx context.Context, qu
} }
// Build the subtree and store all the subtree spans in temporary table getSubTreeSpans+hash // Build the subtree and store all the subtree spans in temporary table getSubTreeSpans+hash
// Use map to store pointer to the spans to avoid duplicates and save memory // Use map to store pointer to the spans to avoid duplicates and save memory
zap.S().Debugf("Building the subtree to store all the subtree spans in temporary table getSubTreeSpans%s", hash) zap.L().Debug("Building the subtree to store all the subtree spans in temporary table getSubTreeSpans", zap.String("hash", hash))
treeSearchResponse, err := getSubTreeAlgorithm(searchSpanResponses, getSpansSubQueryDBResponses) treeSearchResponse, err := getSubTreeAlgorithm(searchSpanResponses, getSpansSubQueryDBResponses)
if err != nil { if err != nil {
zap.S().Error("Error in getSubTreeAlgorithm function: ", err) zap.L().Error("Error in getSubTreeAlgorithm function", zap.Error(err))
return query, hash, err return query, hash, err
} }
zap.S().Debugf("Preparing batch to store subtree spans in temporary table getSubTreeSpans%s", hash) zap.L().Debug("Preparing batch to store subtree spans in temporary table getSubTreeSpans", zap.String("hash", hash))
statement, err := r.conn.PrepareBatch(context.Background(), fmt.Sprintf("INSERT INTO getSubTreeSpans"+hash)) statement, err := r.conn.PrepareBatch(context.Background(), fmt.Sprintf("INSERT INTO getSubTreeSpans"+hash))
if err != nil { if err != nil {
zap.S().Error("Error in preparing batch statement: ", err) zap.L().Error("Error in preparing batch statement", zap.Error(err))
return query, hash, err return query, hash, err
} }
for _, span := range treeSearchResponse { for _, span := range treeSearchResponse {
@ -251,14 +250,14 @@ func (r *ClickhouseReader) getSubTreeSpansCustomFunction(ctx context.Context, qu
span.Events, span.Events,
) )
if err != nil { if err != nil {
zap.S().Debug("Error in processing sql query: ", err) zap.L().Error("Error in processing sql query", zap.Error(err))
return query, hash, err return query, hash, err
} }
} }
zap.S().Debugf("Inserting the subtree spans in temporary table getSubTreeSpans%s", hash) zap.L().Debug("Inserting the subtree spans in temporary table getSubTreeSpans", zap.String("hash", hash))
err = statement.Send() err = statement.Send()
if err != nil { if err != nil {
zap.S().Error("Error in sending statement: ", err) zap.L().Error("Error in sending statement", zap.Error(err))
return query, hash, err return query, hash, err
} }
return query, hash, nil return query, hash, nil
@ -323,7 +322,7 @@ func getSubTreeAlgorithm(payload []basemodel.SearchSpanResponseItem, getSpansSub
spans = append(spans, span) spans = append(spans, span)
} }
zap.S().Debug("Building Tree") zap.L().Debug("Building Tree")
roots, err := buildSpanTrees(&spans) roots, err := buildSpanTrees(&spans)
if err != nil { if err != nil {
return nil, err return nil, err
@ -333,7 +332,7 @@ func getSubTreeAlgorithm(payload []basemodel.SearchSpanResponseItem, getSpansSub
// For each root, get the subtree spans // For each root, get the subtree spans
for _, getSpansSubQueryDBResponse := range getSpansSubQueryDBResponses { for _, getSpansSubQueryDBResponse := range getSpansSubQueryDBResponses {
targetSpan := &model.SpanForTraceDetails{} targetSpan := &model.SpanForTraceDetails{}
// zap.S().Debug("Building tree for span id: " + getSpansSubQueryDBResponse.SpanID + " " + strconv.Itoa(i+1) + " of " + strconv.Itoa(len(getSpansSubQueryDBResponses))) // zap.L().Debug("Building tree for span id: " + getSpansSubQueryDBResponse.SpanID + " " + strconv.Itoa(i+1) + " of " + strconv.Itoa(len(getSpansSubQueryDBResponses)))
// Search target span object in the tree // Search target span object in the tree
for _, root := range roots { for _, root := range roots {
targetSpan, err = breadthFirstSearch(root, getSpansSubQueryDBResponse.SpanID) targetSpan, err = breadthFirstSearch(root, getSpansSubQueryDBResponse.SpanID)
@ -341,7 +340,7 @@ func getSubTreeAlgorithm(payload []basemodel.SearchSpanResponseItem, getSpansSub
break break
} }
if err != nil { if err != nil {
zap.S().Error("Error during BreadthFirstSearch(): ", err) zap.L().Error("Error during BreadthFirstSearch()", zap.Error(err))
return nil, err return nil, err
} }
} }

View File

@ -49,7 +49,7 @@ func SmartTraceAlgorithm(payload []basemodel.SearchSpanResponseItem, targetSpanI
break break
} }
if err != nil { if err != nil {
zap.S().Error("Error during BreadthFirstSearch(): ", err) zap.L().Error("Error during BreadthFirstSearch()", zap.Error(err))
return nil, err return nil, err
} }
} }
@ -186,7 +186,7 @@ func buildSpanTrees(spansPtr *[]*model.SpanForTraceDetails) ([]*model.SpanForTra
// If the parent span is not found, add current span to list of roots // If the parent span is not found, add current span to list of roots
if parent == nil { if parent == nil {
// zap.S().Debug("Parent Span not found parent_id: ", span.ParentID) // zap.L().Debug("Parent Span not found parent_id: ", span.ParentID)
roots = append(roots, span) roots = append(roots, span)
span.ParentID = "" span.ParentID = ""
continue continue

View File

@ -35,6 +35,7 @@ import (
baseapp "go.signoz.io/signoz/pkg/query-service/app" baseapp "go.signoz.io/signoz/pkg/query-service/app"
"go.signoz.io/signoz/pkg/query-service/app/dashboards" "go.signoz.io/signoz/pkg/query-service/app/dashboards"
baseexplorer "go.signoz.io/signoz/pkg/query-service/app/explorer" baseexplorer "go.signoz.io/signoz/pkg/query-service/app/explorer"
"go.signoz.io/signoz/pkg/query-service/app/integrations"
"go.signoz.io/signoz/pkg/query-service/app/logparsingpipeline" "go.signoz.io/signoz/pkg/query-service/app/logparsingpipeline"
"go.signoz.io/signoz/pkg/query-service/app/opamp" "go.signoz.io/signoz/pkg/query-service/app/opamp"
opAmpModel "go.signoz.io/signoz/pkg/query-service/app/opamp/model" opAmpModel "go.signoz.io/signoz/pkg/query-service/app/opamp/model"
@ -133,7 +134,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
var reader interfaces.DataConnector var reader interfaces.DataConnector
storage := os.Getenv("STORAGE") storage := os.Getenv("STORAGE")
if storage == "clickhouse" { if storage == "clickhouse" {
zap.S().Info("Using ClickHouse as datastore ...") zap.L().Info("Using ClickHouse as datastore ...")
qb := db.NewDataConnector( qb := db.NewDataConnector(
localDB, localDB,
serverOptions.PromConfigPath, serverOptions.PromConfigPath,
@ -171,13 +172,22 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
} }
// initiate opamp // initiate opamp
_, err = opAmpModel.InitDB(baseconst.RELATIONAL_DATASOURCE_PATH) _, err = opAmpModel.InitDB(localDB)
if err != nil { if err != nil {
return nil, err return nil, err
} }
integrationsController, err := integrations.NewController(localDB)
if err != nil {
return nil, fmt.Errorf(
"couldn't create integrations controller: %w", err,
)
}
// ingestion pipelines manager // ingestion pipelines manager
logParsingPipelineController, err := logparsingpipeline.NewLogParsingPipelinesController(localDB, "sqlite") logParsingPipelineController, err := logparsingpipeline.NewLogParsingPipelinesController(
localDB, "sqlite", integrationsController.GetPipelinesForInstalledIntegrations,
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -233,6 +243,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
UsageManager: usageManager, UsageManager: usageManager,
FeatureFlags: lm, FeatureFlags: lm,
LicenseManager: lm, LicenseManager: lm,
IntegrationsController: integrationsController,
LogsParsingPipelineController: logParsingPipelineController, LogsParsingPipelineController: logParsingPipelineController,
Cache: c, Cache: c,
FluxInterval: fluxInterval, FluxInterval: fluxInterval,
@ -278,6 +289,7 @@ func (s *Server) createPrivateServer(apiHandler *api.APIHandler) (*http.Server,
r := mux.NewRouter() r := mux.NewRouter()
r.Use(baseapp.LogCommentEnricher)
r.Use(setTimeoutMiddleware) r.Use(setTimeoutMiddleware)
r.Use(s.analyticsMiddleware) r.Use(s.analyticsMiddleware)
r.Use(loggingMiddlewarePrivate) r.Use(loggingMiddlewarePrivate)
@ -310,6 +322,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e
} }
am := baseapp.NewAuthMiddleware(getUserFromRequest) am := baseapp.NewAuthMiddleware(getUserFromRequest)
r.Use(baseapp.LogCommentEnricher)
r.Use(setTimeoutMiddleware) r.Use(setTimeoutMiddleware)
r.Use(s.analyticsMiddleware) r.Use(s.analyticsMiddleware)
r.Use(loggingMiddleware) r.Use(loggingMiddleware)
@ -317,6 +330,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e
apiHandler.RegisterRoutes(r, am) apiHandler.RegisterRoutes(r, am)
apiHandler.RegisterMetricsRoutes(r, am) apiHandler.RegisterMetricsRoutes(r, am)
apiHandler.RegisterLogsRoutes(r, am) apiHandler.RegisterLogsRoutes(r, am)
apiHandler.RegisterIntegrationRoutes(r, am)
apiHandler.RegisterQueryRangeV3Routes(r, am) apiHandler.RegisterQueryRangeV3Routes(r, am)
apiHandler.RegisterQueryRangeV4Routes(r, am) apiHandler.RegisterQueryRangeV4Routes(r, am)
@ -405,30 +419,33 @@ func extractQueryRangeV3Data(path string, r *http.Request) (map[string]interface
signozMetricsUsed := false signozMetricsUsed := false
signozLogsUsed := false signozLogsUsed := false
dataSources := []string{} signozTracesUsed := false
if postData != nil { if postData != nil {
if postData.CompositeQuery != nil { if postData.CompositeQuery != nil {
data["queryType"] = postData.CompositeQuery.QueryType data["queryType"] = postData.CompositeQuery.QueryType
data["panelType"] = postData.CompositeQuery.PanelType data["panelType"] = postData.CompositeQuery.PanelType
signozLogsUsed, signozMetricsUsed = telemetry.GetInstance().CheckSigNozSignals(postData) signozLogsUsed, signozMetricsUsed, signozTracesUsed = telemetry.GetInstance().CheckSigNozSignals(postData)
} }
} }
if signozMetricsUsed || signozLogsUsed { if signozMetricsUsed || signozLogsUsed || signozTracesUsed {
if signozMetricsUsed { if signozMetricsUsed {
dataSources = append(dataSources, "metrics")
telemetry.GetInstance().AddActiveMetricsUser() telemetry.GetInstance().AddActiveMetricsUser()
} }
if signozLogsUsed { if signozLogsUsed {
dataSources = append(dataSources, "logs")
telemetry.GetInstance().AddActiveLogsUser() telemetry.GetInstance().AddActiveLogsUser()
} }
data["dataSources"] = dataSources if signozTracesUsed {
telemetry.GetInstance().AddActiveTracesUser()
}
data["metricsUsed"] = signozMetricsUsed
data["logsUsed"] = signozLogsUsed
data["tracesUsed"] = signozTracesUsed
userEmail, err := baseauth.GetEmailFromJwt(r.Context()) userEmail, err := baseauth.GetEmailFromJwt(r.Context())
if err == nil { if err == nil {
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_QUERY_RANGE_V3, data, userEmail, true) telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_QUERY_RANGE_API, data, userEmail)
} }
} }
return data, true return data, true
@ -508,7 +525,7 @@ func (s *Server) initListeners() error {
return err return err
} }
zap.S().Info(fmt.Sprintf("Query server started listening on %s...", s.serverOptions.HTTPHostPort)) zap.L().Info(fmt.Sprintf("Query server started listening on %s...", s.serverOptions.HTTPHostPort))
// listen on private port to support internal services // listen on private port to support internal services
privateHostPort := s.serverOptions.PrivateHostPort privateHostPort := s.serverOptions.PrivateHostPort
@ -521,7 +538,7 @@ func (s *Server) initListeners() error {
if err != nil { if err != nil {
return err return err
} }
zap.S().Info(fmt.Sprintf("Query server started listening on private port %s...", s.serverOptions.PrivateHostPort)) zap.L().Info(fmt.Sprintf("Query server started listening on private port %s...", s.serverOptions.PrivateHostPort))
return nil return nil
} }
@ -533,7 +550,7 @@ func (s *Server) Start() error {
if !s.serverOptions.DisableRules { if !s.serverOptions.DisableRules {
s.ruleManager.Start() s.ruleManager.Start()
} else { } else {
zap.S().Info("msg: Rules disabled as rules.disable is set to TRUE") zap.L().Info("msg: Rules disabled as rules.disable is set to TRUE")
} }
err := s.initListeners() err := s.initListeners()
@ -547,23 +564,23 @@ func (s *Server) Start() error {
} }
go func() { go func() {
zap.S().Info("Starting HTTP server", zap.Int("port", httpPort), zap.String("addr", s.serverOptions.HTTPHostPort)) zap.L().Info("Starting HTTP server", zap.Int("port", httpPort), zap.String("addr", s.serverOptions.HTTPHostPort))
switch err := s.httpServer.Serve(s.httpConn); err { switch err := s.httpServer.Serve(s.httpConn); err {
case nil, http.ErrServerClosed, cmux.ErrListenerClosed: case nil, http.ErrServerClosed, cmux.ErrListenerClosed:
// normal exit, nothing to do // normal exit, nothing to do
default: default:
zap.S().Error("Could not start HTTP server", zap.Error(err)) zap.L().Error("Could not start HTTP server", zap.Error(err))
} }
s.unavailableChannel <- healthcheck.Unavailable s.unavailableChannel <- healthcheck.Unavailable
}() }()
go func() { go func() {
zap.S().Info("Starting pprof server", zap.String("addr", baseconst.DebugHttpPort)) zap.L().Info("Starting pprof server", zap.String("addr", baseconst.DebugHttpPort))
err = http.ListenAndServe(baseconst.DebugHttpPort, nil) err = http.ListenAndServe(baseconst.DebugHttpPort, nil)
if err != nil { if err != nil {
zap.S().Error("Could not start pprof server", zap.Error(err)) zap.L().Error("Could not start pprof server", zap.Error(err))
} }
}() }()
@ -573,14 +590,14 @@ func (s *Server) Start() error {
} }
go func() { go func() {
zap.S().Info("Starting Private HTTP server", zap.Int("port", privatePort), zap.String("addr", s.serverOptions.PrivateHostPort)) zap.L().Info("Starting Private HTTP server", zap.Int("port", privatePort), zap.String("addr", s.serverOptions.PrivateHostPort))
switch err := s.privateHTTP.Serve(s.privateConn); err { switch err := s.privateHTTP.Serve(s.privateConn); err {
case nil, http.ErrServerClosed, cmux.ErrListenerClosed: case nil, http.ErrServerClosed, cmux.ErrListenerClosed:
// normal exit, nothing to do // normal exit, nothing to do
zap.S().Info("private http server closed") zap.L().Info("private http server closed")
default: default:
zap.S().Error("Could not start private HTTP server", zap.Error(err)) zap.L().Error("Could not start private HTTP server", zap.Error(err))
} }
s.unavailableChannel <- healthcheck.Unavailable s.unavailableChannel <- healthcheck.Unavailable
@ -588,10 +605,10 @@ func (s *Server) Start() error {
}() }()
go func() { go func() {
zap.S().Info("Starting OpAmp Websocket server", zap.String("addr", baseconst.OpAmpWsEndpoint)) zap.L().Info("Starting OpAmp Websocket server", zap.String("addr", baseconst.OpAmpWsEndpoint))
err := s.opampServer.Start(baseconst.OpAmpWsEndpoint) err := s.opampServer.Start(baseconst.OpAmpWsEndpoint)
if err != nil { if err != nil {
zap.S().Info("opamp ws server failed to start", err) zap.L().Error("opamp ws server failed to start", zap.Error(err))
s.unavailableChannel <- healthcheck.Unavailable s.unavailableChannel <- healthcheck.Unavailable
} }
}() }()
@ -667,7 +684,7 @@ func makeRulesManager(
return nil, fmt.Errorf("rule manager error: %v", err) return nil, fmt.Errorf("rule manager error: %v", err)
} }
zap.S().Info("rules manager is ready") zap.L().Info("rules manager is ready")
return manager, nil return manager, nil
} }

View File

@ -17,25 +17,25 @@ import (
func GetUserFromRequest(r *http.Request, apiHandler *api.APIHandler) (*basemodel.UserPayload, error) { func GetUserFromRequest(r *http.Request, apiHandler *api.APIHandler) (*basemodel.UserPayload, error) {
patToken := r.Header.Get("SIGNOZ-API-KEY") patToken := r.Header.Get("SIGNOZ-API-KEY")
if len(patToken) > 0 { if len(patToken) > 0 {
zap.S().Debugf("Received a non-zero length PAT token") zap.L().Debug("Received a non-zero length PAT token")
ctx := context.Background() ctx := context.Background()
dao := apiHandler.AppDao() dao := apiHandler.AppDao()
pat, err := dao.GetPAT(ctx, patToken) pat, err := dao.GetPAT(ctx, patToken)
if err == nil && pat != nil { if err == nil && pat != nil {
zap.S().Debugf("Found valid PAT: %+v", pat) zap.L().Debug("Found valid PAT: ", zap.Any("pat", pat))
if pat.ExpiresAt < time.Now().Unix() && pat.ExpiresAt != 0 { if pat.ExpiresAt < time.Now().Unix() && pat.ExpiresAt != 0 {
zap.S().Debugf("PAT has expired: %+v", pat) zap.L().Info("PAT has expired: ", zap.Any("pat", pat))
return nil, fmt.Errorf("PAT has expired") return nil, fmt.Errorf("PAT has expired")
} }
group, apiErr := dao.GetGroupByName(ctx, pat.Role) group, apiErr := dao.GetGroupByName(ctx, pat.Role)
if apiErr != nil { if apiErr != nil {
zap.S().Debugf("Error while getting group for PAT: %+v", apiErr) zap.L().Error("Error while getting group for PAT: ", zap.Any("apiErr", apiErr))
return nil, apiErr return nil, apiErr
} }
user, err := dao.GetUser(ctx, pat.UserID) user, err := dao.GetUser(ctx, pat.UserID)
if err != nil { if err != nil {
zap.S().Debugf("Error while getting user for PAT: %+v", err) zap.L().Error("Error while getting user for PAT: ", zap.Error(err))
return nil, err return nil, err
} }
telemetry.GetInstance().SetPatTokenUser() telemetry.GetInstance().SetPatTokenUser()
@ -48,7 +48,7 @@ func GetUserFromRequest(r *http.Request, apiHandler *api.APIHandler) (*basemodel
}, nil }, nil
} }
if err != nil { if err != nil {
zap.S().Debugf("Error while getting user for PAT: %+v", err) zap.L().Error("Error while getting user for PAT: ", zap.Error(err))
return nil, err return nil, err
} }
} }

View File

@ -22,19 +22,19 @@ func (m *modelDao) createUserForSAMLRequest(ctx context.Context, email string) (
domain, apierr := m.GetDomainByEmail(ctx, email) domain, apierr := m.GetDomainByEmail(ctx, email)
if apierr != nil { if apierr != nil {
zap.S().Errorf("failed to get domain from email", apierr) zap.L().Error("failed to get domain from email", zap.Error(apierr))
return nil, model.InternalErrorStr("failed to get domain from email") return nil, model.InternalErrorStr("failed to get domain from email")
} }
hash, err := baseauth.PasswordHash(utils.GeneratePassowrd()) hash, err := baseauth.PasswordHash(utils.GeneratePassowrd())
if err != nil { if err != nil {
zap.S().Errorf("failed to generate password hash when registering a user via SSO redirect", zap.Error(err)) zap.L().Error("failed to generate password hash when registering a user via SSO redirect", zap.Error(err))
return nil, model.InternalErrorStr("failed to generate password hash") return nil, model.InternalErrorStr("failed to generate password hash")
} }
group, apiErr := m.GetGroupByName(ctx, baseconst.ViewerGroup) group, apiErr := m.GetGroupByName(ctx, baseconst.ViewerGroup)
if apiErr != nil { if apiErr != nil {
zap.S().Debugf("GetGroupByName failed, err: %v\n", apiErr.Err) zap.L().Error("GetGroupByName failed", zap.Error(apiErr))
return nil, apiErr return nil, apiErr
} }
@ -51,7 +51,7 @@ func (m *modelDao) createUserForSAMLRequest(ctx context.Context, email string) (
user, apiErr = m.CreateUser(ctx, user, false) user, apiErr = m.CreateUser(ctx, user, false)
if apiErr != nil { if apiErr != nil {
zap.S().Debugf("CreateUser failed, err: %v\n", apiErr.Err) zap.L().Error("CreateUser failed", zap.Error(apiErr))
return nil, apiErr return nil, apiErr
} }
@ -65,7 +65,7 @@ func (m *modelDao) PrepareSsoRedirect(ctx context.Context, redirectUri, email st
userPayload, apierr := m.GetUserByEmail(ctx, email) userPayload, apierr := m.GetUserByEmail(ctx, email)
if !apierr.IsNil() { if !apierr.IsNil() {
zap.S().Errorf(" failed to get user with email received from auth provider", apierr.Error()) zap.L().Error("failed to get user with email received from auth provider", zap.String("error", apierr.Error()))
return "", model.BadRequestStr("invalid user email received from the auth provider") return "", model.BadRequestStr("invalid user email received from the auth provider")
} }
@ -75,7 +75,7 @@ func (m *modelDao) PrepareSsoRedirect(ctx context.Context, redirectUri, email st
newUser, apiErr := m.createUserForSAMLRequest(ctx, email) newUser, apiErr := m.createUserForSAMLRequest(ctx, email)
user = newUser user = newUser
if apiErr != nil { if apiErr != nil {
zap.S().Errorf("failed to create user with email received from auth provider: %v", apierr.Error()) zap.L().Error("failed to create user with email received from auth provider", zap.Error(apiErr))
return "", apiErr return "", apiErr
} }
} else { } else {
@ -84,7 +84,7 @@ func (m *modelDao) PrepareSsoRedirect(ctx context.Context, redirectUri, email st
tokenStore, err := baseauth.GenerateJWTForUser(user) tokenStore, err := baseauth.GenerateJWTForUser(user)
if err != nil { if err != nil {
zap.S().Errorf("failed to generate token for SSO login user", err) zap.L().Error("failed to generate token for SSO login user", zap.Error(err))
return "", model.InternalErrorStr("failed to generate token for the user") return "", model.InternalErrorStr("failed to generate token for the user")
} }
@ -143,8 +143,8 @@ func (m *modelDao) PrecheckLogin(ctx context.Context, email, sourceUrl string) (
// do nothing, just skip sso // do nothing, just skip sso
ssoAvailable = false ssoAvailable = false
default: default:
zap.S().Errorf("feature check failed", zap.String("featureKey", model.SSO), zap.Error(err)) zap.L().Error("feature check failed", zap.String("featureKey", model.SSO), zap.Error(err))
return resp, model.BadRequest(err) return resp, model.BadRequestStr(err.Error())
} }
} }
@ -160,7 +160,7 @@ func (m *modelDao) PrecheckLogin(ctx context.Context, email, sourceUrl string) (
if len(emailComponents) > 0 { if len(emailComponents) > 0 {
emailDomain = emailComponents[1] emailDomain = emailComponents[1]
} }
zap.S().Errorf("failed to get org domain from email", zap.String("emailDomain", emailDomain), apierr.ToError()) zap.L().Error("failed to get org domain from email", zap.String("emailDomain", emailDomain), zap.Error(apierr.ToError()))
return resp, apierr return resp, apierr
} }
@ -176,7 +176,7 @@ func (m *modelDao) PrecheckLogin(ctx context.Context, email, sourceUrl string) (
escapedUrl, _ := url.QueryUnescape(sourceUrl) escapedUrl, _ := url.QueryUnescape(sourceUrl)
siteUrl, err := url.Parse(escapedUrl) siteUrl, err := url.Parse(escapedUrl)
if err != nil { if err != nil {
zap.S().Errorf("failed to parse referer", err) zap.L().Error("failed to parse referer", zap.Error(err))
return resp, model.InternalError(fmt.Errorf("failed to generate login request")) return resp, model.InternalError(fmt.Errorf("failed to generate login request"))
} }
@ -185,7 +185,7 @@ func (m *modelDao) PrecheckLogin(ctx context.Context, email, sourceUrl string) (
resp.SsoUrl, err = orgDomain.BuildSsoUrl(siteUrl) resp.SsoUrl, err = orgDomain.BuildSsoUrl(siteUrl)
if err != nil { if err != nil {
zap.S().Errorf("failed to prepare saml request for domain", zap.String("domain", orgDomain.Name), err) zap.L().Error("failed to prepare saml request for domain", zap.String("domain", orgDomain.Name), zap.Error(err))
return resp, model.InternalError(err) return resp, model.InternalError(err)
} }

View File

@ -48,13 +48,13 @@ func (m *modelDao) GetDomainFromSsoResponse(ctx context.Context, relayState *url
if domainIdStr != "" { if domainIdStr != "" {
domainId, err := uuid.Parse(domainIdStr) domainId, err := uuid.Parse(domainIdStr)
if err != nil { if err != nil {
zap.S().Errorf("failed to parse domainId from relay state", err) zap.L().Error("failed to parse domainId from relay state", zap.Error(err))
return nil, fmt.Errorf("failed to parse domainId from IdP response") return nil, fmt.Errorf("failed to parse domainId from IdP response")
} }
domain, err = m.GetDomain(ctx, domainId) domain, err = m.GetDomain(ctx, domainId)
if (err != nil) || domain == nil { if (err != nil) || domain == nil {
zap.S().Errorf("failed to find domain from domainId received in IdP response", err.Error()) zap.L().Error("failed to find domain from domainId received in IdP response", zap.Error(err))
return nil, fmt.Errorf("invalid credentials") return nil, fmt.Errorf("invalid credentials")
} }
} }
@ -64,7 +64,7 @@ func (m *modelDao) GetDomainFromSsoResponse(ctx context.Context, relayState *url
domainFromDB, err := m.GetDomainByName(ctx, domainNameStr) domainFromDB, err := m.GetDomainByName(ctx, domainNameStr)
domain = domainFromDB domain = domainFromDB
if (err != nil) || domain == nil { if (err != nil) || domain == nil {
zap.S().Errorf("failed to find domain from domainName received in IdP response", err.Error()) zap.L().Error("failed to find domain from domainName received in IdP response", zap.Error(err))
return nil, fmt.Errorf("invalid credentials") return nil, fmt.Errorf("invalid credentials")
} }
} }
@ -132,7 +132,7 @@ func (m *modelDao) ListDomains(ctx context.Context, orgId string) ([]model.OrgDo
for _, s := range stored { for _, s := range stored {
domain := model.OrgDomain{Id: s.Id, Name: s.Name, OrgId: s.OrgId} domain := model.OrgDomain{Id: s.Id, Name: s.Name, OrgId: s.OrgId}
if err := domain.LoadConfig(s.Data); err != nil { if err := domain.LoadConfig(s.Data); err != nil {
zap.S().Errorf("ListDomains() failed", zap.Error(err)) zap.L().Error("ListDomains() failed", zap.Error(err))
} }
domains = append(domains, domain) domains = append(domains, domain)
} }
@ -153,7 +153,7 @@ func (m *modelDao) CreateDomain(ctx context.Context, domain *model.OrgDomain) ba
configJson, err := json.Marshal(domain) configJson, err := json.Marshal(domain)
if err != nil { if err != nil {
zap.S().Errorf("failed to unmarshal domain config", zap.Error(err)) zap.L().Error("failed to unmarshal domain config", zap.Error(err))
return model.InternalError(fmt.Errorf("domain creation failed")) return model.InternalError(fmt.Errorf("domain creation failed"))
} }
@ -167,7 +167,7 @@ func (m *modelDao) CreateDomain(ctx context.Context, domain *model.OrgDomain) ba
time.Now().Unix()) time.Now().Unix())
if err != nil { if err != nil {
zap.S().Errorf("failed to insert domain in db", zap.Error(err)) zap.L().Error("failed to insert domain in db", zap.Error(err))
return model.InternalError(fmt.Errorf("domain creation failed")) return model.InternalError(fmt.Errorf("domain creation failed"))
} }
@ -178,13 +178,13 @@ func (m *modelDao) CreateDomain(ctx context.Context, domain *model.OrgDomain) ba
func (m *modelDao) UpdateDomain(ctx context.Context, domain *model.OrgDomain) basemodel.BaseApiError { func (m *modelDao) UpdateDomain(ctx context.Context, domain *model.OrgDomain) basemodel.BaseApiError {
if domain.Id == uuid.Nil { if domain.Id == uuid.Nil {
zap.S().Errorf("domain update failed", zap.Error(fmt.Errorf("OrgDomain.Id is null"))) zap.L().Error("domain update failed", zap.Error(fmt.Errorf("OrgDomain.Id is null")))
return model.InternalError(fmt.Errorf("domain update failed")) return model.InternalError(fmt.Errorf("domain update failed"))
} }
configJson, err := json.Marshal(domain) configJson, err := json.Marshal(domain)
if err != nil { if err != nil {
zap.S().Errorf("domain update failed", zap.Error(err)) zap.L().Error("domain update failed", zap.Error(err))
return model.InternalError(fmt.Errorf("domain update failed")) return model.InternalError(fmt.Errorf("domain update failed"))
} }
@ -195,7 +195,7 @@ func (m *modelDao) UpdateDomain(ctx context.Context, domain *model.OrgDomain) ba
domain.Id) domain.Id)
if err != nil { if err != nil {
zap.S().Errorf("domain update failed", zap.Error(err)) zap.L().Error("domain update failed", zap.Error(err))
return model.InternalError(fmt.Errorf("domain update failed")) return model.InternalError(fmt.Errorf("domain update failed"))
} }
@ -206,7 +206,7 @@ func (m *modelDao) UpdateDomain(ctx context.Context, domain *model.OrgDomain) ba
func (m *modelDao) DeleteDomain(ctx context.Context, id uuid.UUID) basemodel.BaseApiError { func (m *modelDao) DeleteDomain(ctx context.Context, id uuid.UUID) basemodel.BaseApiError {
if id == uuid.Nil { if id == uuid.Nil {
zap.S().Errorf("domain delete failed", zap.Error(fmt.Errorf("OrgDomain.Id is null"))) zap.L().Error("domain delete failed", zap.Error(fmt.Errorf("OrgDomain.Id is null")))
return model.InternalError(fmt.Errorf("domain delete failed")) return model.InternalError(fmt.Errorf("domain delete failed"))
} }
@ -215,7 +215,7 @@ func (m *modelDao) DeleteDomain(ctx context.Context, id uuid.UUID) basemodel.Bas
id) id)
if err != nil { if err != nil {
zap.S().Errorf("domain delete failed", zap.Error(err)) zap.L().Error("domain delete failed", zap.Error(err))
return model.InternalError(fmt.Errorf("domain delete failed")) return model.InternalError(fmt.Errorf("domain delete failed"))
} }

View File

@ -26,12 +26,12 @@ func (m *modelDao) CreatePAT(ctx context.Context, p model.PAT) (model.PAT, basem
p.Revoked, p.Revoked,
) )
if err != nil { if err != nil {
zap.S().Errorf("Failed to insert PAT in db, err: %v", zap.Error(err)) zap.L().Error("Failed to insert PAT in db, err: %v", zap.Error(err))
return model.PAT{}, model.InternalError(fmt.Errorf("PAT insertion failed")) return model.PAT{}, model.InternalError(fmt.Errorf("PAT insertion failed"))
} }
id, err := result.LastInsertId() id, err := result.LastInsertId()
if err != nil { if err != nil {
zap.S().Errorf("Failed to get last inserted id, err: %v", zap.Error(err)) zap.L().Error("Failed to get last inserted id, err: %v", zap.Error(err))
return model.PAT{}, model.InternalError(fmt.Errorf("PAT insertion failed")) return model.PAT{}, model.InternalError(fmt.Errorf("PAT insertion failed"))
} }
p.Id = strconv.Itoa(int(id)) p.Id = strconv.Itoa(int(id))
@ -62,7 +62,7 @@ func (m *modelDao) UpdatePAT(ctx context.Context, p model.PAT, id string) basemo
p.UpdatedByUserID, p.UpdatedByUserID,
id) id)
if err != nil { if err != nil {
zap.S().Errorf("Failed to update PAT in db, err: %v", zap.Error(err)) zap.L().Error("Failed to update PAT in db, err: %v", zap.Error(err))
return model.InternalError(fmt.Errorf("PAT update failed")) return model.InternalError(fmt.Errorf("PAT update failed"))
} }
return nil return nil
@ -74,7 +74,7 @@ func (m *modelDao) UpdatePATLastUsed(ctx context.Context, token string, lastUsed
lastUsed, lastUsed,
token) token)
if err != nil { if err != nil {
zap.S().Errorf("Failed to update PAT last used in db, err: %v", zap.Error(err)) zap.L().Error("Failed to update PAT last used in db, err: %v", zap.Error(err))
return model.InternalError(fmt.Errorf("PAT last used update failed")) return model.InternalError(fmt.Errorf("PAT last used update failed"))
} }
return nil return nil
@ -84,7 +84,7 @@ func (m *modelDao) ListPATs(ctx context.Context) ([]model.PAT, basemodel.BaseApi
pats := []model.PAT{} pats := []model.PAT{}
if err := m.DB().Select(&pats, "SELECT * FROM personal_access_tokens WHERE revoked=false ORDER by updated_at DESC;"); err != nil { if err := m.DB().Select(&pats, "SELECT * FROM personal_access_tokens WHERE revoked=false ORDER by updated_at DESC;"); err != nil {
zap.S().Errorf("Failed to fetch PATs err: %v", zap.Error(err)) zap.L().Error("Failed to fetch PATs err: %v", zap.Error(err))
return nil, model.InternalError(fmt.Errorf("failed to fetch PATs")) return nil, model.InternalError(fmt.Errorf("failed to fetch PATs"))
} }
for i := range pats { for i := range pats {
@ -129,7 +129,7 @@ func (m *modelDao) RevokePAT(ctx context.Context, id string, userID string) base
"UPDATE personal_access_tokens SET revoked=true, updated_by_user_id = $1, updated_at=$2 WHERE id=$3", "UPDATE personal_access_tokens SET revoked=true, updated_by_user_id = $1, updated_at=$2 WHERE id=$3",
userID, updatedAt, id) userID, updatedAt, id)
if err != nil { if err != nil {
zap.S().Errorf("Failed to revoke PAT in db, err: %v", zap.Error(err)) zap.L().Error("Failed to revoke PAT in db, err: %v", zap.Error(err))
return model.InternalError(fmt.Errorf("PAT revoke failed")) return model.InternalError(fmt.Errorf("PAT revoke failed"))
} }
return nil return nil

View File

@ -47,13 +47,13 @@ func ActivateLicense(key, siteId string) (*ActivationResponse, *model.ApiError)
httpResponse, err := http.Post(C.Prefix+"/licenses/activate", APPLICATION_JSON, bytes.NewBuffer(reqString)) httpResponse, err := http.Post(C.Prefix+"/licenses/activate", APPLICATION_JSON, bytes.NewBuffer(reqString))
if err != nil { if err != nil {
zap.S().Errorf("failed to connect to license.signoz.io", err) 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")) return nil, model.BadRequest(fmt.Errorf("unable to connect with license.signoz.io, please check your network connection"))
} }
httpBody, err := io.ReadAll(httpResponse.Body) httpBody, err := io.ReadAll(httpResponse.Body)
if err != nil { if err != nil {
zap.S().Errorf("failed to read activation response from license.signoz.io", err) 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")) return nil, model.BadRequest(fmt.Errorf("failed to read activation response from license.signoz.io"))
} }
@ -63,7 +63,7 @@ func ActivateLicense(key, siteId string) (*ActivationResponse, *model.ApiError)
result := ActivationResult{} result := ActivationResult{}
err = json.Unmarshal(httpBody, &result) err = json.Unmarshal(httpBody, &result)
if err != nil { if err != nil {
zap.S().Errorf("failed to marshal activation response from license.signoz.io", err) 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")) return nil, model.InternalError(errors.Wrap(err, "failed to marshal license activation response"))
} }

View File

@ -97,7 +97,7 @@ func (r *Repo) InsertLicense(ctx context.Context, l *model.License) error {
l.ValidationMessage) l.ValidationMessage)
if err != nil { if err != nil {
zap.S().Errorf("error in inserting license data: ", zap.Error(err)) zap.L().Error("error in inserting license data: ", zap.Error(err))
return fmt.Errorf("failed to insert license in db: %v", err) return fmt.Errorf("failed to insert license in db: %v", err)
} }
@ -121,7 +121,7 @@ func (r *Repo) UpdatePlanDetails(ctx context.Context,
_, err := r.db.ExecContext(ctx, query, planDetails, time.Now(), key) _, err := r.db.ExecContext(ctx, query, planDetails, time.Now(), key)
if err != nil { if err != nil {
zap.S().Errorf("error in updating license: ", zap.Error(err)) zap.L().Error("error in updating license: ", zap.Error(err))
return fmt.Errorf("failed to update license in db: %v", err) return fmt.Errorf("failed to update license in db: %v", err)
} }

View File

@ -100,7 +100,7 @@ func (lm *Manager) SetActive(l *model.License) {
err := lm.InitFeatures(lm.activeFeatures) err := lm.InitFeatures(lm.activeFeatures)
if err != nil { if err != nil {
zap.S().Panicf("Couldn't activate features: %v", err) zap.L().Panic("Couldn't activate features", zap.Error(err))
} }
if !lm.validatorRunning { if !lm.validatorRunning {
// we want to make sure only one validator runs, // we want to make sure only one validator runs,
@ -125,13 +125,13 @@ func (lm *Manager) LoadActiveLicense() error {
if active != nil { if active != nil {
lm.SetActive(active) lm.SetActive(active)
} else { } else {
zap.S().Info("No active license found, defaulting to basic plan") 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 // if no active license is found, we default to basic(free) plan with all default features
lm.activeFeatures = model.BasicPlan lm.activeFeatures = model.BasicPlan
setDefaultFeatures(lm) setDefaultFeatures(lm)
err := lm.InitFeatures(lm.activeFeatures) err := lm.InitFeatures(lm.activeFeatures)
if err != nil { if err != nil {
zap.S().Error("Couldn't initialize features: ", err) zap.L().Error("Couldn't initialize features", zap.Error(err))
return err return err
} }
} }
@ -191,7 +191,7 @@ func (lm *Manager) Validator(ctx context.Context) {
// Validate validates the current active license // Validate validates the current active license
func (lm *Manager) Validate(ctx context.Context) (reterr error) { func (lm *Manager) Validate(ctx context.Context) (reterr error) {
zap.S().Info("License validation started") zap.L().Info("License validation started")
if lm.activeLicense == nil { if lm.activeLicense == nil {
return nil return nil
} }
@ -201,12 +201,12 @@ func (lm *Manager) Validate(ctx context.Context) (reterr error) {
lm.lastValidated = time.Now().Unix() lm.lastValidated = time.Now().Unix()
if reterr != nil { if reterr != nil {
zap.S().Errorf("License validation completed with error", reterr) zap.L().Error("License validation completed with error", zap.Error(reterr))
atomic.AddUint64(&lm.failedAttempts, 1) atomic.AddUint64(&lm.failedAttempts, 1)
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_LICENSE_CHECK_FAILED, telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_LICENSE_CHECK_FAILED,
map[string]interface{}{"err": reterr.Error()}, "") map[string]interface{}{"err": reterr.Error()}, "")
} else { } else {
zap.S().Info("License validation completed with no errors") zap.L().Info("License validation completed with no errors")
} }
lm.mutex.Unlock() lm.mutex.Unlock()
@ -214,7 +214,7 @@ func (lm *Manager) Validate(ctx context.Context) (reterr error) {
response, apiError := validate.ValidateLicense(lm.activeLicense.ActivationId) response, apiError := validate.ValidateLicense(lm.activeLicense.ActivationId)
if apiError != nil { if apiError != nil {
zap.S().Errorf("failed to validate license", apiError) zap.L().Error("failed to validate license", zap.Error(apiError.Err))
return apiError.Err return apiError.Err
} }
@ -235,7 +235,7 @@ func (lm *Manager) Validate(ctx context.Context) (reterr error) {
} }
if err := l.ParsePlan(); err != nil { if err := l.ParsePlan(); err != nil {
zap.S().Errorf("failed to parse updated license", zap.Error(err)) zap.L().Error("failed to parse updated license", zap.Error(err))
return err return err
} }
@ -245,7 +245,7 @@ func (lm *Manager) Validate(ctx context.Context) (reterr error) {
if err != nil { if err != nil {
// unexpected db write issue but we can let the user continue // unexpected db write issue but we can let the user continue
// and wait for update to work in next cycle. // and wait for update to work in next cycle.
zap.S().Errorf("failed to validate license", zap.Error(err)) zap.L().Error("failed to validate license", zap.Error(err))
} }
} }
@ -270,7 +270,7 @@ func (lm *Manager) Activate(ctx context.Context, key string) (licenseResponse *m
response, apiError := validate.ActivateLicense(key, "") response, apiError := validate.ActivateLicense(key, "")
if apiError != nil { if apiError != nil {
zap.S().Errorf("failed to activate license", zap.Error(apiError.Err)) zap.L().Error("failed to activate license", zap.Error(apiError.Err))
return nil, apiError return nil, apiError
} }
@ -284,14 +284,14 @@ func (lm *Manager) Activate(ctx context.Context, key string) (licenseResponse *m
err := l.ParsePlan() err := l.ParsePlan()
if err != nil { if err != nil {
zap.S().Errorf("failed to activate license", zap.Error(err)) zap.L().Error("failed to activate license", zap.Error(err))
return nil, model.InternalError(err) return nil, model.InternalError(err)
} }
// store the license before activating it // store the license before activating it
err = lm.repo.InsertLicense(ctx, l) err = lm.repo.InsertLicense(ctx, l)
if err != nil { if err != nil {
zap.S().Errorf("failed to activate license", zap.Error(err)) zap.L().Error("failed to activate license", zap.Error(err))
return nil, model.InternalError(err) return nil, model.InternalError(err)
} }

View File

@ -14,10 +14,10 @@ import (
semconv "go.opentelemetry.io/otel/semconv/v1.4.0" semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
"go.signoz.io/signoz/ee/query-service/app" "go.signoz.io/signoz/ee/query-service/app"
"go.signoz.io/signoz/pkg/query-service/auth" "go.signoz.io/signoz/pkg/query-service/auth"
"go.signoz.io/signoz/pkg/query-service/constants"
baseconst "go.signoz.io/signoz/pkg/query-service/constants" baseconst "go.signoz.io/signoz/pkg/query-service/constants"
"go.signoz.io/signoz/pkg/query-service/version" "go.signoz.io/signoz/pkg/query-service/version"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
zapotlpencoder "github.com/SigNoz/zap_otlp/zap_otlp_encoder" zapotlpencoder "github.com/SigNoz/zap_otlp/zap_otlp_encoder"
zapotlpsync "github.com/SigNoz/zap_otlp/zap_otlp_sync" zapotlpsync "github.com/SigNoz/zap_otlp/zap_otlp_sync"
@ -27,18 +27,19 @@ import (
) )
func initZapLog(enableQueryServiceLogOTLPExport bool) *zap.Logger { func initZapLog(enableQueryServiceLogOTLPExport bool) *zap.Logger {
config := zap.NewDevelopmentConfig() config := zap.NewProductionConfig()
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop() defer stop()
config.EncoderConfig.EncodeDuration = zapcore.StringDurationEncoder config.EncoderConfig.EncodeDuration = zapcore.MillisDurationEncoder
otlpEncoder := zapotlpencoder.NewOTLPEncoder(config.EncoderConfig) config.EncoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
consoleEncoder := zapcore.NewConsoleEncoder(config.EncoderConfig)
defaultLogLevel := zapcore.DebugLevel
config.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
config.EncoderConfig.TimeKey = "timestamp" config.EncoderConfig.TimeKey = "timestamp"
config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
otlpEncoder := zapotlpencoder.NewOTLPEncoder(config.EncoderConfig)
consoleEncoder := zapcore.NewJSONEncoder(config.EncoderConfig)
defaultLogLevel := zapcore.InfoLevel
res := resource.NewWithAttributes( res := resource.NewWithAttributes(
semconv.SchemaURL, semconv.SchemaURL,
semconv.ServiceNameKey.String("query-service"), semconv.ServiceNameKey.String("query-service"),
@ -48,14 +49,15 @@ func initZapLog(enableQueryServiceLogOTLPExport bool) *zap.Logger {
zapcore.NewCore(consoleEncoder, os.Stdout, defaultLogLevel), zapcore.NewCore(consoleEncoder, os.Stdout, defaultLogLevel),
) )
if enableQueryServiceLogOTLPExport == true { if enableQueryServiceLogOTLPExport {
conn, err := grpc.DialContext(ctx, constants.OTLPTarget, grpc.WithBlock(), grpc.WithInsecure(), grpc.WithTimeout(time.Second*30)) ctx, _ := context.WithTimeout(ctx, time.Second*30)
conn, err := grpc.DialContext(ctx, baseconst.OTLPTarget, grpc.WithBlock(), grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil { if err != nil {
log.Println("failed to connect to otlp collector to export query service logs with error:", err) log.Fatalf("failed to establish connection: %v", err)
} else { } else {
logExportBatchSizeInt, err := strconv.Atoi(baseconst.LogExportBatchSize) logExportBatchSizeInt, err := strconv.Atoi(baseconst.LogExportBatchSize)
if err != nil { if err != nil {
logExportBatchSizeInt = 1000 logExportBatchSizeInt = 512
} }
ws := zapcore.AddSync(zapotlpsync.NewOtlpSyncer(conn, zapotlpsync.Options{ ws := zapcore.AddSync(zapotlpsync.NewOtlpSyncer(conn, zapotlpsync.Options{
BatchSize: logExportBatchSizeInt, BatchSize: logExportBatchSizeInt,
@ -113,7 +115,6 @@ func main() {
zap.ReplaceGlobals(loggerMgr) zap.ReplaceGlobals(loggerMgr)
defer loggerMgr.Sync() // flushes buffer, if any defer loggerMgr.Sync() // flushes buffer, if any
logger := loggerMgr.Sugar()
version.PrintVersion() version.PrintVersion()
serverOptions := &app.ServerOptions{ serverOptions := &app.ServerOptions{
@ -137,22 +138,22 @@ func main() {
auth.JwtSecret = os.Getenv("SIGNOZ_JWT_SECRET") auth.JwtSecret = os.Getenv("SIGNOZ_JWT_SECRET")
if len(auth.JwtSecret) == 0 { if len(auth.JwtSecret) == 0 {
zap.S().Warn("No JWT secret key is specified.") zap.L().Warn("No JWT secret key is specified.")
} else { } else {
zap.S().Info("No JWT secret key set successfully.") zap.L().Info("JWT secret key set successfully.")
} }
server, err := app.NewServer(serverOptions) server, err := app.NewServer(serverOptions)
if err != nil { if err != nil {
logger.Fatal("Failed to create server", zap.Error(err)) zap.L().Fatal("Failed to create server", zap.Error(err))
} }
if err := server.Start(); err != nil { if err := server.Start(); err != nil {
logger.Fatal("Could not start servers", zap.Error(err)) zap.L().Fatal("Could not start server", zap.Error(err))
} }
if err := auth.InitAuthCache(context.Background()); err != nil { if err := auth.InitAuthCache(context.Background()); err != nil {
logger.Fatal("Failed to initialize auth cache", zap.Error(err)) zap.L().Fatal("Failed to initialize auth cache", zap.Error(err))
} }
signalsChannel := make(chan os.Signal, 1) signalsChannel := make(chan os.Signal, 1)
@ -161,9 +162,9 @@ func main() {
for { for {
select { select {
case status := <-server.HealthCheckStatus(): case status := <-server.HealthCheckStatus():
logger.Info("Received HealthCheck status: ", zap.Int("status", int(status))) zap.L().Info("Received HealthCheck status: ", zap.Int("status", int(status)))
case <-signalsChannel: case <-signalsChannel:
logger.Fatal("Received OS Interrupt Signal ... ") zap.L().Fatal("Received OS Interrupt Signal ... ")
server.Stop() server.Stop()
} }
} }

View File

@ -9,8 +9,8 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/pkg/errors" "github.com/pkg/errors"
saml2 "github.com/russellhaering/gosaml2" saml2 "github.com/russellhaering/gosaml2"
"go.signoz.io/signoz/ee/query-service/sso/saml"
"go.signoz.io/signoz/ee/query-service/sso" "go.signoz.io/signoz/ee/query-service/sso"
"go.signoz.io/signoz/ee/query-service/sso/saml"
basemodel "go.signoz.io/signoz/pkg/query-service/model" basemodel "go.signoz.io/signoz/pkg/query-service/model"
"go.uber.org/zap" "go.uber.org/zap"
) )
@ -24,16 +24,16 @@ const (
// OrgDomain identify org owned web domains for auth and other purposes // OrgDomain identify org owned web domains for auth and other purposes
type OrgDomain struct { type OrgDomain struct {
Id uuid.UUID `json:"id"` Id uuid.UUID `json:"id"`
Name string `json:"name"` Name string `json:"name"`
OrgId string `json:"orgId"` OrgId string `json:"orgId"`
SsoEnabled bool `json:"ssoEnabled"` SsoEnabled bool `json:"ssoEnabled"`
SsoType SSOType `json:"ssoType"` SsoType SSOType `json:"ssoType"`
SamlConfig *SamlConfig `json:"samlConfig"` SamlConfig *SamlConfig `json:"samlConfig"`
GoogleAuthConfig *GoogleOAuthConfig `json:"googleAuthConfig"` GoogleAuthConfig *GoogleOAuthConfig `json:"googleAuthConfig"`
Org *basemodel.Organization Org *basemodel.Organization
} }
func (od *OrgDomain) String() string { func (od *OrgDomain) String() string {
@ -138,7 +138,6 @@ func (od *OrgDomain) PrepareSamlRequest(siteUrl *url.URL) (*saml2.SAMLServicePro
func (od *OrgDomain) BuildSsoUrl(siteUrl *url.URL) (ssoUrl string, err error) { func (od *OrgDomain) BuildSsoUrl(siteUrl *url.URL) (ssoUrl string, err error) {
fmtDomainId := strings.Replace(od.Id.String(), "-", ":", -1) fmtDomainId := strings.Replace(od.Id.String(), "-", ":", -1)
// build redirect url from window.location sent by frontend // build redirect url from window.location sent by frontend
@ -156,8 +155,7 @@ func (od *OrgDomain) BuildSsoUrl(siteUrl *url.URL) (ssoUrl string, err error) {
// auth provider to the backend (HandleCallback or HandleSSO method). // auth provider to the backend (HandleCallback or HandleSSO method).
relayState := fmt.Sprintf("%s?domainId=%s", redirectURL, fmtDomainId) relayState := fmt.Sprintf("%s?domainId=%s", redirectURL, fmtDomainId)
switch od.SsoType {
switch (od.SsoType) {
case SAML: case SAML:
sp, err := od.PrepareSamlRequest(siteUrl) sp, err := od.PrepareSamlRequest(siteUrl)
@ -176,9 +174,8 @@ func (od *OrgDomain) BuildSsoUrl(siteUrl *url.URL) (ssoUrl string, err error) {
return googleProvider.BuildAuthURL(relayState) return googleProvider.BuildAuthURL(relayState)
default: default:
zap.S().Errorf("found unsupported SSO config for the org domain", zap.String("orgDomain", od.Name)) zap.L().Error("found unsupported SSO config for the org domain", zap.String("orgDomain", od.Name))
return "", fmt.Errorf("unsupported SSO config for the domain") return "", fmt.Errorf("unsupported SSO config for the domain")
} }
} }

View File

@ -90,6 +90,13 @@ var BasicPlan = basemodel.FeatureSet{
UsageLimit: -1, UsageLimit: -1,
Route: "", Route: "",
}, },
basemodel.Feature{
Name: basemodel.AlertChannelEmail,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{ basemodel.Feature{
Name: basemodel.AlertChannelMsTeams, Name: basemodel.AlertChannelMsTeams,
Active: false, Active: false,
@ -177,6 +184,13 @@ var ProPlan = basemodel.FeatureSet{
UsageLimit: -1, UsageLimit: -1,
Route: "", Route: "",
}, },
basemodel.Feature{
Name: basemodel.AlertChannelEmail,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{ basemodel.Feature{
Name: basemodel.AlertChannelMsTeams, Name: basemodel.AlertChannelMsTeams,
Active: true, Active: true,
@ -264,6 +278,13 @@ var EnterprisePlan = basemodel.FeatureSet{
UsageLimit: -1, UsageLimit: -1,
Route: "", Route: "",
}, },
basemodel.Feature{
Name: basemodel.AlertChannelEmail,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{ basemodel.Feature{
Name: basemodel.AlertChannelMsTeams, Name: basemodel.AlertChannelMsTeams,
Active: true, Active: true,
@ -279,17 +300,17 @@ var EnterprisePlan = basemodel.FeatureSet{
Route: "", Route: "",
}, },
basemodel.Feature{ basemodel.Feature{
Name: Onboarding, Name: Onboarding,
Active: true, Active: true,
Usage: 0, Usage: 0,
UsageLimit: -1, UsageLimit: -1,
Route: "", Route: "",
}, },
basemodel.Feature{ basemodel.Feature{
Name: ChatSupport, Name: ChatSupport,
Active: true, Active: true,
Usage: 0, Usage: 0,
UsageLimit: -1, UsageLimit: -1,
Route: "", Route: "",
}, },
} }

View File

@ -102,6 +102,6 @@ func PrepareRequest(issuer, acsUrl, audience, entity, idp, certString string) (*
IDPCertificateStore: certStore, IDPCertificateStore: certStore,
SPKeyStore: randomKeyStore, SPKeyStore: randomKeyStore,
} }
zap.S().Debugf("SAML request:", sp) zap.L().Debug("SAML request", zap.Any("sp", sp))
return sp, nil return sp, nil
} }

View File

@ -91,12 +91,12 @@ func (lm *Manager) UploadUsage() {
// check if license is present or not // check if license is present or not
license, err := lm.licenseRepo.GetActiveLicense(ctx) license, err := lm.licenseRepo.GetActiveLicense(ctx)
if err != nil { if err != nil {
zap.S().Errorf("failed to get active license: %v", zap.Error(err)) zap.L().Error("failed to get active license", zap.Error(err))
return return
} }
if license == nil { if license == nil {
// we will not start the usage reporting if license is not present. // we will not start the usage reporting if license is not present.
zap.S().Info("no license present, skipping usage reporting") zap.L().Info("no license present, skipping usage reporting")
return return
} }
@ -123,7 +123,7 @@ func (lm *Manager) UploadUsage() {
dbusages := []model.UsageDB{} dbusages := []model.UsageDB{}
err := lm.clickhouseConn.Select(ctx, &dbusages, fmt.Sprintf(query, db, db), time.Now().Add(-(24 * time.Hour))) err := lm.clickhouseConn.Select(ctx, &dbusages, fmt.Sprintf(query, db, db), time.Now().Add(-(24 * time.Hour)))
if err != nil && !strings.Contains(err.Error(), "doesn't exist") { if err != nil && !strings.Contains(err.Error(), "doesn't exist") {
zap.S().Errorf("failed to get usage from clickhouse: %v", zap.Error(err)) zap.L().Error("failed to get usage from clickhouse: %v", zap.Error(err))
return return
} }
for _, u := range dbusages { for _, u := range dbusages {
@ -133,16 +133,16 @@ func (lm *Manager) UploadUsage() {
} }
if len(usages) <= 0 { if len(usages) <= 0 {
zap.S().Info("no snapshots to upload, skipping.") zap.L().Info("no snapshots to upload, skipping.")
return return
} }
zap.S().Info("uploading usage data") zap.L().Info("uploading usage data")
orgName := "" orgName := ""
orgNames, orgError := lm.modelDao.GetOrgs(ctx) orgNames, orgError := lm.modelDao.GetOrgs(ctx)
if orgError != nil { if orgError != nil {
zap.S().Errorf("failed to get org data: %v", zap.Error(orgError)) zap.L().Error("failed to get org data: %v", zap.Error(orgError))
} }
if len(orgNames) == 1 { if len(orgNames) == 1 {
orgName = orgNames[0].Name orgName = orgNames[0].Name
@ -152,14 +152,14 @@ func (lm *Manager) UploadUsage() {
for _, usage := range usages { for _, usage := range usages {
usageDataBytes, err := encryption.Decrypt([]byte(usage.ExporterID[:32]), []byte(usage.Data)) usageDataBytes, err := encryption.Decrypt([]byte(usage.ExporterID[:32]), []byte(usage.Data))
if err != nil { if err != nil {
zap.S().Errorf("error while decrypting usage data: %v", zap.Error(err)) zap.L().Error("error while decrypting usage data: %v", zap.Error(err))
return return
} }
usageData := model.Usage{} usageData := model.Usage{}
err = json.Unmarshal(usageDataBytes, &usageData) err = json.Unmarshal(usageDataBytes, &usageData)
if err != nil { if err != nil {
zap.S().Errorf("error while unmarshalling usage data: %v", zap.Error(err)) zap.L().Error("error while unmarshalling usage data: %v", zap.Error(err))
return return
} }
@ -184,13 +184,13 @@ func (lm *Manager) UploadUsageWithExponentalBackOff(ctx context.Context, payload
for i := 1; i <= MaxRetries; i++ { for i := 1; i <= MaxRetries; i++ {
apiErr := licenseserver.SendUsage(ctx, payload) apiErr := licenseserver.SendUsage(ctx, payload)
if apiErr != nil && i == MaxRetries { if apiErr != nil && i == MaxRetries {
zap.S().Errorf("retries stopped : %v", zap.Error(apiErr)) zap.L().Error("retries stopped : %v", zap.Error(apiErr))
// not returning error here since it is captured in the failed count // not returning error here since it is captured in the failed count
return return
} else if apiErr != nil { } else if apiErr != nil {
// sleeping for exponential backoff // sleeping for exponential backoff
sleepDuration := RetryInterval * time.Duration(i) sleepDuration := RetryInterval * time.Duration(i)
zap.S().Errorf("failed to upload snapshot retrying after %v secs : %v", sleepDuration.Seconds(), zap.Error(apiErr.Err)) zap.L().Error("failed to upload snapshot retrying after %v secs : %v", zap.Duration("sleepDuration", sleepDuration), zap.Error(apiErr.Err))
time.Sleep(sleepDuration) time.Sleep(sleepDuration)
} else { } else {
break break
@ -201,7 +201,7 @@ func (lm *Manager) UploadUsageWithExponentalBackOff(ctx context.Context, payload
func (lm *Manager) Stop() { func (lm *Manager) Stop() {
lm.scheduler.Stop() lm.scheduler.Stop()
zap.S().Debug("sending usage data before shutting down") zap.L().Info("sending usage data before shutting down")
// send usage before shutting down // send usage before shutting down
lm.UploadUsage() lm.UploadUsage()

View File

@ -107,6 +107,7 @@
"react-virtuoso": "4.0.3", "react-virtuoso": "4.0.3",
"redux": "^4.0.5", "redux": "^4.0.5",
"redux-thunk": "^2.3.0", "redux-thunk": "^2.3.0",
"rehype-raw": "7.0.0",
"stream": "^0.0.2", "stream": "^0.0.2",
"style-loader": "1.3.0", "style-loader": "1.3.0",
"styled-components": "^5.3.11", "styled-components": "^5.3.11",
@ -203,6 +204,7 @@
"jest-styled-components": "^7.0.8", "jest-styled-components": "^7.0.8",
"lint-staged": "^12.5.0", "lint-staged": "^12.5.0",
"msw": "1.3.2", "msw": "1.3.2",
"npm-run-all": "latest",
"portfinder-sync": "^0.0.2", "portfinder-sync": "^0.0.2",
"prettier": "2.2.1", "prettier": "2.2.1",
"raw-loader": "4.0.2", "raw-loader": "4.0.2",
@ -216,8 +218,7 @@
"ts-node": "^10.2.1", "ts-node": "^10.2.1",
"typescript-plugin-css-modules": "5.0.1", "typescript-plugin-css-modules": "5.0.1",
"webpack-bundle-analyzer": "^4.5.0", "webpack-bundle-analyzer": "^4.5.0",
"webpack-cli": "^4.9.2", "webpack-cli": "^4.9.2"
"npm-run-all": "latest"
}, },
"lint-staged": { "lint-staged": {
"*.(js|jsx|ts|tsx)": [ "*.(js|jsx|ts|tsx)": [

View File

@ -0,0 +1 @@
<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#prefix__clip0_2022_1972)" stroke="#fff" stroke-width="1.333" stroke-linecap="round" stroke-linejoin="round"><path d="M6.667 2h.006M9.333 1.333h.007M1.333 6l13.334-3.333M8 8V4.333M11.333 8H4.667a2 2 0 00-2 2v2.667a2 2 0 002 2h6.666a2 2 0 002-2V10a2 2 0 00-2-2zM6 8v3.333M10 8v3.333M2.667 11.334h10.666"/></g><defs><clipPath id="prefix__clip0_2022_1972"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 507 B

View File

@ -0,0 +1 @@
<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><g stroke="#C0C1C3" stroke-width="1.333" stroke-linecap="round"><path d="M9.71 4.745a.576.576 0 000 .806l.922.922a.576.576 0 00.806 0l2.171-2.171a3.455 3.455 0 01-4.572 4.572l-3.98 3.98a1.222 1.222 0 11-1.727-1.728l3.98-3.98a3.455 3.455 0 014.572-4.572L9.717 4.739l-.006.006z" stroke-linejoin="round"/><path d="M4 7L2.527 5.566a1.333 1.333 0 01-.013-1.898l.81-.81a1.333 1.333 0 011.991.119L5.333 3M10.75 10.988l1.179 1.178m0 0l-.138.138a.833.833 0 00.387 1.397v0a.833.833 0 00.792-.219l.446-.446a.833.833 0 00.176-.917v0a.833.833 0 00-1.355-.261l-.308.308z"/></g></svg>

After

Width:  |  Height:  |  Size: 644 B

View File

@ -0,0 +1 @@
<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><g stroke="#C0C1C3" stroke-width="1.333" stroke-linecap="round" stroke-linejoin="round"><path d="M2 4.667V3.333C2 2.6 2.6 2 3.333 2h1.334M11.333 2h1.334C13.4 2 14 2.6 14 3.333v1.334M14 11.334v1.333C14 13.4 13.4 14 12.667 14h-1.334M4.667 14H3.333C2.6 14 2 13.4 2 12.667v-1.333M8.667 4.667H5.333a.667.667 0 00-.666.666v2c0 .368.298.667.666.667h3.334a.667.667 0 00.666-.667v-2a.667.667 0 00-.666-.667zM10.667 8H7.333a.667.667 0 00-.666.667v2c0 .368.298.666.666.666h3.334a.667.667 0 00.666-.666v-2A.667.667 0 0010.667 8z"/></g></svg>

After

Width:  |  Height:  |  Size: 604 B

View File

@ -0,0 +1 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M23.06 17.526c-1.281.668-7.916 3.396-9.328 4.132-1.413.736-2.198.73-3.314.196C9.303 21.32 2.242 18.468.97 17.86c-.636-.303-.97-.56-.97-.802v-2.426s9.192-2.001 10.676-2.534c1.484-.532 1.999-.551 3.262-.089 1.263.463 8.814 1.826 10.062 2.283v2.391c0 .24-.288.503-.94.843z" fill="#912626"/><path d="M23.06 15.114c-1.281.668-7.916 3.396-9.329 4.132-1.412.737-2.197.73-3.313.196C9.302 18.91 2.242 16.056.97 15.45c-1.272-.608-1.298-1.027-.049-1.516 1.25-.49 8.271-3.244 9.755-3.776 1.484-.533 1.999-.552 3.262-.09 1.263.463 7.858 3.088 9.106 3.546 1.248.457 1.296.834.015 1.501z" fill="#C6302B"/><path d="M23.06 13.6c-1.281.668-7.916 3.396-9.328 4.133-1.413.736-2.198.73-3.314.196S2.242 14.543.97 13.935c-.636-.304-.97-.56-.97-.802v-2.426s9.192-2.001 10.676-2.534c1.484-.532 1.999-.551 3.262-.089C15.2 8.547 22.752 9.91 24 10.366v2.392c0 .24-.288.503-.94.843z" fill="#912626"/><path d="M23.06 11.19c-1.281.667-7.916 3.395-9.329 4.131-1.412.737-2.197.73-3.313.196-1.116-.533-8.176-3.386-9.448-3.993-1.272-.608-1.298-1.027-.049-1.516 1.25-.49 8.271-3.244 9.755-3.776 1.484-.533 1.999-.552 3.262-.09 1.263.463 7.858 3.088 9.106 3.545 1.248.458 1.296.835.015 1.502z" fill="#C6302B"/><path d="M23.06 9.53c-1.281.668-7.916 3.396-9.328 4.132-1.413.737-2.198.73-3.314.196-1.116-.533-8.176-3.386-9.448-3.993C.334 9.56 0 9.305 0 9.062V6.636s9.192-2 10.676-2.533c1.484-.533 1.999-.552 3.262-.09C15.2 4.477 22.752 5.84 24 6.297v2.392c0 .24-.288.502-.94.842z" fill="#912626"/><path d="M23.06 7.118c-1.281.668-7.916 3.396-9.329 4.132-1.412.737-2.197.73-3.313.196C9.303 10.913 2.242 8.061.97 7.453-.302 6.845-.328 6.427.921 5.937c1.25-.489 8.271-3.244 9.755-3.776 1.484-.532 1.999-.552 3.262-.089 1.263.463 7.858 3.088 9.106 3.545 1.248.457 1.296.834.015 1.501z" fill="#C6302B"/><path d="M14.933 4.758l-2.064.215-.462 1.111-.746-1.24L9.28 4.63l1.778-.641-.534-.985 1.665.651 1.569-.513-.424 1.017 1.6.6zm-2.649 5.393l-3.85-1.597 5.517-.847-1.667 2.444zM6.945 5.376c1.63 0 2.95.512 2.95 1.143 0 .632-1.32 1.144-2.95 1.144-1.629 0-2.95-.512-2.95-1.144 0-.63 1.321-1.143 2.95-1.143z" fill="#fff"/><path d="M17.371 5.062l3.266 1.29-3.263 1.29-.003-2.58z" fill="#621B1C"/><path d="M13.758 6.492l3.613-1.43.003 2.58-.354.139-3.262-1.29z" fill="#9A2928"/></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -37,11 +37,16 @@
"text_condition1": "Send a notification when", "text_condition1": "Send a notification when",
"text_condition2": "the threshold", "text_condition2": "the threshold",
"text_condition3": "during the last", "text_condition3": "during the last",
"option_1min": "1 min",
"option_5min": "5 mins", "option_5min": "5 mins",
"option_10min": "10 mins", "option_10min": "10 mins",
"option_15min": "15 mins", "option_15min": "15 mins",
"option_30min": "30 mins",
"option_60min": "60 mins", "option_60min": "60 mins",
"option_4hours": "4 hours", "option_4hours": "4 hours",
"option_3hours": "3 hours",
"option_6hours": "6 hours",
"option_12hours": "12 hours",
"option_24hours": "24 hours", "option_24hours": "24 hours",
"field_threshold": "Alert Threshold", "field_threshold": "Alert Threshold",
"option_allthetimes": "all the times", "option_allthetimes": "all the times",
@ -111,5 +116,8 @@
"exceptions_based_alert": "Exceptions-based Alert", "exceptions_based_alert": "Exceptions-based Alert",
"exceptions_based_alert_desc": "Send a notification when a condition occurs in the exceptions data.", "exceptions_based_alert_desc": "Send a notification when a condition occurs in the exceptions data.",
"field_unit": "Threshold unit", "field_unit": "Threshold unit",
"text_alert_on_absent": "Send a notification if data is missing for",
"text_alert_frequency": "Run alert every",
"text_for": "minutes",
"selected_query_placeholder": "Select query" "selected_query_placeholder": "Select query"
} }

View File

@ -25,5 +25,5 @@
"dashboard_unsave_changes": "There are unsaved changes in the Query builder, please stage and run the query or the changes will be lost. Press OK to discard.", "dashboard_unsave_changes": "There are unsaved changes in the Query builder, please stage and run the query or the changes will be lost. Press OK to discard.",
"dashboard_save_changes": "Your graph built with {{queryTag}} query will be saved. Press OK to confirm.", "dashboard_save_changes": "Your graph built with {{queryTag}} query will be saved. Press OK to confirm.",
"your_graph_build_with": "Your graph built with", "your_graph_build_with": "Your graph built with",
"dashboar_ok_confirm": "query will be saved. Press OK to confirm." "dashboard_ok_confirm": "query will be saved. Press OK to confirm."
} }

View File

@ -14,6 +14,5 @@
"delete_domain_message": "Are you sure you want to delete this domain?", "delete_domain_message": "Are you sure you want to delete this domain?",
"delete_domain": "Delete Domain", "delete_domain": "Delete Domain",
"add_domain": "Add Domains", "add_domain": "Add Domains",
"saml_settings": "Your SAML settings have been saved, please login from incognito window to confirm that it has been set up correctly", "saml_settings": "Your SAML settings have been saved, please login from incognito window to confirm that it has been set up correctly"
"invite_link_share_manually": "After inviting members, please copy the invite link and send them the link manually"
} }

View File

@ -37,11 +37,16 @@
"text_condition1": "Send a notification when", "text_condition1": "Send a notification when",
"text_condition2": "the threshold", "text_condition2": "the threshold",
"text_condition3": "during the last", "text_condition3": "during the last",
"option_1min": "1 min",
"option_5min": "5 mins", "option_5min": "5 mins",
"option_10min": "10 mins", "option_10min": "10 mins",
"option_15min": "15 mins", "option_15min": "15 mins",
"option_30min": "30 mins",
"option_60min": "60 mins", "option_60min": "60 mins",
"option_3hours": "3 hours",
"option_4hours": "4 hours", "option_4hours": "4 hours",
"option_6hours": "6 hours",
"option_12hours": "12 hours",
"option_24hours": "24 hours", "option_24hours": "24 hours",
"field_threshold": "Alert Threshold", "field_threshold": "Alert Threshold",
"option_allthetimes": "all the times", "option_allthetimes": "all the times",
@ -111,5 +116,8 @@
"exceptions_based_alert": "Exceptions-based Alert", "exceptions_based_alert": "Exceptions-based Alert",
"exceptions_based_alert_desc": "Send a notification when a condition occurs in the exceptions data.", "exceptions_based_alert_desc": "Send a notification when a condition occurs in the exceptions data.",
"field_unit": "Threshold unit", "field_unit": "Threshold unit",
"text_alert_on_absent": "Send a notification if data is missing for",
"text_alert_frequency": "Run alert every",
"text_for": "minutes",
"selected_query_placeholder": "Select query" "selected_query_placeholder": "Select query"
} }

View File

@ -0,0 +1,14 @@
{
"days_remaining": "days remaining in your billing period.",
"billing": "Billing",
"manage_billing_and_costs": "Manage your billing information, invoices, and monitor costs.",
"enterprise_cloud": "Enterprise Cloud",
"enterprise": "Enterprise",
"card_details_recieved_and_billing_info": "We have received your card details, your billing will only start after the end of your free trial period.",
"upgrade_plan": "Upgrade Plan",
"manage_billing": "Manage Billing",
"upgrade_now_text": "Upgrade now to have uninterrupted access",
"billing_start_info": "Your billing will start only after the trial period",
"checkout_plans": "Check out features in paid plans",
"here": "here"
}

View File

@ -23,6 +23,12 @@
"field_opsgenie_api_key": "API Key", "field_opsgenie_api_key": "API Key",
"field_opsgenie_description": "Description", "field_opsgenie_description": "Description",
"placeholder_opsgenie_description": "Description", "placeholder_opsgenie_description": "Description",
"help_email_to": "Email address(es) to send alerts to (comma separated)",
"field_email_to": "To",
"placeholder_email_to": "To",
"help_email_html": "Send email in html format",
"field_email_html": "Email body template",
"placeholder_email_html": "Email body template",
"field_webhook_username": "User Name (optional)", "field_webhook_username": "User Name (optional)",
"field_webhook_password": "Password (optional)", "field_webhook_password": "Password (optional)",
"field_pager_routing_key": "Routing Key", "field_pager_routing_key": "Routing Key",

View File

@ -28,5 +28,5 @@
"dashboard_unsave_changes": "There are unsaved changes in the Query builder, please stage and run the query or the changes will be lost. Press OK to discard.", "dashboard_unsave_changes": "There are unsaved changes in the Query builder, please stage and run the query or the changes will be lost. Press OK to discard.",
"dashboard_save_changes": "Your graph built with {{queryTag}} query will be saved. Press OK to confirm.", "dashboard_save_changes": "Your graph built with {{queryTag}} query will be saved. Press OK to confirm.",
"your_graph_build_with": "Your graph built with", "your_graph_build_with": "Your graph built with",
"dashboar_ok_confirm": "query will be saved. Press OK to confirm." "dashboard_ok_confirm": "query will be saved. Press OK to confirm."
} }

View File

@ -14,6 +14,5 @@
"delete_domain_message": "Are you sure you want to delete this domain?", "delete_domain_message": "Are you sure you want to delete this domain?",
"delete_domain": "Delete Domain", "delete_domain": "Delete Domain",
"add_domain": "Add Domains", "add_domain": "Add Domains",
"saml_settings": "Your SAML settings have been saved, please login from incognito window to confirm that it has been set up correctly", "saml_settings": "Your SAML settings have been saved, please login from incognito window to confirm that it has been set up correctly"
"invite_link_share_manually": "After inviting members, please copy the invite link and send them the link manually"
} }

View File

@ -4,6 +4,10 @@
"SERVICE_METRICS": "SigNoz | Service Metrics", "SERVICE_METRICS": "SigNoz | Service Metrics",
"SERVICE_MAP": "SigNoz | Service Map", "SERVICE_MAP": "SigNoz | Service Map",
"GET_STARTED": "SigNoz | Get Started", "GET_STARTED": "SigNoz | Get Started",
"GET_STARTED_APPLICATION_MONITORING": "SigNoz | Get Started | APM",
"GET_STARTED_LOGS_MANAGEMENT": "SigNoz | Get Started | Logs",
"GET_STARTED_INFRASTRUCTURE_MONITORING": "SigNoz | Get Started | Infrastructure",
"GET_STARTED_AWS_MONITORING": "SigNoz | Get Started | AWS",
"TRACE": "SigNoz | Trace", "TRACE": "SigNoz | Trace",
"TRACE_DETAIL": "SigNoz | Trace Detail", "TRACE_DETAIL": "SigNoz | Trace Detail",
"TRACES_EXPLORER": "SigNoz | Traces Explorer", "TRACES_EXPLORER": "SigNoz | Traces Explorer",
@ -40,8 +44,9 @@
"LIST_LICENSES": "SigNoz | List of Licenses", "LIST_LICENSES": "SigNoz | List of Licenses",
"WORKSPACE_LOCKED": "SigNoz | Workspace Locked", "WORKSPACE_LOCKED": "SigNoz | Workspace Locked",
"SUPPORT": "SigNoz | Support", "SUPPORT": "SigNoz | Support",
"LOGS_SAVE_VIEWS": "SigNoz | Logs Save Views", "LOGS_SAVE_VIEWS": "SigNoz | Logs Saved Views",
"TRACES_SAVE_VIEWS": "SigNoz | Traces Save Views", "TRACES_SAVE_VIEWS": "SigNoz | Traces Saved Views",
"DEFAULT": "Open source Observability Platform | SigNoz", "DEFAULT": "Open source Observability Platform | SigNoz",
"SHORTCUTS": "SigNoz | Shortcuts" "SHORTCUTS": "SigNoz | Shortcuts",
"INTEGRATIONS_INSTALLED": "SigNoz | Integrations"
} }

View File

@ -9,7 +9,7 @@ done
# create temporary tsconfig which includes only passed files # create temporary tsconfig which includes only passed files
str="{ str="{
\"extends\": \"./tsconfig.json\", \"extends\": \"./tsconfig.json\",
\"include\": [\"src/types/global.d.ts\",\"src/typings/window.ts\", \"src/typings/chartjs-adapter-date-fns.d.ts\", \"src/typings/environment.ts\" ,\"src/container/OnboardingContainer/typings.d.ts\",$files] \"include\": [ \"src/typings/**/*.ts\",\"src/**/*.d.ts\", \"./babel.config.js\", \"./jest.config.ts\", \"./.eslintrc.js\",\"./__mocks__\",\"./conf/default.conf\",\"./public\",\"./tests\",\"./playwright.config.ts\",\"./commitlint.config.ts\",\"./webpack.config.js\",\"./webpack.config.prod.js\",\"./jest.setup.ts\",\"./**/*.d.ts\",$files]
}" }"
echo $str > tsconfig.tmp echo $str > tsconfig.tmp

View File

@ -190,3 +190,18 @@ export const WorkspaceBlocked = Loadable(
export const ShortcutsPage = Loadable( export const ShortcutsPage = Loadable(
() => import(/* webpackChunkName: "ShortcutsPage" */ 'pages/Shortcuts'), () => import(/* webpackChunkName: "ShortcutsPage" */ 'pages/Shortcuts'),
); );
export const InstalledIntegrations = Loadable(
() =>
import(
/* webpackChunkName: "InstalledIntegrations" */ 'pages/IntegrationsModulePage'
),
);
export const IntegrationsMarketPlace = Loadable(
// eslint-disable-next-line sonarjs/no-identical-functions
() =>
import(
/* webpackChunkName: "IntegrationsMarketPlace" */ 'pages/IntegrationsModulePage'
),
);

View File

@ -1,6 +1,4 @@
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import Shortcuts from 'pages/Shortcuts/Shortcuts';
import WorkspaceBlocked from 'pages/WorkspaceLocked';
import { RouteProps } from 'react-router-dom'; import { RouteProps } from 'react-router-dom';
import { import {
@ -16,6 +14,8 @@ import {
EditRulesPage, EditRulesPage,
ErrorDetails, ErrorDetails,
IngestionSettings, IngestionSettings,
InstalledIntegrations,
IntegrationsMarketPlace,
LicensePage, LicensePage,
ListAllALertsPage, ListAllALertsPage,
LiveLogs, LiveLogs,
@ -35,6 +35,7 @@ import {
ServiceMetricsPage, ServiceMetricsPage,
ServicesTablePage, ServicesTablePage,
SettingsPage, SettingsPage,
ShortcutsPage,
SignupPage, SignupPage,
SomethingWentWrong, SomethingWentWrong,
StatusPage, StatusPage,
@ -45,6 +46,7 @@ import {
TracesSaveViews, TracesSaveViews,
UnAuthorized, UnAuthorized,
UsageExplorerPage, UsageExplorerPage,
WorkspaceBlocked,
} from './pageComponents'; } from './pageComponents';
const routes: AppRoutes[] = [ const routes: AppRoutes[] = [
@ -57,7 +59,7 @@ const routes: AppRoutes[] = [
}, },
{ {
path: ROUTES.GET_STARTED, path: ROUTES.GET_STARTED,
exact: true, exact: false,
component: Onboarding, component: Onboarding,
isPrivate: true, isPrivate: true,
key: 'GET_STARTED', key: 'GET_STARTED',
@ -331,10 +333,24 @@ const routes: AppRoutes[] = [
{ {
path: ROUTES.SHORTCUTS, path: ROUTES.SHORTCUTS,
exact: true, exact: true,
component: Shortcuts, component: ShortcutsPage,
isPrivate: true, isPrivate: true,
key: 'SHORTCUTS', key: 'SHORTCUTS',
}, },
{
path: ROUTES.INTEGRATIONS_INSTALLED,
exact: true,
component: InstalledIntegrations,
isPrivate: true,
key: 'INTEGRATIONS_INSTALLED',
},
{
path: ROUTES.INTEGRATIONS_MARKETPLACE,
exact: true,
component: IntegrationsMarketPlace,
isPrivate: true,
key: 'INTEGRATIONS_MARKETPLACE',
},
]; ];
export const SUPPORT_ROUTE: AppRoutes = { export const SUPPORT_ROUTE: AppRoutes = {
@ -358,6 +374,8 @@ export const oldRoutes = [
'/logs/old-logs-explorer', '/logs/old-logs-explorer',
'/logs-explorer', '/logs-explorer',
'/logs-explorer/live', '/logs-explorer/live',
'/logs-save-views',
'/traces-save-views',
'/settings/api-keys', '/settings/api-keys',
]; ];
@ -366,6 +384,8 @@ export const oldNewRoutesMapping: Record<string, string> = {
'/logs/old-logs-explorer': '/logs/old-logs-explorer', '/logs/old-logs-explorer': '/logs/old-logs-explorer',
'/logs-explorer': '/logs/logs-explorer', '/logs-explorer': '/logs/logs-explorer',
'/logs-explorer/live': '/logs/logs-explorer/live', '/logs-explorer/live': '/logs/logs-explorer/live',
'/logs-save-views': '/logs/saved-views',
'/traces-save-views': '/traces/saved-views',
'/settings/api-keys': '/settings/access-tokens', '/settings/api-keys': '/settings/access-tokens',
}; };

View File

@ -0,0 +1,7 @@
import axios from 'api';
import { AxiosResponse } from 'axios';
import { AllIntegrationsProps } from 'types/api/integrations/types';
export const getAllIntegrations = (): Promise<
AxiosResponse<AllIntegrationsProps>
> => axios.get(`/integrations`);

View File

@ -0,0 +1,11 @@
import axios from 'api';
import { AxiosResponse } from 'axios';
import {
GetIntegrationPayloadProps,
GetIntegrationProps,
} from 'types/api/integrations/types';
export const getIntegration = (
props: GetIntegrationPayloadProps,
): Promise<AxiosResponse<GetIntegrationProps>> =>
axios.get(`/integrations/${props.integrationId}`);

View File

@ -0,0 +1,11 @@
import axios from 'api';
import { AxiosResponse } from 'axios';
import {
GetIntegrationPayloadProps,
GetIntegrationStatusProps,
} from 'types/api/integrations/types';
export const getIntegrationStatus = (
props: GetIntegrationPayloadProps,
): Promise<AxiosResponse<GetIntegrationStatusProps>> =>
axios.get(`/integrations/${props.integrationId}/connection_status`);

View File

@ -0,0 +1,31 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
InstalledIntegrationsSuccessResponse,
InstallIntegrationKeyProps,
} from 'types/api/integrations/types';
const installIntegration = async (
props: InstallIntegrationKeyProps,
): Promise<
SuccessResponse<InstalledIntegrationsSuccessResponse> | ErrorResponse
> => {
try {
const response = await axios.post('/integrations/install', {
...props,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default installIntegration;

View File

@ -0,0 +1,31 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
UninstallIntegrationProps,
UninstallIntegrationSuccessResponse,
} from 'types/api/integrations/types';
const unInstallIntegration = async (
props: UninstallIntegrationProps,
): Promise<
SuccessResponse<UninstallIntegrationSuccessResponse> | ErrorResponse
> => {
try {
const response = await axios.post('/integrations/uninstall', {
...props,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default unInstallIntegration;

View File

@ -8,7 +8,7 @@ const listAllDomain = async (
props: Props, props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => { ): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try { try {
const response = await axios.get(`orgs/${props.orgId}/domains`); const response = await axios.get(`/orgs/${props.orgId}/domains`);
return { return {
statusCode: 200, statusCode: 200,

View File

@ -2,6 +2,7 @@ const apiV1 = '/api/v1/';
export const apiV2 = '/api/v2/'; export const apiV2 = '/api/v2/';
export const apiV3 = '/api/v3/'; export const apiV3 = '/api/v3/';
export const apiV4 = '/api/v4/';
export const apiAlertManager = '/api/alertmanager'; export const apiAlertManager = '/api/alertmanager';
export default apiV1; export default apiV1;

View File

@ -13,6 +13,7 @@ export interface UsageResponsePayloadProps {
billTotal: number; billTotal: number;
}; };
discount: number; discount: number;
subscriptionStatus?: string;
} }
const getUsage = async ( const getUsage = async (

View File

@ -0,0 +1,34 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/channels/createEmail';
const create = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.post('/channels', {
name: props.name,
email_configs: [
{
send_resolved: true,
to: props.to,
html: props.html,
headers: props.headers,
},
],
});
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default create;

View File

@ -0,0 +1,34 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/channels/editEmail';
const editEmail = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.put(`/channels/${props.id}`, {
name: props.name,
email_configs: [
{
send_resolved: true,
to: props.to,
html: props.html,
headers: props.headers,
},
],
});
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default editEmail;

View File

@ -0,0 +1,34 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/channels/createEmail';
const testEmail = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.post('/testChannel', {
name: props.name,
email_configs: [
{
send_resolved: true,
to: props.to,
html: props.html,
headers: props.headers,
},
],
});
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default testEmail;

View File

@ -9,7 +9,7 @@ import { ENVIRONMENT } from 'constants/env';
import { LOCALSTORAGE } from 'constants/localStorage'; import { LOCALSTORAGE } from 'constants/localStorage';
import store from 'store'; import store from 'store';
import apiV1, { apiAlertManager, apiV2, apiV3 } from './apiV1'; import apiV1, { apiAlertManager, apiV2, apiV3, apiV4 } from './apiV1';
import { Logout } from './utils'; import { Logout } from './utils';
const interceptorsResponse = ( const interceptorsResponse = (
@ -114,6 +114,7 @@ ApiV2Instance.interceptors.request.use(interceptorsRequestResponse);
export const ApiV3Instance = axios.create({ export const ApiV3Instance = axios.create({
baseURL: `${ENVIRONMENT.baseURL}${apiV3}`, baseURL: `${ENVIRONMENT.baseURL}${apiV3}`,
}); });
ApiV3Instance.interceptors.response.use( ApiV3Instance.interceptors.response.use(
interceptorsResponse, interceptorsResponse,
interceptorRejected, interceptorRejected,
@ -121,6 +122,18 @@ ApiV3Instance.interceptors.response.use(
ApiV3Instance.interceptors.request.use(interceptorsRequestResponse); ApiV3Instance.interceptors.request.use(interceptorsRequestResponse);
// //
// axios V4
export const ApiV4Instance = axios.create({
baseURL: `${ENVIRONMENT.baseURL}${apiV4}`,
});
ApiV4Instance.interceptors.response.use(
interceptorsResponse,
interceptorRejected,
);
ApiV4Instance.interceptors.request.use(interceptorsRequestResponse);
//
AxiosAlertManagerInstance.interceptors.response.use( AxiosAlertManagerInstance.interceptors.response.use(
interceptorsResponse, interceptorsResponse,
interceptorRejected, interceptorRejected,

View File

@ -1,6 +1,7 @@
import { ApiV3Instance as axios } from 'api'; import { ApiV3Instance, ApiV4Instance } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { ErrorResponse, SuccessResponse } from 'types/api'; import { ErrorResponse, SuccessResponse } from 'types/api';
import { import {
MetricRangePayloadV3, MetricRangePayloadV3,
@ -9,10 +10,23 @@ import {
export const getMetricsQueryRange = async ( export const getMetricsQueryRange = async (
props: QueryRangePayload, props: QueryRangePayload,
version: string,
signal: AbortSignal, signal: AbortSignal,
): Promise<SuccessResponse<MetricRangePayloadV3> | ErrorResponse> => { ): Promise<SuccessResponse<MetricRangePayloadV3> | ErrorResponse> => {
try { try {
const response = await axios.post('/query_range', props, { signal }); if (version && version === ENTITY_VERSION_V4) {
const response = await ApiV4Instance.post('/query_range', props, { signal });
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
params: props,
};
}
const response = await ApiV3Instance.post('/query_range', props, { signal });
return { return {
statusCode: 200, statusCode: 200,

View File

@ -24,7 +24,7 @@ export const getAggregateAttribute = async ({
const response: AxiosResponse<{ const response: AxiosResponse<{
data: IQueryAutocompleteResponse; data: IQueryAutocompleteResponse;
}> = await ApiV3Instance.get( }> = await ApiV3Instance.get(
`autocomplete/aggregate_attributes?${createQueryParams({ `/autocomplete/aggregate_attributes?${createQueryParams({
aggregateOperator, aggregateOperator,
searchText, searchText,
dataSource, dataSource,

View File

@ -25,7 +25,7 @@ export const getAggregateKeys = async ({
const response: AxiosResponse<{ const response: AxiosResponse<{
data: IQueryAutocompleteResponse; data: IQueryAutocompleteResponse;
}> = await ApiV3Instance.get( }> = await ApiV3Instance.get(
`autocomplete/attribute_keys?${createQueryParams({ `/autocomplete/attribute_keys?${createQueryParams({
aggregateOperator, aggregateOperator,
searchText, searchText,
dataSource, dataSource,

View File

@ -2,4 +2,4 @@ import axios from 'api';
import { DeleteViewPayloadProps } from 'types/api/saveViews/types'; import { DeleteViewPayloadProps } from 'types/api/saveViews/types';
export const deleteView = (uuid: string): Promise<DeleteViewPayloadProps> => export const deleteView = (uuid: string): Promise<DeleteViewPayloadProps> =>
axios.delete(`explorer/views/${uuid}`); axios.delete(`/explorer/views/${uuid}`);

View File

@ -6,4 +6,4 @@ import { DataSource } from 'types/common/queryBuilder';
export const getAllViews = ( export const getAllViews = (
sourcepage: DataSource, sourcepage: DataSource,
): Promise<AxiosResponse<AllViewsProps>> => ): Promise<AxiosResponse<AllViewsProps>> =>
axios.get(`explorer/views?sourcePage=${sourcepage}`); axios.get(`/explorer/views?sourcePage=${sourcepage}`);

View File

@ -8,7 +8,7 @@ export const saveView = ({
viewName, viewName,
extraData, extraData,
}: SaveViewProps): Promise<AxiosResponse<SaveViewPayloadProps>> => }: SaveViewProps): Promise<AxiosResponse<SaveViewPayloadProps>> =>
axios.post('explorer/views', { axios.post('/explorer/views', {
name: viewName, name: viewName,
sourcePage, sourcePage,
compositeQuery, compositeQuery,

View File

@ -11,7 +11,7 @@ export const updateView = ({
sourcePage, sourcePage,
viewKey, viewKey,
}: UpdateViewProps): Promise<UpdateViewPayloadProps> => }: UpdateViewProps): Promise<UpdateViewPayloadProps> =>
axios.put(`explorer/views/${viewKey}`, { axios.put(`/explorer/views/${viewKey}`, {
name: viewName, name: viewName,
compositeQuery, compositeQuery,
extraData, extraData,

View File

@ -0,0 +1,23 @@
import { Color } from '@signozhq/design-tokens';
import { useIsDarkMode } from 'hooks/useDarkMode';
function ConfigureIcon(): JSX.Element {
const isDarkMode = useIsDarkMode();
return (
<svg width="14" height="14" fill="none" xmlns="http://www.w3.org/2000/svg">
<g
stroke={isDarkMode ? Color.BG_VANILLA_100 : Color.BG_INK_500}
strokeWidth="1.333"
strokeLinecap="round"
>
<path
d="M9.71 4.745a.576.576 0 000 .806l.922.922a.576.576 0 00.806 0l2.171-2.171a3.455 3.455 0 01-4.572 4.572l-3.98 3.98a1.222 1.222 0 11-1.727-1.728l3.98-3.98a3.455 3.455 0 014.572-4.572L9.717 4.739l-.006.006z"
strokeLinejoin="round"
/>
<path d="M4 7L2.527 5.566a1.333 1.333 0 01-.013-1.898l.81-.81a1.333 1.333 0 011.991.119L5.333 3M10.75 10.988l1.179 1.178m0 0l-.138.138a.833.833 0 00.387 1.397v0a.833.833 0 00.792-.219l.446-.446a.833.833 0 00.176-.917v0a.833.833 0 00-1.355-.261l-.308.308z" />
</g>
</svg>
);
}
export default ConfigureIcon;

View File

@ -115,6 +115,9 @@ function CustomTimePicker({
const handleOpenChange = (newOpen: boolean): void => { const handleOpenChange = (newOpen: boolean): void => {
setOpen(newOpen); setOpen(newOpen);
if (!newOpen) {
setCustomDTPickerVisible?.(false);
}
}; };
const debouncedHandleInputChange = debounce((inputValue): void => { const debouncedHandleInputChange = debounce((inputValue): void => {

View File

@ -1,6 +1,6 @@
import './CustomTimePicker.styles.scss'; import './CustomTimePicker.styles.scss';
import { Button, DatePicker } from 'antd'; import { Button } from 'antd';
import cx from 'classnames'; import cx from 'classnames';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal'; import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
@ -9,12 +9,10 @@ import {
Option, Option,
RelativeDurationSuggestionOptions, RelativeDurationSuggestionOptions,
} from 'container/TopNav/DateTimeSelectionV2/config'; } from 'container/TopNav/DateTimeSelectionV2/config';
import dayjs, { Dayjs } from 'dayjs';
import { Dispatch, SetStateAction, useMemo } from 'react'; import { Dispatch, SetStateAction, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime'; import RangePickerModal from './RangePickerModal';
interface CustomTimePickerPopoverContentProps { interface CustomTimePickerPopoverContentProps {
options: any[]; options: any[];
@ -40,35 +38,12 @@ function CustomTimePickerPopoverContent({
handleGoLive, handleGoLive,
selectedTime, selectedTime,
}: CustomTimePickerPopoverContentProps): JSX.Element { }: CustomTimePickerPopoverContentProps): JSX.Element {
const { RangePicker } = DatePicker;
const { pathname } = useLocation(); const { pathname } = useLocation();
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const isLogsExplorerPage = useMemo(() => pathname === ROUTES.LOGS_EXPLORER, [ const isLogsExplorerPage = useMemo(() => pathname === ROUTES.LOGS_EXPLORER, [
pathname, pathname,
]); ]);
const disabledDate = (current: Dayjs): boolean => {
const currentDay = dayjs(current);
return currentDay.isAfter(dayjs());
};
const onPopoverClose = (visible: boolean): void => {
if (!visible) {
setCustomDTPickerVisible(false);
}
setIsOpen(visible);
};
const onModalOkHandler = (date_time: any): void => {
if (date_time?.[1]) {
onPopoverClose(false);
}
onCustomDateHandler(date_time, LexicalContext.CUSTOM_DATE_PICKER);
};
function getTimeChips(options: Option[]): JSX.Element { function getTimeChips(options: Option[]): JSX.Element {
return ( return (
<div className="relative-date-time-section"> <div className="relative-date-time-section">
@ -105,26 +80,32 @@ function CustomTimePickerPopoverContent({
}} }}
className={cx( className={cx(
'date-time-options-btn', 'date-time-options-btn',
selectedTime === option.value && 'active', customDateTimeVisible
? option.value === 'custom' && 'active'
: selectedTime === option.value && 'active',
)} )}
> >
{option.label} {option.label}
</Button> </Button>
))} ))}
</div> </div>
<div className="relative-date-time"> <div
className={cx(
'relative-date-time',
selectedTime === 'custom' || customDateTimeVisible
? 'date-picker'
: 'relative-times',
)}
>
{selectedTime === 'custom' || customDateTimeVisible ? ( {selectedTime === 'custom' || customDateTimeVisible ? (
<RangePicker <RangePickerModal
disabledDate={disabledDate} setCustomDTPickerVisible={setCustomDTPickerVisible}
allowClear setIsOpen={setIsOpen}
onCalendarChange={onModalOkHandler} onCustomDateHandler={onCustomDateHandler}
// eslint-disable-next-line react/jsx-props-no-spreading selectedTime={selectedTime}
{...(selectedTime === 'custom' && {
defaultValue: [dayjs(minTime / 1000000), dayjs(maxTime / 1000000)],
})}
/> />
) : ( ) : (
<div> <div className="relative-times-container">
<div className="time-heading">RELATIVE TIMES</div> <div className="time-heading">RELATIVE TIMES</div>
<div>{getTimeChips(RelativeDurationSuggestionOptions)}</div> <div>{getTimeChips(RelativeDurationSuggestionOptions)}</div>
</div> </div>

View File

@ -0,0 +1,4 @@
.custom-date-picker {
display: flex;
flex-direction: column;
}

View File

@ -0,0 +1,68 @@
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 { Dispatch, SetStateAction } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
interface RangePickerModalProps {
setCustomDTPickerVisible: Dispatch<SetStateAction<boolean>>;
setIsOpen: Dispatch<SetStateAction<boolean>>;
onCustomDateHandler: (
dateTimeRange: DateTimeRangeType,
lexicalContext?: LexicalContext | undefined,
) => void;
selectedTime: string;
}
function RangePickerModal(props: RangePickerModalProps): JSX.Element {
const {
setCustomDTPickerVisible,
setIsOpen,
onCustomDateHandler,
selectedTime,
} = props;
const { RangePicker } = DatePicker;
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const disabledDate = (current: Dayjs): boolean => {
const currentDay = dayjs(current);
return currentDay.isAfter(dayjs());
};
const onPopoverClose = (visible: boolean): void => {
if (!visible) {
setCustomDTPickerVisible(false);
}
setIsOpen(visible);
};
const onModalOkHandler = (date_time: any): void => {
if (date_time?.[1]) {
onPopoverClose(false);
}
onCustomDateHandler(date_time, LexicalContext.CUSTOM_DATE_PICKER);
};
return (
<div className="custom-date-picker">
<RangePicker
disabledDate={disabledDate}
allowClear
showTime
onOk={onModalOkHandler}
// eslint-disable-next-line react/jsx-props-no-spreading
{...(selectedTime === 'custom' && {
defaultValue: [dayjs(minTime / 1000000), dayjs(maxTime / 1000000)],
})}
/>
</div>
);
}
export default RangePickerModal;

View File

@ -18,6 +18,8 @@
} }
.ant-drawer-body { .ant-drawer-body {
display: flex;
flex-direction: column;
padding: 16px; padding: 16px;
} }

View File

@ -1,10 +1,13 @@
.query-builder-search-wrapper { .query-builder-search-wrapper {
margin-top: 10px; margin-top: 10px;
height: 46px; border: 1px solid var(--bg-slate-400);
border: 1px solid var(--bg-slate-400); border-bottom: none;
border-bottom: none;
.ant-select-selector { .ant-select-selector {
border: none !important; border: none !important;
}
input {
font-size: 12px;
}
}
} }

View File

@ -9,6 +9,7 @@ import dayjs from 'dayjs';
import dompurify from 'dompurify'; import dompurify from 'dompurify';
import { useActiveLog } from 'hooks/logs/useActiveLog'; import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink'; import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useIsDarkMode } from 'hooks/useDarkMode';
// utils // utils
import { FlatLogData } from 'lib/logs/flatLogData'; import { FlatLogData } from 'lib/logs/flatLogData';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
@ -19,9 +20,8 @@ import { ILog } from 'types/api/logs/log';
// components // components
import AddToQueryHOC, { AddToQueryHOCProps } from '../AddToQueryHOC'; import AddToQueryHOC, { AddToQueryHOCProps } from '../AddToQueryHOC';
import LogLinesActionButtons from '../LogLinesActionButtons/LogLinesActionButtons'; import LogLinesActionButtons from '../LogLinesActionButtons/LogLinesActionButtons';
import LogStateIndicator, { import LogStateIndicator from '../LogStateIndicator/LogStateIndicator';
LogType, import { getLogIndicatorType } from '../LogStateIndicator/utils';
} from '../LogStateIndicator/LogStateIndicator';
// styles // styles
import { import {
Container, Container,
@ -37,12 +37,17 @@ const convert = new Convert();
interface LogFieldProps { interface LogFieldProps {
fieldKey: string; fieldKey: string;
fieldValue: string; fieldValue: string;
linesPerRow?: number;
} }
type LogSelectedFieldProps = LogFieldProps & type LogSelectedFieldProps = Omit<LogFieldProps, 'linesPerRow'> &
Pick<AddToQueryHOCProps, 'onAddToQuery'>; Pick<AddToQueryHOCProps, 'onAddToQuery'>;
function LogGeneralField({ fieldKey, fieldValue }: LogFieldProps): JSX.Element { function LogGeneralField({
fieldKey,
fieldValue,
linesPerRow = 1,
}: LogFieldProps): JSX.Element {
const html = useMemo( const html = useMemo(
() => ({ () => ({
__html: convert.toHtml(dompurify.sanitize(fieldValue)), __html: convert.toHtml(dompurify.sanitize(fieldValue)),
@ -55,7 +60,11 @@ function LogGeneralField({ fieldKey, fieldValue }: LogFieldProps): JSX.Element {
<Text ellipsis type="secondary" className="log-field-key"> <Text ellipsis type="secondary" className="log-field-key">
{`${fieldKey} : `} {`${fieldKey} : `}
</Text> </Text>
<LogText dangerouslySetInnerHTML={html} className="log-value" /> <LogText
dangerouslySetInnerHTML={html}
className="log-value"
linesPerRow={linesPerRow > 1 ? linesPerRow : undefined}
/>
</TextContainer> </TextContainer>
); );
} }
@ -92,6 +101,7 @@ type ListLogViewProps = {
onSetActiveLog: (log: ILog) => void; onSetActiveLog: (log: ILog) => void;
onAddToQuery: AddToQueryHOCProps['onAddToQuery']; onAddToQuery: AddToQueryHOCProps['onAddToQuery'];
activeLog?: ILog | null; activeLog?: ILog | null;
linesPerRow: number;
}; };
function ListLogView({ function ListLogView({
@ -100,6 +110,7 @@ function ListLogView({
onSetActiveLog, onSetActiveLog,
onAddToQuery, onAddToQuery,
activeLog, activeLog,
linesPerRow,
}: ListLogViewProps): JSX.Element { }: ListLogViewProps): JSX.Element {
const flattenLogData = useMemo(() => FlatLogData(logData), [logData]); const flattenLogData = useMemo(() => FlatLogData(logData), [logData]);
@ -114,6 +125,8 @@ function ListLogView({
onClearActiveLog: handleClearActiveContextLog, onClearActiveLog: handleClearActiveContextLog,
} = useActiveLog(); } = useActiveLog();
const isDarkMode = useIsDarkMode();
const handlerClearActiveContextLog = useCallback( const handlerClearActiveContextLog = useCallback(
(event: React.MouseEvent | React.KeyboardEvent) => { (event: React.MouseEvent | React.KeyboardEvent) => {
event.preventDefault(); event.preventDefault();
@ -149,7 +162,7 @@ function ListLogView({
[flattenLogData.timestamp], [flattenLogData.timestamp],
); );
const logType = logData?.attributes_string?.log_level || LogType.INFO; const logType = getLogIndicatorType(logData);
const handleMouseEnter = (): void => { const handleMouseEnter = (): void => {
setHasActionButtons(true); setHasActionButtons(true);
@ -163,6 +176,7 @@ function ListLogView({
<> <>
<Container <Container
$isActiveLog={isHighlighted} $isActiveLog={isHighlighted}
$isDarkMode={isDarkMode}
onMouseEnter={handleMouseEnter} onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave} onMouseLeave={handleMouseLeave}
onClick={handleDetailedView} onClick={handleDetailedView}
@ -176,7 +190,11 @@ function ListLogView({
/> />
<div> <div>
<LogContainer> <LogContainer>
<LogGeneralField fieldKey="Log" fieldValue={flattenLogData.body} /> <LogGeneralField
fieldKey="Log"
fieldValue={flattenLogData.body}
linesPerRow={linesPerRow}
/>
{flattenLogData.stream && ( {flattenLogData.stream && (
<LogGeneralField fieldKey="Stream" fieldValue={flattenLogData.stream} /> <LogGeneralField fieldKey="Stream" fieldValue={flattenLogData.stream} />
)} )}
@ -219,4 +237,8 @@ ListLogView.defaultProps = {
activeLog: null, activeLog: null,
}; };
LogGeneralField.defaultProps = {
linesPerRow: 1,
};
export default ListLogView; export default ListLogView;

View File

@ -1,23 +1,33 @@
import { Color } from '@signozhq/design-tokens';
import { Card, Typography } from 'antd'; import { Card, Typography } from 'antd';
import styled from 'styled-components'; import styled from 'styled-components';
import { getActiveLogBackground } from 'utils/logs';
interface LogTextProps {
linesPerRow?: number;
}
export const Container = styled(Card)<{ export const Container = styled(Card)<{
$isActiveLog: boolean; $isActiveLog: boolean;
$isDarkMode: boolean;
}>` }>`
width: 100% !important; width: 100% !important;
margin-bottom: 0.3rem; margin-bottom: 0.3rem;
cursor: pointer; cursor: pointer;
.ant-card-body { .ant-card-body {
padding: 0.3rem 0.6rem; padding: 0.3rem 0.6rem;
}
${({ $isActiveLog }): string => getActiveLogBackground($isActiveLog)} ${({ $isActiveLog, $isDarkMode }): string =>
$isActiveLog
? `background-color: ${
$isDarkMode ? Color.BG_SLATE_500 : Color.BG_VANILLA_300
} !important`
: ''}
}
`; `;
export const Text = styled(Typography.Text)` export const Text = styled(Typography.Text)`
&&& { &&& {
min-width: 1.5rem; min-width: 2.5rem;
white-space: nowrap; white-space: nowrap;
} }
`; `;
@ -35,11 +45,19 @@ export const LogContainer = styled.div`
gap: 6px; gap: 6px;
`; `;
export const LogText = styled.div` export const LogText = styled.div<LogTextProps>`
display: inline-block; display: inline-block;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
white-space: nowrap; ${({ linesPerRow }): string =>
linesPerRow
? `-webkit-line-clamp: ${linesPerRow};
line-clamp: ${linesPerRow};
display: -webkit-box;
-webkit-box-orient: vertical;
white-space: normal; `
: 'white-space: nowrap;'};
};
`; `;
export const SelectedLog = styled.div` export const SelectedLog = styled.div`

View File

@ -10,15 +10,27 @@
background-color: transparent; background-color: transparent;
&.INFO { &.INFO {
background-color: #1d212d; background-color: var(--bg-slate-400);
} }
&.WARNING { &.WARNING, &.WARN {
background-color: #ffcd56; background-color: var(--bg-amber-500);
} }
&.ERROR { &.ERROR {
background-color: #e5484d; background-color: var(--bg-cherry-500);
}
&.TRACE {
background-color: var(--bg-robin-300);
}
&.DEBUG {
background-color: var(--bg-forest-500);
}
&.FATAL {
background-color: var(--bg-sakura-500);
} }
} }

View File

@ -0,0 +1,45 @@
import { render } from '@testing-library/react';
import LogStateIndicator from './LogStateIndicator';
describe('LogStateIndicator', () => {
it('renders correctly with default props', () => {
const { container } = render(<LogStateIndicator type="INFO" />);
const indicator = container.firstChild as HTMLElement;
expect(indicator.classList.contains('log-state-indicator')).toBe(true);
expect(indicator.classList.contains('isActive')).toBe(false);
expect(container.querySelector('.line')).toBeTruthy();
expect(container.querySelector('.line')?.classList.contains('INFO')).toBe(
true,
);
});
it('renders correctly when isActive is true', () => {
const { container } = render(<LogStateIndicator type="INFO" isActive />);
const indicator = container.firstChild as HTMLElement;
expect(indicator.classList.contains('isActive')).toBe(true);
});
it('renders correctly with different types', () => {
const { container: containerInfo } = render(
<LogStateIndicator type="INFO" />,
);
expect(containerInfo.querySelector('.line')?.classList.contains('INFO')).toBe(
true,
);
const { container: containerWarning } = render(
<LogStateIndicator type="WARNING" />,
);
expect(
containerWarning.querySelector('.line')?.classList.contains('WARNING'),
).toBe(true);
const { container: containerError } = render(
<LogStateIndicator type="ERROR" />,
);
expect(
containerError.querySelector('.line')?.classList.contains('ERROR'),
).toBe(true);
});
});

View File

@ -2,11 +2,40 @@ import './LogStateIndicator.styles.scss';
import cx from 'classnames'; import cx from 'classnames';
export const SEVERITY_TEXT_TYPE = {
TRACE: 'TRACE',
TRACE2: 'TRACE2',
TRACE3: 'TRACE3',
TRACE4: 'TRACE4',
DEBUG: 'DEBUG',
DEBUG2: 'DEBUG2',
DEBUG3: 'DEBUG3',
DEBUG4: 'DEBUG4',
INFO: 'INFO',
INFO2: 'INFO2',
INFO3: 'INFO3',
INFO4: 'INFO4',
WARN: 'WARN',
WARN2: 'WARN2',
WARN3: 'WARN3',
WARN4: 'WARN4',
WARNING: 'WARNING',
ERROR: 'ERROR',
ERROR2: 'ERROR2',
ERROR3: 'ERROR3',
ERROR4: 'ERROR4',
FATAL: 'FATAL',
FATAL2: 'FATAL2',
FATAL3: 'FATAL3',
FATAL4: 'FATAL4',
} as const;
export const LogType = { export const LogType = {
INFO: 'INFO', INFO: 'INFO',
WARNING: 'WARNING', WARNING: 'WARNING',
ERROR: 'ERROR', ERROR: 'ERROR',
}; } as const;
function LogStateIndicator({ function LogStateIndicator({
type, type,
isActive, isActive,

View File

@ -0,0 +1,89 @@
import { ILog } from 'types/api/logs/log';
import { getLogIndicatorType, getLogIndicatorTypeForTable } from './utils';
describe('getLogIndicatorType', () => {
it('should return severity type for valid log with severityText', () => {
const log = {
date: '2024-02-29T12:34:46Z',
timestamp: 1646115296,
id: '123456',
traceId: '987654',
spanId: '54321',
traceFlags: 0,
severityText: 'INFO',
severityNumber: 2,
body: 'Sample log Message',
resources_string: {},
attributesString: {},
attributes_string: {},
attributesInt: {},
attributesFloat: {},
severity_text: 'INFO',
};
expect(getLogIndicatorType(log)).toBe('INFO');
});
it('should return log level if severityText is missing', () => {
const log: ILog = {
date: '2024-02-29T12:34:58Z',
timestamp: 1646115296,
id: '123456',
traceId: '987654',
spanId: '54321',
traceFlags: 0,
severityNumber: 2,
body: 'Sample log',
resources_string: {},
attributesString: {},
attributes_string: {},
attributesInt: {},
attributesFloat: {},
severity_text: 'FATAL',
severityText: '',
};
expect(getLogIndicatorType(log)).toBe('FATAL');
});
});
describe('getLogIndicatorTypeForTable', () => {
it('should return severity type for valid log with severityText', () => {
const log = {
date: '2024-02-29T12:34:56Z',
timestamp: 1646115296,
id: '123456',
traceId: '987654',
spanId: '54321',
traceFlags: 0,
severity_number: 2,
body: 'Sample log message',
resources_string: {},
attributesString: {},
attributes_string: {},
attributesInt: {},
attributesFloat: {},
severity_text: 'WARN',
};
expect(getLogIndicatorTypeForTable(log)).toBe('WARN');
});
it('should return log level if severityText is missing', () => {
const log = {
date: '2024-02-29T12:34:56Z',
timestamp: 1646115296,
id: '123456',
traceId: '987654',
spanId: '54321',
traceFlags: 0,
severityNumber: 2,
body: 'Sample log message',
resources_string: {},
attributesString: {},
attributes_string: {},
attributesInt: {},
attributesFloat: {},
log_level: 'INFO',
};
expect(getLogIndicatorTypeForTable(log)).toBe('INFO');
});
});

View File

@ -0,0 +1,57 @@
import { ILog } from 'types/api/logs/log';
import { LogType, SEVERITY_TEXT_TYPE } from './LogStateIndicator';
const getSeverityType = (severityText: string): string => {
switch (severityText) {
case SEVERITY_TEXT_TYPE.TRACE:
case SEVERITY_TEXT_TYPE.TRACE2:
case SEVERITY_TEXT_TYPE.TRACE3:
case SEVERITY_TEXT_TYPE.TRACE4:
return SEVERITY_TEXT_TYPE.TRACE;
case SEVERITY_TEXT_TYPE.DEBUG:
case SEVERITY_TEXT_TYPE.DEBUG2:
case SEVERITY_TEXT_TYPE.DEBUG3:
case SEVERITY_TEXT_TYPE.DEBUG4:
return SEVERITY_TEXT_TYPE.DEBUG;
case SEVERITY_TEXT_TYPE.INFO:
case SEVERITY_TEXT_TYPE.INFO2:
case SEVERITY_TEXT_TYPE.INFO3:
case SEVERITY_TEXT_TYPE.INFO4:
return SEVERITY_TEXT_TYPE.INFO;
case SEVERITY_TEXT_TYPE.WARN:
case SEVERITY_TEXT_TYPE.WARN2:
case SEVERITY_TEXT_TYPE.WARN3:
case SEVERITY_TEXT_TYPE.WARN4:
case SEVERITY_TEXT_TYPE.WARNING:
return SEVERITY_TEXT_TYPE.WARN;
case SEVERITY_TEXT_TYPE.ERROR:
case SEVERITY_TEXT_TYPE.ERROR2:
case SEVERITY_TEXT_TYPE.ERROR3:
case SEVERITY_TEXT_TYPE.ERROR4:
return SEVERITY_TEXT_TYPE.ERROR;
case SEVERITY_TEXT_TYPE.FATAL:
case SEVERITY_TEXT_TYPE.FATAL2:
case SEVERITY_TEXT_TYPE.FATAL3:
case SEVERITY_TEXT_TYPE.FATAL4:
return SEVERITY_TEXT_TYPE.FATAL;
default:
return SEVERITY_TEXT_TYPE.INFO;
}
};
export const getLogIndicatorType = (logData: ILog): string => {
if (logData.severity_text) {
return getSeverityType(logData.severity_text);
}
return logData.attributes_string?.log_level || LogType.INFO;
};
export const getLogIndicatorTypeForTable = (
log: Record<string, unknown>,
): string => {
if (log.severity_text) {
return getSeverityType(log.severity_text as string);
}
return (log.log_level as string) || LogType.INFO;
};

View File

@ -23,9 +23,8 @@ import {
} from 'react'; } from 'react';
import LogLinesActionButtons from '../LogLinesActionButtons/LogLinesActionButtons'; import LogLinesActionButtons from '../LogLinesActionButtons/LogLinesActionButtons';
import LogStateIndicator, { import LogStateIndicator from '../LogStateIndicator/LogStateIndicator';
LogType, import { getLogIndicatorType } from '../LogStateIndicator/utils';
} from '../LogStateIndicator/LogStateIndicator';
// styles // styles
import { RawLogContent, RawLogViewContainer } from './styles'; import { RawLogContent, RawLogViewContainer } from './styles';
import { RawLogViewProps } from './types'; import { RawLogViewProps } from './types';
@ -64,7 +63,7 @@ function RawLogView({
const severityText = data.severity_text ? `${data.severity_text} |` : ''; const severityText = data.severity_text ? `${data.severity_text} |` : '';
const logType = data?.attributes_string?.log_level || LogType.INFO; const logType = getLogIndicatorType(data);
const updatedSelecedFields = useMemo( const updatedSelecedFields = useMemo(
() => selectedFields.filter((e) => e.name !== 'id'), () => selectedFields.filter((e) => e.name !== 'id'),
@ -164,7 +163,11 @@ function RawLogView({
> >
<LogStateIndicator <LogStateIndicator
type={logType} type={logType}
isActive={activeLog?.id === data.id || activeContextLog?.id === data.id} isActive={
activeLog?.id === data.id ||
activeContextLog?.id === data.id ||
isActiveLog
}
/> />
<RawLogContent <RawLogContent

View File

@ -30,6 +30,14 @@ export const RawLogViewContainer = styled(Row)<{
$isActiveLog $isActiveLog
? getActiveLogBackground($isActiveLog, $isDarkMode) ? getActiveLogBackground($isActiveLog, $isDarkMode)
: getDefaultLogBackground($isReadOnly, $isDarkMode)} : getDefaultLogBackground($isReadOnly, $isDarkMode)}
${({ $isHightlightedLog, $isDarkMode }): string =>
$isHightlightedLog
? `background-color: ${
$isDarkMode ? Color.BG_SLATE_500 : Color.BG_VANILLA_300
};
transition: background-color 2s ease-in;`
: ''}
`; `;
export const ExpandIconWrapper = styled(Col)` export const ExpandIconWrapper = styled(Col)`

View File

@ -14,12 +14,12 @@ export function getDefaultCellStyle(isDarkMode?: boolean): CSSProperties {
lineHeight: '18px', lineHeight: '18px',
letterSpacing: '-0.07px', letterSpacing: '-0.07px',
marginBottom: '0px', marginBottom: '0px',
minWidth: '10rem',
}; };
} }
export const defaultTableStyle: CSSProperties = { export const defaultTableStyle: CSSProperties = {
minWidth: '40rem', minWidth: '40rem',
maxWidth: '40rem',
}; };
export const defaultListViewPanelStyle: CSSProperties = { export const defaultListViewPanelStyle: CSSProperties = {

View File

@ -23,6 +23,7 @@ export type UseTableViewProps = {
onOpenLogsContext?: (log: ILog) => void; onOpenLogsContext?: (log: ILog) => void;
onClickExpand?: (log: ILog) => void; onClickExpand?: (log: ILog) => void;
activeLog?: ILog | null; activeLog?: ILog | null;
activeLogIndex?: number;
activeContextLog?: ILog | null; activeContextLog?: ILog | null;
isListViewPanel?: boolean; isListViewPanel?: boolean;
} & LogsTableViewProps; } & LogsTableViewProps;

View File

@ -7,12 +7,10 @@ import dayjs from 'dayjs';
import dompurify from 'dompurify'; import dompurify from 'dompurify';
import { useIsDarkMode } from 'hooks/useDarkMode'; import { useIsDarkMode } from 'hooks/useDarkMode';
import { FlatLogData } from 'lib/logs/flatLogData'; import { FlatLogData } from 'lib/logs/flatLogData';
import { defaultTo } from 'lodash-es';
import { useMemo } from 'react'; import { useMemo } from 'react';
import LogStateIndicator, { import LogStateIndicator from '../LogStateIndicator/LogStateIndicator';
LogType, import { getLogIndicatorTypeForTable } from '../LogStateIndicator/utils';
} from '../LogStateIndicator/LogStateIndicator';
import { import {
defaultListViewPanelStyle, defaultListViewPanelStyle,
defaultTableStyle, defaultTableStyle,
@ -84,7 +82,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
children: ( children: (
<div className="table-timestamp"> <div className="table-timestamp">
<LogStateIndicator <LogStateIndicator
type={defaultTo(item.log_level, LogType.INFO) as string} type={getLogIndicatorTypeForTable(item)}
isActive={ isActive={
activeLog?.id === item.id || activeContextLog?.id === item.id activeLog?.id === item.id || activeContextLog?.id === item.id
} }

View File

@ -27,7 +27,7 @@
line-height: 18px; line-height: 18px;
letter-spacing: 0.08em; letter-spacing: 0.08em;
text-align: left; text-align: left;
color: var(--bg-slate-200, #52575c); color: #52575c;
} }
.menu-items { .menu-items {
@ -65,7 +65,7 @@
padding: 12px; padding: 12px;
.title { .title {
color: var(--bg-slate-200, #52575c); color: #52575c;
font-family: Inter; font-family: Inter;
font-size: 11px; font-size: 11px;
font-style: normal; font-style: normal;
@ -149,7 +149,7 @@
} }
.title { .title {
color: var(--bg-slate-200, #52575c); color: #52575c;
font-family: Inter; font-family: Inter;
font-size: 11px; font-size: 11px;
font-style: normal; font-style: normal;

View File

@ -72,8 +72,6 @@ export default function LogsFormatOptionsMenu({
setAddNewColumn(!addNewColumn); setAddNewColumn(!addNewColumn);
}; };
// console.log('optionsMenuConfig', config);
const handleLinesPerRowChange = (maxLinesPerRow: number | null): void => { const handleLinesPerRowChange = (maxLinesPerRow: number | null): void => {
if ( if (
maxLinesPerRow && maxLinesPerRow &&
@ -122,38 +120,36 @@ export default function LogsFormatOptionsMenu({
{selectedItem && ( {selectedItem && (
<> <>
{selectedItem === 'raw' && ( <>
<> <div className="horizontal-line" />
<div className="horizontal-line" /> <div className="max-lines-per-row">
<div className="max-lines-per-row"> <div className="title"> max lines per row </div>
<div className="title"> max lines per row </div> <div className="raw-format max-lines-per-row-input">
<div className="raw-format max-lines-per-row-input"> <button
<button type="button"
type="button" className="periscope-btn"
className="periscope-btn" onClick={decrementMaxLinesPerRow}
onClick={decrementMaxLinesPerRow} >
> {' '}
{' '} <Minus size={12} />{' '}
<Minus size={12} />{' '} </button>
</button> <InputNumber
<InputNumber min={1}
min={1} max={10}
max={10} value={maxLinesPerRow}
value={maxLinesPerRow} onChange={handleLinesPerRowChange}
onChange={handleLinesPerRowChange} />
/> <button
<button type="button"
type="button" className="periscope-btn"
className="periscope-btn" onClick={incrementMaxLinesPerRow}
onClick={incrementMaxLinesPerRow} >
> {' '}
{' '} <Plus size={12} />{' '}
<Plus size={12} />{' '} </button>
</button>
</div>
</div> </div>
</> </div>
)} </>
<div className="selected-item-content-container active"> <div className="selected-item-content-container active">
{!addNewColumn && <div className="horizontal-line" />} {!addNewColumn && <div className="horizontal-line" />}
@ -221,8 +217,6 @@ export default function LogsFormatOptionsMenu({
className="column-name" className="column-name"
key={value} key={value}
onClick={(eve): void => { onClick={(eve): void => {
console.log('coluimn name', label, value);
eve.stopPropagation(); eve.stopPropagation();
if (addColumn && addColumn?.onSelect) { if (addColumn && addColumn?.onSelect) {

View File

@ -1,10 +1,12 @@
/* eslint-disable no-restricted-syntax */ /* eslint-disable no-restricted-syntax */
/* eslint-disable react/jsx-props-no-spreading */ /* eslint-disable react/jsx-props-no-spreading */
/* eslint-disable @typescript-eslint/explicit-function-return-type */ /* eslint-disable @typescript-eslint/explicit-function-return-type */
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
import { CodeProps } from 'react-markdown/lib/ast-to-react'; import { CodeProps } from 'react-markdown/lib/ast-to-react';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { a11yDark } from 'react-syntax-highlighter/dist/cjs/styles/prism'; import { a11yDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
import rehypeRaw from 'rehype-raw';
import CodeCopyBtn from './CodeCopyBtn/CodeCopyBtn'; import CodeCopyBtn from './CodeCopyBtn/CodeCopyBtn';
@ -74,6 +76,10 @@ const interpolateMarkdown = (
return interpolatedContent; return interpolatedContent;
}; };
function CustomTag({ color }: { color: string }): JSX.Element {
return <h1 style={{ color }}>This is custom element</h1>;
}
function MarkdownRenderer({ function MarkdownRenderer({
markdownContent, markdownContent,
variables, variables,
@ -85,12 +91,14 @@ function MarkdownRenderer({
return ( return (
<ReactMarkdown <ReactMarkdown
rehypePlugins={[rehypeRaw as any]}
components={{ components={{
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
a: Link, a: Link,
pre: Pre, pre: Pre,
code: Code, code: Code,
customtag: CustomTag,
}} }}
> >
{interpolatedMarkdown} {interpolatedMarkdown}

View File

@ -13,3 +13,6 @@ export const SIGNOZ_UPGRADE_PLAN_URL =
'https://upgrade.signoz.io/upgrade-from-app'; 'https://upgrade.signoz.io/upgrade-from-app';
export const DASHBOARD_TIME_IN_DURATION = 'refreshInterval'; export const DASHBOARD_TIME_IN_DURATION = 'refreshInterval';
export const DEFAULT_ENTITY_VERSION = 'v3';
export const ENTITY_VERSION_V4 = 'v4';

View File

@ -16,4 +16,6 @@ export enum LOCALSTORAGE {
CHAT_SUPPORT = 'CHAT_SUPPORT', CHAT_SUPPORT = 'CHAT_SUPPORT',
IS_IDENTIFIED_USER = 'IS_IDENTIFIED_USER', IS_IDENTIFIED_USER = 'IS_IDENTIFIED_USER',
DASHBOARD_VARIABLES = 'DASHBOARD_VARIABLES', DASHBOARD_VARIABLES = 'DASHBOARD_VARIABLES',
SHOW_EXPLORER_TOOLBAR = 'SHOW_EXPLORER_TOOLBAR',
PINNED_ATTRIBUTES = 'PINNED_ATTRIBUTES',
} }

View File

@ -27,5 +27,6 @@ export enum QueryParams {
viewName = 'viewName', viewName = 'viewName',
viewKey = 'viewKey', viewKey = 'viewKey',
expandedWidgetId = 'expandedWidgetId', expandedWidgetId = 'expandedWidgetId',
integration = 'integration',
pagination = 'pagination', pagination = 'pagination',
} }

View File

@ -36,6 +36,11 @@ import { v4 as uuid } from 'uuid';
import { import {
logsAggregateOperatorOptions, logsAggregateOperatorOptions,
metricAggregateOperatorOptions, metricAggregateOperatorOptions,
metricsGaugeAggregateOperatorOptions,
metricsGaugeSpaceAggregateOperatorOptions,
metricsHistogramSpaceAggregateOperatorOptions,
metricsSumAggregateOperatorOptions,
metricsSumSpaceAggregateOperatorOptions,
tracesAggregateOperatorOptions, tracesAggregateOperatorOptions,
} from './queryBuilderOperators'; } from './queryBuilderOperators';
@ -74,6 +79,18 @@ export const mapOfOperators = {
traces: tracesAggregateOperatorOptions, traces: tracesAggregateOperatorOptions,
}; };
export const metricsOperatorsByType = {
Sum: metricsSumAggregateOperatorOptions,
Gauge: metricsGaugeAggregateOperatorOptions,
};
export const metricsSpaceAggregationOperatorsByType = {
Sum: metricsSumSpaceAggregateOperatorOptions,
Gauge: metricsGaugeSpaceAggregateOperatorOptions,
Histogram: metricsHistogramSpaceAggregateOperatorOptions,
ExponentialHistogram: metricsHistogramSpaceAggregateOperatorOptions,
};
export const mapOfQueryFilters: Record<DataSource, QueryAdditionalFilter[]> = { export const mapOfQueryFilters: Record<DataSource, QueryAdditionalFilter[]> = {
metrics: [ metrics: [
// eslint-disable-next-line sonarjs/no-duplicate-string // eslint-disable-next-line sonarjs/no-duplicate-string
@ -148,6 +165,9 @@ export const initialQueryBuilderFormValues: IBuilderQuery = {
queryName: createNewBuilderItemName({ existNames: [], sourceNames: alphabet }), queryName: createNewBuilderItemName({ existNames: [], sourceNames: alphabet }),
aggregateOperator: MetricAggregateOperator.COUNT, aggregateOperator: MetricAggregateOperator.COUNT,
aggregateAttribute: initialAutocompleteData, aggregateAttribute: initialAutocompleteData,
timeAggregation: MetricAggregateOperator.RATE,
spaceAggregation: MetricAggregateOperator.SUM,
functions: [],
filters: { items: [], op: 'AND' }, filters: { items: [], op: 'AND' },
expression: createNewBuilderItemName({ expression: createNewBuilderItemName({
existNames: [], existNames: [],
@ -160,7 +180,7 @@ export const initialQueryBuilderFormValues: IBuilderQuery = {
orderBy: [], orderBy: [],
groupBy: [], groupBy: [],
legend: '', legend: '',
reduceTo: 'sum', reduceTo: 'avg',
}; };
const initialQueryBuilderFormLogsValues: IBuilderQuery = { const initialQueryBuilderFormLogsValues: IBuilderQuery = {
@ -268,6 +288,14 @@ export enum PANEL_TYPES {
EMPTY_WIDGET = 'EMPTY_WIDGET', EMPTY_WIDGET = 'EMPTY_WIDGET',
} }
// eslint-disable-next-line @typescript-eslint/naming-convention
export enum ATTRIBUTE_TYPES {
SUM = 'Sum',
GAUGE = 'Gauge',
HISTOGRAM = 'Histogram',
EXPONENTIAL_HISTOGRAM = 'ExponentialHistogram',
}
export type IQueryBuilderState = 'search'; export type IQueryBuilderState = 'search';
export const QUERY_BUILDER_SEARCH_VALUES = { export const QUERY_BUILDER_SEARCH_VALUES = {

View File

@ -302,3 +302,126 @@ export const logsAggregateOperatorOptions: SelectOption<string, string>[] = [
label: 'Rate_max', label: 'Rate_max',
}, },
]; ];
export const metricsSumAggregateOperatorOptions: SelectOption<
string,
string
>[] = [
{
value: MetricAggregateOperator.RATE,
label: 'Rate',
},
{
value: MetricAggregateOperator.INCREASE,
label: 'Increase',
},
];
export const metricsGaugeAggregateOperatorOptions: SelectOption<
string,
string
>[] = [
{
value: MetricAggregateOperator.LATEST,
label: 'Latest',
},
{
value: MetricAggregateOperator.SUM,
label: 'Sum',
},
{
value: MetricAggregateOperator.AVG,
label: 'Avg',
},
{
value: MetricAggregateOperator.MIN,
label: 'Min',
},
{
value: MetricAggregateOperator.MAX,
label: 'Max',
},
{
value: MetricAggregateOperator.COUNT,
label: 'Count',
},
{
value: MetricAggregateOperator.COUNT_DISTINCT,
label: 'Count Distinct',
},
];
export const metricsSumSpaceAggregateOperatorOptions: SelectOption<
string,
string
>[] = [
{
value: MetricAggregateOperator.SUM,
label: 'Sum',
},
{
value: MetricAggregateOperator.AVG,
label: 'Avg',
},
{
value: MetricAggregateOperator.MIN,
label: 'Min',
},
{
value: MetricAggregateOperator.MAX,
label: 'Max',
},
];
export const metricsGaugeSpaceAggregateOperatorOptions: SelectOption<
string,
string
>[] = [
{
value: MetricAggregateOperator.SUM,
label: 'Sum',
},
{
value: MetricAggregateOperator.AVG,
label: 'Avg',
},
{
value: MetricAggregateOperator.MIN,
label: 'Min',
},
{
value: MetricAggregateOperator.MAX,
label: 'Max',
},
];
export const metricsHistogramSpaceAggregateOperatorOptions: SelectOption<
string,
string
>[] = [
{
value: MetricAggregateOperator.P50,
label: 'P50',
},
{
value: MetricAggregateOperator.P75,
label: 'P75',
},
{
value: MetricAggregateOperator.P90,
label: 'P90',
},
{
value: MetricAggregateOperator.P95,
label: 'P95',
},
{
value: MetricAggregateOperator.P99,
label: 'P99',
},
];
export const metricsEmptyTimeAggregateOperatorOptions: SelectOption<
string,
string
>[] = [];

View File

@ -0,0 +1,137 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { QueryFunctionsTypes } from 'types/common/queryBuilder';
import { SelectOption } from 'types/common/select';
export const queryFunctionOptions: SelectOption<string, string>[] = [
{
value: QueryFunctionsTypes.CUTOFF_MIN,
label: 'Cut Off Min',
},
{
value: QueryFunctionsTypes.CUTOFF_MAX,
label: 'Cut Off Max',
},
{
value: QueryFunctionsTypes.CLAMP_MIN,
label: 'Clamp Min',
},
{
value: QueryFunctionsTypes.CLAMP_MAX,
label: 'Clamp Max',
},
{
value: QueryFunctionsTypes.ABSOLUTE,
label: 'Absolute',
},
{
value: QueryFunctionsTypes.LOG_2,
label: 'Log2',
},
{
value: QueryFunctionsTypes.LOG_10,
label: 'Log10',
},
{
value: QueryFunctionsTypes.CUMULATIVE_SUM,
label: 'Cumulative Sum',
},
{
value: QueryFunctionsTypes.EWMA_3,
label: 'EWMA 3',
},
{
value: QueryFunctionsTypes.EWMA_5,
label: 'EWMA 5',
},
{
value: QueryFunctionsTypes.EWMA_7,
label: 'EWMA 7',
},
{
value: QueryFunctionsTypes.MEDIAN_3,
label: 'Median 3',
},
{
value: QueryFunctionsTypes.MEDIAN_5,
label: 'Median 5',
},
{
value: QueryFunctionsTypes.MEDIAN_7,
label: 'Median 7',
},
{
value: QueryFunctionsTypes.TIME_SHIFT,
label: 'Time Shift',
},
];
interface QueryFunctionConfigType {
[key: string]: {
showInput: boolean;
inputType?: string;
placeholder?: string;
};
}
export const queryFunctionsTypesConfig: QueryFunctionConfigType = {
cutOffMin: {
showInput: true,
inputType: 'text',
placeholder: 'Threshold',
},
cutOffMax: {
showInput: true,
inputType: 'text',
placeholder: 'Threshold',
},
clampMin: {
showInput: true,
inputType: 'text',
placeholder: 'Threshold',
},
clampMax: {
showInput: true,
inputType: 'text',
placeholder: 'Threshold',
},
absolute: {
showInput: false,
},
log2: {
showInput: false,
},
log10: {
showInput: false,
},
cumSum: {
showInput: false,
},
ewma3: {
showInput: true,
inputType: 'text',
placeholder: 'Alpha',
},
ewma5: {
showInput: true,
inputType: 'text',
placeholder: 'Alpha',
},
ewma7: {
showInput: true,
inputType: 'text',
placeholder: 'Alpha',
},
median3: {
showInput: false,
},
median5: {
showInput: false,
},
median7: {
showInput: false,
},
timeShift: {
showInput: true,
inputType: 'text',
},
};

View File

@ -7,6 +7,11 @@ const ROUTES = {
TRACE_DETAIL: '/trace/:id', TRACE_DETAIL: '/trace/:id',
TRACES_EXPLORER: '/traces-explorer', TRACES_EXPLORER: '/traces-explorer',
GET_STARTED: '/get-started', GET_STARTED: '/get-started',
GET_STARTED_APPLICATION_MONITORING: '/get-started/application-monitoring',
GET_STARTED_LOGS_MANAGEMENT: '/get-started/logs-management',
GET_STARTED_INFRASTRUCTURE_MONITORING:
'/get-started/infrastructure-monitoring',
GET_STARTED_AWS_MONITORING: '/get-started/aws-monitoring',
USAGE_EXPLORER: '/usage-explorer', USAGE_EXPLORER: '/usage-explorer',
APPLICATION: '/services', APPLICATION: '/services',
ALL_DASHBOARD: '/dashboard', ALL_DASHBOARD: '/dashboard',
@ -42,10 +47,13 @@ const ROUTES = {
TRACE_EXPLORER: '/trace-explorer', TRACE_EXPLORER: '/trace-explorer',
BILLING: '/billing', BILLING: '/billing',
SUPPORT: '/support', SUPPORT: '/support',
LOGS_SAVE_VIEWS: '/logs-save-views', LOGS_SAVE_VIEWS: '/logs/saved-views',
TRACES_SAVE_VIEWS: '/traces-save-views', TRACES_SAVE_VIEWS: '/traces/saved-views',
WORKSPACE_LOCKED: '/workspace-locked', WORKSPACE_LOCKED: '/workspace-locked',
SHORTCUTS: '/shortcuts', SHORTCUTS: '/shortcuts',
INTEGRATIONS_BASE: '/integrations',
INTEGRATIONS_INSTALLED: '/integrations/installed',
INTEGRATIONS_MARKETPLACE: '/integrations/marketplace',
} as const; } as const;
export default ROUTES; export default ROUTES;

View File

@ -0,0 +1,17 @@
import { getUserOperatingSystem, UserOperatingSystem } from 'utils/getUserOS';
const userOS = getUserOperatingSystem();
export const DashboardShortcuts = {
SaveChanges: 's+meta',
DiscardChanges: 'd+meta',
};
export const DashboardShortcutsName = {
SaveChanges: `${userOS === UserOperatingSystem.MACOS ? 'cmd' : 'ctrl'}+s`,
};
export const DashboardShortcutsDescription = {
SaveChanges: 'Save Changes',
DiscardChanges: 'Discard Changes',
};

View File

@ -0,0 +1,17 @@
import { getUserOperatingSystem, UserOperatingSystem } from 'utils/getUserOS';
const userOS = getUserOperatingSystem();
export const QBShortcuts = {
StageAndRunQuery: 'enter+meta',
};
export const QBShortcutsName = {
StageAndRunQuery: `${
userOS === UserOperatingSystem.MACOS ? 'cmd' : 'ctrl'
}+enter`,
};
export const QBShortcutsDescription = {
StageAndRunQuery: 'Stage and Run the query',
};

View File

@ -231,7 +231,12 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const routeKey = useMemo(() => getRouteKey(pathname), [pathname]); const routeKey = useMemo(() => getRouteKey(pathname), [pathname]);
const pageTitle = t(routeKey); const pageTitle = t(routeKey);
const renderFullScreen = const renderFullScreen =
pathname === ROUTES.GET_STARTED || pathname === ROUTES.WORKSPACE_LOCKED; pathname === ROUTES.GET_STARTED ||
pathname === ROUTES.WORKSPACE_LOCKED ||
pathname === ROUTES.GET_STARTED_APPLICATION_MONITORING ||
pathname === ROUTES.GET_STARTED_INFRASTRUCTURE_MONITORING ||
pathname === ROUTES.GET_STARTED_LOGS_MANAGEMENT ||
pathname === ROUTES.GET_STARTED_AWS_MONITORING;
const [showTrialExpiryBanner, setShowTrialExpiryBanner] = useState(false); const [showTrialExpiryBanner, setShowTrialExpiryBanner] = useState(false);

View File

@ -1,13 +1,29 @@
.billing-container { .billing-container {
padding: 16px 0; padding-top: 36px;
width: 100%; width: 65%;
.billing-summary { .billing-summary {
margin: 24px 8px; margin: 24px 8px;
} }
.billing-details { .billing-details {
margin: 36px 8px; margin: 24px 0px;
.ant-table-title {
color: var(--bg-vanilla-400);
background-color: rgb(27, 28, 32);
}
.ant-table-cell {
background-color: var(--bg-ink-400);
border-color: var(--bg-slate-500);
}
.ant-table-tbody {
td {
border-color: var(--bg-slate-500);
}
}
} }
.upgrade-plan-benefits { .upgrade-plan-benefits {
@ -24,6 +40,15 @@
} }
} }
} }
.empty-graph-card {
.ant-card-body {
height: 40vh;
display: flex;
justify-content: center;
align-items: center;
}
}
} }
.ant-skeleton.ant-skeleton-element.ant-skeleton-active { .ant-skeleton.ant-skeleton-element.ant-skeleton-active {
@ -34,3 +59,20 @@
.ant-skeleton.ant-skeleton-element .ant-skeleton-input { .ant-skeleton.ant-skeleton-element .ant-skeleton-input {
min-width: 100% !important; min-width: 100% !important;
} }
.lightMode {
.billing-container {
.billing-details {
.ant-table-cell {
background: var(--bg-vanilla-100);
border-color: var(--bg-vanilla-200);
}
.ant-table-tbody {
td {
border-color: var(--bg-vanilla-200);
}
}
}
}
}

View File

@ -12,13 +12,36 @@ import BillingContainer from './BillingContainer';
const lisenceUrl = 'http://localhost/api/v2/licenses'; const lisenceUrl = 'http://localhost/api/v2/licenses';
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
window.ResizeObserver =
window.ResizeObserver ||
jest.fn().mockImplementation(() => ({
disconnect: jest.fn(),
observe: jest.fn(),
unobserve: jest.fn(),
}));
describe('BillingContainer', () => { describe('BillingContainer', () => {
test('Component should render', async () => { test('Component should render', async () => {
act(() => { act(() => {
render(<BillingContainer />); render(<BillingContainer />);
}); });
const unit = screen.getAllByText(/unit/i);
expect(unit[1]).toBeInTheDocument();
const dataInjection = screen.getByRole('columnheader', { const dataInjection = screen.getByRole('columnheader', {
name: /data ingested/i, name: /data ingested/i,
}); });
@ -32,24 +55,15 @@ describe('BillingContainer', () => {
}); });
expect(cost).toBeInTheDocument(); expect(cost).toBeInTheDocument();
const total = screen.getByRole('cell', {
name: /total/i,
});
expect(total).toBeInTheDocument();
const manageBilling = screen.getByRole('button', { const manageBilling = screen.getByRole('button', {
name: /manage billing/i, name: 'manage_billing',
}); });
expect(manageBilling).toBeInTheDocument(); expect(manageBilling).toBeInTheDocument();
const dollar = screen.getByRole('cell', { const dollar = screen.getByText(/\$0/i);
name: /\$0/i,
});
expect(dollar).toBeInTheDocument(); expect(dollar).toBeInTheDocument();
const currentBill = screen.getByRole('heading', { const currentBill = screen.getByText('billing');
name: /current bill total/i,
});
expect(currentBill).toBeInTheDocument(); expect(currentBill).toBeInTheDocument();
}); });
@ -61,9 +75,7 @@ describe('BillingContainer', () => {
const freeTrailText = await screen.findByText('Free Trial'); const freeTrailText = await screen.findByText('Free Trial');
expect(freeTrailText).toBeInTheDocument(); expect(freeTrailText).toBeInTheDocument();
const currentBill = await screen.findByRole('heading', { const currentBill = screen.getByText('billing');
name: /current bill total/i,
});
expect(currentBill).toBeInTheDocument(); expect(currentBill).toBeInTheDocument();
const dollar0 = await screen.findByText(/\$0/i); const dollar0 = await screen.findByText(/\$0/i);
@ -73,18 +85,14 @@ describe('BillingContainer', () => {
); );
expect(onTrail).toBeInTheDocument(); expect(onTrail).toBeInTheDocument();
const numberOfDayRemaining = await screen.findByText( const numberOfDayRemaining = await screen.findByText(/1 days_remaining/i);
/1 days remaining in your billing period./i,
);
expect(numberOfDayRemaining).toBeInTheDocument(); expect(numberOfDayRemaining).toBeInTheDocument();
const upgradeButton = await screen.findAllByRole('button', { const upgradeButton = await screen.findAllByRole('button', {
name: /upgrade/i, name: /upgrade_plan/i,
}); });
expect(upgradeButton[1]).toBeInTheDocument(); expect(upgradeButton[1]).toBeInTheDocument();
expect(upgradeButton.length).toBe(2); expect(upgradeButton.length).toBe(2);
const checkPaidPlan = await screen.findByText( const checkPaidPlan = await screen.findByText(/checkout_plans/i);
/Check out features in paid plans/i,
);
expect(checkPaidPlan).toBeInTheDocument(); expect(checkPaidPlan).toBeInTheDocument();
const link = screen.getByRole('link', { name: /here/i }); const link = screen.getByRole('link', { name: /here/i });
@ -102,9 +110,7 @@ describe('BillingContainer', () => {
render(<BillingContainer />); render(<BillingContainer />);
}); });
const currentBill = await screen.findByRole('heading', { const currentBill = screen.getByText('billing');
name: /current bill total/i,
});
expect(currentBill).toBeInTheDocument(); expect(currentBill).toBeInTheDocument();
const dollar0 = await screen.findByText(/\$0/i); const dollar0 = await screen.findByText(/\$0/i);
@ -116,17 +122,17 @@ describe('BillingContainer', () => {
expect(onTrail).toBeInTheDocument(); expect(onTrail).toBeInTheDocument();
const receivedCardDetails = await screen.findByText( const receivedCardDetails = await screen.findByText(
/We have received your card details, your billing will only start after the end of your free trial period./i, /card_details_recieved_and_billing_info/i,
); );
expect(receivedCardDetails).toBeInTheDocument(); expect(receivedCardDetails).toBeInTheDocument();
const manageBillingButton = await screen.findByRole('button', { const manageBillingButton = await screen.findByRole('button', {
name: /manage billing/i, name: /manage_billing/i,
}); });
expect(manageBillingButton).toBeInTheDocument(); expect(manageBillingButton).toBeInTheDocument();
const dayRemainingInBillingPeriod = await screen.findByText( const dayRemainingInBillingPeriod = await screen.findByText(
/1 days remaining in your billing period./i, /1 days_remaining/i,
); );
expect(dayRemainingInBillingPeriod).toBeInTheDocument(); expect(dayRemainingInBillingPeriod).toBeInTheDocument();
}); });
@ -137,45 +143,30 @@ describe('BillingContainer', () => {
res(ctx.status(200), ctx.json(notOfTrailResponse)), res(ctx.status(200), ctx.json(notOfTrailResponse)),
), ),
); );
render(<BillingContainer />); const { findByText } = render(<BillingContainer />);
const billingPeriodText = `Your current billing period is from ${getFormattedDate( const billingPeriodText = `Your current billing period is from ${getFormattedDate(
billingSuccessResponse.data.billingPeriodStart, billingSuccessResponse.data.billingPeriodStart,
)} to ${getFormattedDate(billingSuccessResponse.data.billingPeriodEnd)}`; )} to ${getFormattedDate(billingSuccessResponse.data.billingPeriodEnd)}`;
const billingPeriod = await screen.findByRole('heading', { const billingPeriod = await findByText(billingPeriodText);
name: new RegExp(billingPeriodText, 'i'),
});
expect(billingPeriod).toBeInTheDocument(); expect(billingPeriod).toBeInTheDocument();
const currentBill = await screen.findByRole('heading', { const currentBill = screen.getByText('billing');
name: /current bill total/i,
});
expect(currentBill).toBeInTheDocument(); expect(currentBill).toBeInTheDocument();
const dollar0 = await screen.findAllByText(/\$1278.3/i); const dollar0 = await screen.findByText(/\$1,278.3/i);
expect(dollar0[0]).toBeInTheDocument(); expect(dollar0).toBeInTheDocument();
expect(dollar0.length).toBe(2);
const metricsRow = await screen.findByRole('row', { const metricsRow = await screen.findByRole('row', {
name: /metrics Million 4012 0.1 \$ 401.2/i, name: /metrics 4012 Million 0.1 \$ 401.2/i,
}); });
expect(metricsRow).toBeInTheDocument(); expect(metricsRow).toBeInTheDocument();
const logRow = await screen.findByRole('row', { const logRow = await screen.findByRole('row', {
name: /Logs GB 497 0.4 \$ 198.8/i, name: /Logs 497 GB 0.4 \$ 198.8/i,
}); });
expect(logRow).toBeInTheDocument(); expect(logRow).toBeInTheDocument();
const totalBill = await screen.findByRole('cell', {
name: /\$1278/i,
});
expect(totalBill).toBeInTheDocument();
const totalBillRow = await screen.findByRole('row', {
name: /total \$1278/i,
});
expect(totalBillRow).toBeInTheDocument();
}); });
test('Should render corrent day remaining in billing period', async () => { test('Should render corrent day remaining in billing period', async () => {
@ -186,7 +177,7 @@ describe('BillingContainer', () => {
); );
render(<BillingContainer />); render(<BillingContainer />);
const dayRemainingInBillingPeriod = await screen.findByText( const dayRemainingInBillingPeriod = await screen.findByText(
/11 days remaining in your billing period./i, /11 days_remaining/i,
); );
expect(dayRemainingInBillingPeriod).toBeInTheDocument(); expect(dayRemainingInBillingPeriod).toBeInTheDocument();
}); });

View File

@ -2,19 +2,33 @@
import './BillingContainer.styles.scss'; import './BillingContainer.styles.scss';
import { CheckCircleOutlined } from '@ant-design/icons'; import { CheckCircleOutlined } from '@ant-design/icons';
import { Button, Col, Row, Skeleton, Table, Tag, Typography } from 'antd'; import { Color } from '@signozhq/design-tokens';
import {
Alert,
Button,
Card,
Col,
Flex,
Row,
Skeleton,
Table,
Tag,
Typography,
} from 'antd';
import { ColumnsType } from 'antd/es/table'; import { ColumnsType } from 'antd/es/table';
import updateCreditCardApi from 'api/billing/checkout'; import updateCreditCardApi from 'api/billing/checkout';
import getUsage from 'api/billing/getUsage'; import getUsage, { UsageResponsePayloadProps } from 'api/billing/getUsage';
import manageCreditCardApi from 'api/billing/manage'; import manageCreditCardApi from 'api/billing/manage';
import Spinner from 'components/Spinner';
import { SOMETHING_WENT_WRONG } from 'constants/api'; import { SOMETHING_WENT_WRONG } from 'constants/api';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import useAnalytics from 'hooks/analytics/useAnalytics'; import useAnalytics from 'hooks/analytics/useAnalytics';
import useAxiosError from 'hooks/useAxiosError'; import useAxiosError from 'hooks/useAxiosError';
import useLicense from 'hooks/useLicense'; import useLicense from 'hooks/useLicense';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import { pick } from 'lodash-es'; import { isEmpty, pick } from 'lodash-es';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useMutation, useQuery } from 'react-query'; import { useMutation, useQuery } from 'react-query';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
@ -22,8 +36,11 @@ import { ErrorResponse, SuccessResponse } from 'types/api';
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout'; import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
import { License } from 'types/api/licenses/def'; import { License } from 'types/api/licenses/def';
import AppReducer from 'types/reducer/app'; import AppReducer from 'types/reducer/app';
import { isCloudUser } from 'utils/app';
import { getFormattedDate, getRemainingDays } from 'utils/timeUtils'; import { getFormattedDate, getRemainingDays } from 'utils/timeUtils';
import { BillingUsageGraph } from './BillingUsageGraph/BillingUsageGraph';
interface DataType { interface DataType {
key: string; key: string;
name: string; name: string;
@ -33,6 +50,11 @@ interface DataType {
cost: string; cost: string;
} }
enum SubscriptionStatus {
PastDue = 'past_due',
Active = 'active',
}
const renderSkeletonInput = (): JSX.Element => ( const renderSkeletonInput = (): JSX.Element => (
<Skeleton.Input <Skeleton.Input
style={{ marginTop: '10px', height: '40px', width: '100%' }} style={{ marginTop: '10px', height: '40px', width: '100%' }}
@ -100,16 +122,19 @@ const dummyColumns: ColumnsType<DataType> = [
}, },
]; ];
// eslint-disable-next-line sonarjs/cognitive-complexity
export default function BillingContainer(): JSX.Element { export default function BillingContainer(): JSX.Element {
const daysRemainingStr = 'days remaining in your billing period.'; const { t } = useTranslation(['billings']);
const daysRemainingStr = t('days_remaining');
const [headerText, setHeaderText] = useState(''); const [headerText, setHeaderText] = useState('');
const [billAmount, setBillAmount] = useState(0); const [billAmount, setBillAmount] = useState(0);
const [totalBillAmount, setTotalBillAmount] = useState(0);
const [activeLicense, setActiveLicense] = useState<License | null>(null); const [activeLicense, setActiveLicense] = useState<License | null>(null);
const [daysRemaining, setDaysRemaining] = useState(0); const [daysRemaining, setDaysRemaining] = useState(0);
const [isFreeTrial, setIsFreeTrial] = useState(false); const [isFreeTrial, setIsFreeTrial] = useState(false);
const [data, setData] = useState<any[]>([]); const [data, setData] = useState<any[]>([]);
const billCurrency = '$'; const [apiResponse, setApiResponse] = useState<
Partial<UsageResponsePayloadProps>
>({});
const { trackEvent } = useAnalytics(); const { trackEvent } = useAnalytics();
@ -120,10 +145,15 @@ export default function BillingContainer(): JSX.Element {
const handleError = useAxiosError(); const handleError = useAxiosError();
const isCloudUserVal = isCloudUser();
const processUsageData = useCallback( const processUsageData = useCallback(
(data: any): void => { (data: any): void => {
if (isEmpty(data?.payload)) {
return;
}
const { const {
details: { breakdown = [], total, billTotal }, details: { breakdown = [], billTotal },
billingPeriodStart, billingPeriodStart,
billingPeriodEnd, billingPeriodEnd,
} = data?.payload || {}; } = data?.payload || {};
@ -141,8 +171,7 @@ export default function BillingContainer(): JSX.Element {
formattedUsageData.push({ formattedUsageData.push({
key: `${index}${i}`, key: `${index}${i}`,
name: i === 0 ? element?.type : '', name: i === 0 ? element?.type : '',
unit: element?.unit, dataIngested: `${tier.quantity} ${element?.unit}`,
dataIngested: tier.quantity,
pricePerUnit: tier.unitPrice, pricePerUnit: tier.unitPrice,
cost: `$ ${tier.tierCost}`, cost: `$ ${tier.tierCost}`,
}); });
@ -152,7 +181,6 @@ export default function BillingContainer(): JSX.Element {
} }
setData(formattedUsageData); setData(formattedUsageData);
setTotalBillAmount(total);
if (!licensesData?.payload?.onTrial) { if (!licensesData?.payload?.onTrial) {
const remainingDays = getRemainingDays(billingPeriodEnd) - 1; const remainingDays = getRemainingDays(billingPeriodEnd) - 1;
@ -165,11 +193,16 @@ export default function BillingContainer(): JSX.Element {
setDaysRemaining(remainingDays > 0 ? remainingDays : 0); setDaysRemaining(remainingDays > 0 ? remainingDays : 0);
setBillAmount(billTotal); setBillAmount(billTotal);
} }
setApiResponse(data?.payload || {});
}, },
[licensesData?.payload?.onTrial], [licensesData?.payload?.onTrial],
); );
const { isLoading } = useQuery( const isSubscriptionPastDue =
apiResponse.subscriptionStatus === SubscriptionStatus.PastDue;
const { isLoading, isFetching: isFetchingBillingData } = useQuery(
[REACT_QUERY_KEY.GET_BILLING_USAGE, user?.userId], [REACT_QUERY_KEY.GET_BILLING_USAGE, user?.userId],
{ {
queryFn: () => getUsage(activeLicense?.key || ''), queryFn: () => getUsage(activeLicense?.key || ''),
@ -208,11 +241,6 @@ export default function BillingContainer(): JSX.Element {
key: 'name', key: 'name',
render: (text): JSX.Element => <div>{text}</div>, render: (text): JSX.Element => <div>{text}</div>,
}, },
{
title: 'Unit',
dataIndex: 'unit',
key: 'unit',
},
{ {
title: 'Data Ingested', title: 'Data Ingested',
dataIndex: 'dataIngested', dataIndex: 'dataIngested',
@ -230,24 +258,6 @@ export default function BillingContainer(): JSX.Element {
}, },
]; ];
const renderSummary = (): JSX.Element => (
<Table.Summary.Row>
<Table.Summary.Cell index={0}>
<Typography.Title level={3} style={{ fontWeight: 500, margin: ' 0px' }}>
Total
</Typography.Title>
</Table.Summary.Cell>
<Table.Summary.Cell index={1}> &nbsp; </Table.Summary.Cell>
<Table.Summary.Cell index={2}> &nbsp;</Table.Summary.Cell>
<Table.Summary.Cell index={3}> &nbsp; </Table.Summary.Cell>
<Table.Summary.Cell index={4}>
<Typography.Title level={3} style={{ fontWeight: 500, margin: ' 0px' }}>
${totalBillAmount}
</Typography.Title>
</Table.Summary.Cell>
</Table.Summary.Row>
);
const renderTableSkeleton = (): JSX.Element => ( const renderTableSkeleton = (): JSX.Element => (
<Table <Table
dataSource={dummyData} dataSource={dummyData}
@ -336,78 +346,121 @@ export default function BillingContainer(): JSX.Element {
updateCreditCard, updateCreditCard,
]); ]);
const BillingUsageGraphCallback = useCallback(
() =>
!isLoading && !isFetchingBillingData ? (
<BillingUsageGraph data={apiResponse} billAmount={billAmount} />
) : (
<Card className="empty-graph-card" bordered={false}>
<Spinner size="large" tip="Loading..." height="35vh" />
</Card>
),
[apiResponse, billAmount, isLoading, isFetchingBillingData],
);
const { Text } = Typography;
const subscriptionPastDueMessage = (): JSX.Element => (
<Typography>
{`We were not able to process payments for your account. Please update your card details `}
<Text type="danger" onClick={handleBilling} style={{ cursor: 'pointer' }}>
{t('here')}
</Text>
{` if your payment information has changed. Email us at `}
<Text type="secondary">cloud-support@signoz.io</Text>
{` otherwise. Be sure to provide this information immediately to avoid interruption to your service.`}
</Typography>
);
return ( return (
<div className="billing-container"> <div className="billing-container">
<Row <Flex vertical style={{ marginBottom: 16 }}>
justify="space-between" <Typography.Text style={{ fontWeight: 500, fontSize: 18 }}>
align="middle" {t('billing')}
gutter={[16, 16]} </Typography.Text>
style={{ <Typography.Text color={Color.BG_VANILLA_400}>
margin: 0, {t('manage_billing_and_costs')}
}} </Typography.Text>
</Flex>
<Card
bordered={false}
style={{ minHeight: 150, marginBottom: 16 }}
className="page-info"
> >
<Col span={20}> <Flex justify="space-between" align="center">
<Typography.Title level={4} ellipsis style={{ fontWeight: '300' }}> <Flex vertical>
{headerText} <Typography.Title level={5} style={{ marginTop: 2, fontWeight: 500 }}>
</Typography.Title> {isCloudUserVal ? t('enterprise_cloud') : t('enterprise')}{' '}
{isFreeTrial ? <Tag color="success"> Free Trial </Tag> : ''}
{licensesData?.payload?.onTrial && </Typography.Title>
licensesData?.payload?.trialConvertedToSubscription && ( {!isLoading && !isFetchingBillingData ? (
<Typography.Title <Typography.Text style={{ fontSize: 12, color: Color.BG_VANILLA_400 }}>
level={5} {daysRemaining} {daysRemainingStr}
ellipsis </Typography.Text>
style={{ fontWeight: '300', color: '#49aa19' }} ) : null}
> </Flex>
We have received your card details, your billing will only start after
the end of your free trial period.
</Typography.Title>
)}
</Col>
<Col span={4} style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Button <Button
type="primary" type="primary"
size="middle" size="middle"
loading={isLoadingBilling || isLoadingManageBilling} loading={isLoadingBilling || isLoadingManageBilling}
disabled={isLoading}
onClick={handleBilling} onClick={handleBilling}
> >
{isFreeTrial && !licensesData?.payload?.trialConvertedToSubscription {isFreeTrial && !licensesData?.payload?.trialConvertedToSubscription
? 'Upgrade Plan' ? t('upgrade_plan')
: 'Manage Billing'} : t('manage_billing')}
</Button> </Button>
</Col> </Flex>
</Row>
<div className="billing-summary"> {licensesData?.payload?.onTrial &&
<Typography.Title level={4} style={{ margin: '16px 0' }}> licensesData?.payload?.trialConvertedToSubscription && (
Current bill total <Typography.Text
</Typography.Title> ellipsis
style={{ fontWeight: '300', color: '#49aa19', fontSize: 12 }}
>
{t('card_details_recieved_and_billing_info')}
</Typography.Text>
)}
<Typography.Title {!isLoading && !isFetchingBillingData ? (
level={3} headerText && (
style={{ margin: '16px 0', display: 'flex', alignItems: 'center' }} <Alert
> message={headerText}
{billCurrency} type="info"
{billAmount} &nbsp; showIcon
{isFreeTrial ? <Tag color="success"> Free Trial </Tag> : ''} style={{ marginTop: 12 }}
</Typography.Title> />
)
) : (
<Skeleton.Input active style={{ height: 20, marginTop: 20 }} />
)}
<Typography.Paragraph style={{ margin: '16px 0' }}> {isSubscriptionPastDue &&
{daysRemaining} {daysRemainingStr} (!isLoading && !isFetchingBillingData ? (
</Typography.Paragraph> <Alert
</div> message={subscriptionPastDueMessage()}
type="error"
showIcon
style={{ marginTop: 12 }}
/>
) : (
<Skeleton.Input active style={{ height: 20, marginTop: 20 }} />
))}
</Card>
<BillingUsageGraphCallback />
<div className="billing-details"> <div className="billing-details">
{!isLoading && ( {!isLoading && !isFetchingBillingData && (
<Table <Table
columns={columns} columns={columns}
dataSource={data} dataSource={data}
pagination={false} pagination={false}
summary={renderSummary} bordered={false}
/> />
)} )}
{isLoading && renderTableSkeleton()} {(isLoading || isFetchingBillingData) && renderTableSkeleton()}
</div> </div>
{isFreeTrial && !licensesData?.payload?.trialConvertedToSubscription && ( {isFreeTrial && !licensesData?.payload?.trialConvertedToSubscription && (
@ -423,16 +476,16 @@ export default function BillingContainer(): JSX.Element {
<Col span={20} className="plan-benefits"> <Col span={20} className="plan-benefits">
<Typography.Text className="plan-benefit"> <Typography.Text className="plan-benefit">
<CheckCircleOutlined /> <CheckCircleOutlined />
Upgrade now to have uninterrupted access {t('upgrade_now_text')}
</Typography.Text> </Typography.Text>
<Typography.Text className="plan-benefit"> <Typography.Text className="plan-benefit">
<CheckCircleOutlined /> <CheckCircleOutlined />
Your billing will start only after the trial period {t('Your billing will start only after the trial period')}
</Typography.Text> </Typography.Text>
<Typography.Text className="plan-benefit"> <Typography.Text className="plan-benefit">
<CheckCircleOutlined /> <CheckCircleOutlined />
<span> <span>
Check out features in paid plans &nbsp; {t('checkout_plans')} &nbsp;
<a <a
href="https://signoz.io/pricing/" href="https://signoz.io/pricing/"
style={{ style={{
@ -441,7 +494,7 @@ export default function BillingContainer(): JSX.Element {
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >
here {t('here')}
</a> </a>
</span> </span>
</Typography.Text> </Typography.Text>
@ -453,7 +506,7 @@ export default function BillingContainer(): JSX.Element {
loading={isLoadingBilling || isLoadingManageBilling} loading={isLoadingBilling || isLoadingManageBilling}
onClick={handleBilling} onClick={handleBilling}
> >
Upgrade Plan {t('upgrade_plan')}
</Button> </Button>
</Col> </Col>
</Row> </Row>

View File

@ -0,0 +1,29 @@
.billing-graph-card {
.ant-card-body {
height: 40vh;
.uplot-graph-container {
padding: 8px;
}
}
.total-spent {
font-family: 'SF Mono' monospace;
font-size: 16px;
font-style: normal;
font-weight: 600;
line-height: 24px;
}
.total-spent-title {
font-size: 12px;
font-weight: 500;
line-height: 22px;
letter-spacing: 0.48px;
color: rgba(255, 255, 255, 0.5);
}
}
.lightMode {
.total-spent-title {
color: var(--bg-ink-100);
}
}

View File

@ -0,0 +1,201 @@
import './BillingUsageGraph.styles.scss';
import '../../../lib/uPlotLib/uPlotLib.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Card, Flex, Typography } from 'antd';
import Uplot from 'components/Uplot';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
import tooltipPlugin from 'lib/uPlotLib/plugins/tooltipPlugin';
import getAxes from 'lib/uPlotLib/utils/getAxes';
import getRenderer from 'lib/uPlotLib/utils/getRenderer';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { getXAxisScale } from 'lib/uPlotLib/utils/getXAxisScale';
import { getYAxisScale } from 'lib/uPlotLib/utils/getYAxisScale';
import { useMemo, useRef } from 'react';
import uPlot from 'uplot';
import {
convertDataToMetricRangePayload,
fillMissingValuesForQuantities,
} from './utils';
interface BillingUsageGraphProps {
data: any;
billAmount: number;
}
const paths = (
u: any,
seriesIdx: number,
idx0: number,
idx1: number,
extendGap: boolean,
buildClip: boolean,
): uPlot.Series.PathBuilder => {
const s = u.series[seriesIdx];
const style = s.drawStyle;
const interp = s.lineInterpolation;
const renderer = getRenderer(style, interp);
return renderer(u, seriesIdx, idx0, idx1, extendGap, buildClip);
};
const calculateStartEndTime = (
data: any,
): { startTime: number; endTime: number } => {
const timestamps: number[] = [];
data?.details?.breakdown?.forEach((breakdown: any) => {
breakdown?.dayWiseBreakdown?.breakdown.forEach((entry: any) => {
timestamps.push(entry?.timestamp);
});
});
const billingTime = [data?.billingPeriodStart, data?.billingPeriodEnd];
const startTime: number = Math.min(...timestamps, ...billingTime);
const endTime: number = Math.max(...timestamps, ...billingTime);
return { startTime, endTime };
};
export function BillingUsageGraph(props: BillingUsageGraphProps): JSX.Element {
const { data, billAmount } = props;
const graphCompatibleData = useMemo(
() => convertDataToMetricRangePayload(data),
[data],
);
const chartData = getUPlotChartData(graphCompatibleData);
const graphRef = useRef<HTMLDivElement>(null);
const isDarkMode = useIsDarkMode();
const containerDimensions = useResizeObserver(graphRef);
const { startTime, endTime } = useMemo(() => calculateStartEndTime(data), [
data,
]);
const getGraphSeries = (color: string, label: string): any => ({
drawStyle: 'bars',
paths,
lineInterpolation: 'spline',
show: true,
label,
fill: color,
stroke: color,
width: 2,
spanGaps: true,
points: {
size: 5,
show: false,
stroke: color,
},
});
const uPlotSeries: any = useMemo(
() => [
{ label: 'Timestamp', stroke: 'purple' },
getGraphSeries(
'#7CEDBE',
graphCompatibleData.data.result[0]?.legend as string,
),
getGraphSeries(
'#4E74F8',
graphCompatibleData.data.result[1]?.legend as string,
),
getGraphSeries(
'#F24769',
graphCompatibleData.data.result[2]?.legend as string,
),
],
[graphCompatibleData.data.result],
);
const axesOptions = getAxes(isDarkMode, '');
const optionsForChart: uPlot.Options = useMemo(
() => ({
id: 'billing-usage-breakdown',
series: uPlotSeries,
width: containerDimensions.width,
height: containerDimensions.height - 30,
axes: [
{
...axesOptions[0],
grid: {
...axesOptions.grid,
show: false,
stroke: isDarkMode ? Color.BG_VANILLA_400 : Color.BG_INK_400,
},
},
{
...axesOptions[1],
stroke: isDarkMode ? Color.BG_SLATE_200 : Color.BG_INK_400,
},
],
scales: {
x: {
...getXAxisScale(startTime - 86400, endTime), // Minus 86400 from startTime to decrease a day to have a buffer start
},
y: {
...getYAxisScale({
series: graphCompatibleData?.data.newResult.data.result,
yAxisUnit: '',
softMax: null,
softMin: null,
}),
},
},
legend: {
show: true,
live: false,
isolate: true,
},
cursor: {
lock: false,
focus: {
prox: 1e6,
bias: 1,
},
},
focus: {
alpha: 0.3,
},
padding: [32, 32, 16, 16],
plugins: [
tooltipPlugin(
fillMissingValuesForQuantities(graphCompatibleData, chartData[0]),
'',
true,
),
],
}),
[
axesOptions,
chartData,
containerDimensions.height,
containerDimensions.width,
endTime,
graphCompatibleData,
isDarkMode,
startTime,
uPlotSeries,
],
);
const numberFormatter = new Intl.NumberFormat('en-US');
return (
<Card bordered={false} className="billing-graph-card">
<Flex justify="space-between">
<Flex vertical gap={6}>
<Typography.Text className="total-spent-title">
TOTAL SPENT
</Typography.Text>
<Typography.Text color={Color.BG_VANILLA_100} className="total-spent">
${numberFormatter.format(billAmount)}
</Typography.Text>
</Flex>
</Flex>
<div ref={graphRef} style={{ height: '100%', paddingBottom: 48 }}>
<Uplot data={chartData} options={optionsForChart} />
</div>
</Card>
);
}

View File

@ -0,0 +1,87 @@
import { isEmpty, isNull } from 'lodash-es';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
export const convertDataToMetricRangePayload = (
data: any,
): MetricRangePayloadProps => {
const emptyStateData = {
data: {
newResult: { data: { result: [], resultType: '' } },
result: [],
resultType: '',
},
};
if (isEmpty(data)) {
return emptyStateData;
}
const {
details: { breakdown = [] },
} = data || {};
if (isNull(breakdown) || breakdown.length === 0) {
return emptyStateData;
}
const payload = breakdown.map((info: any) => {
const metric = info.type;
const sortedBreakdownData = (info?.dayWiseBreakdown?.breakdown || []).sort(
(a: any, b: any) => a.timestamp - b.timestamp,
);
const values = (sortedBreakdownData || []).map((categoryInfo: any) => [
categoryInfo.timestamp,
categoryInfo.total,
]);
const queryName = info.type;
const legend = info.type;
const { unit } = info;
const quantity = sortedBreakdownData.map(
(categoryInfo: any) => categoryInfo.quantity,
);
return { metric, values, queryName, legend, quantity, unit };
});
const sortedData = payload.sort((a: any, b: any) => {
const sumA = a.values.reduce((acc: any, val: any) => acc + val[1], 0);
const avgA = a.values.length ? sumA / a.values.length : 0;
const sumB = b.values.reduce((acc: any, val: any) => acc + val[1], 0);
const avgB = b.values.length ? sumB / b.values.length : 0;
return sumA === sumB ? avgB - avgA : sumB - sumA;
});
return {
data: {
newResult: { data: { result: sortedData, resultType: '' } },
result: sortedData,
resultType: '',
},
};
};
export function fillMissingValuesForQuantities(
data: any,
timestampArray: number[],
): MetricRangePayloadProps {
const { result } = data.data;
const transformedResultArr: any[] = [];
result.forEach((item: any) => {
const timestampToQuantityMap: { [timestamp: number]: number } = {};
item.values.forEach((val: number[], index: number) => {
timestampToQuantityMap[val[0]] = item.quantity[index];
});
const quantityArray = timestampArray.map(
(timestamp: number) => timestampToQuantityMap[timestamp] ?? null,
);
transformedResultArr.push({ ...item, quantity: quantityArray });
});
return {
data: {
newResult: { data: { result: transformedResultArr, resultType: '' } },
result: transformedResultArr,
resultType: '',
},
};
}

View File

@ -64,6 +64,16 @@ export interface OpsgenieChannel extends Channel {
priority?: string; priority?: string;
} }
export interface EmailChannel extends Channel {
// comma separated list of email addresses to send alerts to
to: string;
// HTML body of the email notification.
html: string;
// Further headers email header key/value pairs.
// [ headers: { <string>: <tmpl_string>, ... } ]
headers: Record<string, string>;
}
export const ValidatePagerChannel = (p: PagerChannel): string => { export const ValidatePagerChannel = (p: PagerChannel): string => {
if (!p) { if (!p) {
return 'Received unexpected input for this channel, please contact your administrator '; return 'Received unexpected input for this channel, please contact your administrator ';

View File

@ -1,4 +1,4 @@
import { OpsgenieChannel, PagerChannel } from './config'; import { EmailChannel, OpsgenieChannel, PagerChannel } from './config';
export const PagerInitialConfig: Partial<PagerChannel> = { export const PagerInitialConfig: Partial<PagerChannel> = {
description: `[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .CommonLabels.alertname }} for {{ .CommonLabels.job }} description: `[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .CommonLabels.alertname }} for {{ .CommonLabels.job }}
@ -50,3 +50,399 @@ export const OpsgenieInitialConfig: Partial<OpsgenieChannel> = {
priority: priority:
'{{ if eq (index .Alerts 0).Labels.severity "critical" }}P1{{ else if eq (index .Alerts 0).Labels.severity "warning" }}P2{{ else if eq (index .Alerts 0).Labels.severity "info" }}P3{{ else }}P4{{ end }}', '{{ if eq (index .Alerts 0).Labels.severity "critical" }}P1{{ else if eq (index .Alerts 0).Labels.severity "warning" }}P2{{ else if eq (index .Alerts 0).Labels.severity "info" }}P3{{ else }}P4{{ end }}',
}; };
export const EmailInitialConfig: Partial<EmailChannel> = {
send_resolved: true,
html: `<!--
Credits: https://github.com/mailgun/transactional-email-templates
-->
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>{{ template "__subject" . }}</title>
<style>
/* -------------------------------------
GLOBAL
A very basic CSS reset
------------------------------------- */
* {
margin: 0;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
}
img {
max-width: 100%;
}
body {
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: none;
width: 100% !important;
height: 100%;
line-height: 1.6em;
/* 1.6em * 14px = 22.4px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */
/*line-height: 22px;*/
}
/* Let's make sure all tables have defaults */
table td {
vertical-align: top;
}
/* -------------------------------------
BODY & CONTAINER
------------------------------------- */
body {
background-color: #f6f6f6;
}
.body-wrap {
background-color: #f6f6f6;
width: 100%;
}
.container {
display: block !important;
max-width: 600px !important;
margin: 0 auto !important;
/* makes it centered */
clear: both !important;
}
.content {
max-width: 600px;
margin: 0 auto;
display: block;
padding: 20px;
}
/* -------------------------------------
HEADER, FOOTER, MAIN
------------------------------------- */
.main {
background-color: #fff;
border: 1px solid #e9e9e9;
border-radius: 3px;
}
.content-wrap {
padding: 30px;
}
.content-block {
padding: 0 0 20px;
}
.header {
width: 100%;
margin-bottom: 20px;
}
.footer {
width: 100%;
clear: both;
color: #999;
padding: 20px;
}
.footer p,
.footer a,
.footer td {
color: #999;
font-size: 12px;
}
/* -------------------------------------
TYPOGRAPHY
------------------------------------- */
h1,
h2,
h3 {
font-family: "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
color: #000;
margin: 40px 0 0;
line-height: 1.2em;
font-weight: 400;
}
h1 {
font-size: 32px;
font-weight: 500;
/* 1.2em * 32px = 38.4px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */
/*line-height: 38px;*/
}
h2 {
font-size: 24px;
/* 1.2em * 24px = 28.8px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */
/*line-height: 29px;*/
}
h3 {
font-size: 18px;
/* 1.2em * 18px = 21.6px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */
/*line-height: 22px;*/
}
h4 {
font-size: 14px;
font-weight: 600;
}
p,
ul,
ol {
margin-bottom: 10px;
font-weight: normal;
}
p li,
ul li,
ol li {
margin-left: 5px;
list-style-position: inside;
}
/* -------------------------------------
LINKS & BUTTONS
------------------------------------- */
a {
color: #348eda;
text-decoration: underline;
}
.btn-primary {
text-decoration: none;
color: #FFF;
background-color: #348eda;
border: solid #348eda;
border-width: 10px 20px;
line-height: 2em;
/* 2em * 14px = 28px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */
/*line-height: 28px;*/
font-weight: bold;
text-align: center;
cursor: pointer;
display: inline-block;
border-radius: 5px;
text-transform: capitalize;
}
/* -------------------------------------
OTHER STYLES THAT MIGHT BE USEFUL
------------------------------------- */
.last {
margin-bottom: 0;
}
.first {
margin-top: 0;
}
.aligncenter {
text-align: center;
}
.alignright {
text-align: right;
}
.alignleft {
text-align: left;
}
.clear {
clear: both;
}
/* -------------------------------------
ALERTS
Change the class depending on warning email, good email or bad email
------------------------------------- */
.alert {
font-size: 16px;
color: #fff;
font-weight: 500;
padding: 20px;
text-align: center;
border-radius: 3px 3px 0 0;
}
.alert a {
color: #fff;
text-decoration: none;
font-weight: 500;
font-size: 16px;
}
.alert.alert-warning {
background-color: #E6522C;
}
.alert.alert-bad {
background-color: #D0021B;
}
.alert.alert-good {
background-color: #68B90F;
}
/* -------------------------------------
INVOICE
Styles for the billing table
------------------------------------- */
.invoice {
margin: 40px auto;
text-align: left;
width: 80%;
}
.invoice td {
padding: 5px 0;
}
.invoice .invoice-items {
width: 100%;
}
.invoice .invoice-items td {
border-top: #eee 1px solid;
}
.invoice .invoice-items .total td {
border-top: 2px solid #333;
border-bottom: 2px solid #333;
font-weight: 700;
}
/* -------------------------------------
RESPONSIVE AND MOBILE FRIENDLY STYLES
------------------------------------- */
@media only screen and (max-width: 640px) {
body {
padding: 0 !important;
}
h1,
h2,
h3,
h4 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h1 {
font-size: 22px !important;
}
h2 {
font-size: 18px !important;
}
h3 {
font-size: 16px !important;
}
.container {
padding: 0 !important;
width: 100% !important;
}
.content {
padding: 0 !important;
}
.content-wrap {
padding: 10px !important;
}
.invoice {
width: 100% !important;
}
}
</style>
</head>
<body itemscope itemtype="http://schema.org/EmailMessage">
<table class="body-wrap">
<tr>
<td></td>
<td class="container" width="600">
<div class="content">
<table class="main" width="100%" cellpadding="0" cellspacing="0">
<tr>
{{ if gt (len .Alerts.Firing) 0 }}
<td class="alert alert-warning">
{{ else }}
<td class="alert alert-good">
{{ end }}
{{ .Alerts | len }} alert{{ if gt (len .Alerts) 1 }}s{{ end }} for {{ range .GroupLabels.SortedPairs }}
{{ .Name }}={{ .Value }}
{{ end }}
</td>
</tr>
<tr>
<td class="content-wrap">
<table width="100%" cellpadding="0" cellspacing="0">
{{ if gt (len .Alerts.Firing) 0 }}
<tr>
<td class="content-block">
<strong>[{{ .Alerts.Firing | len }}] Firing</strong>
</td>
</tr>
{{ end }}
{{ range .Alerts.Firing }}
<tr>
<td class="content-block">
<strong>Labels</strong><br />
{{ range .Labels.SortedPairs }}{{ .Name }} = {{ .Value }}<br />{{ end }}
{{ if gt (len .Annotations) 0 }}<strong>Annotations</strong><br />{{ end }}
{{ range .Annotations.SortedPairs }}{{ .Name }} = {{ .Value }}<br />{{ end }}
<a href="{{ .GeneratorURL }}">Source</a><br />
</td>
</tr>
{{ end }}
{{ if gt (len .Alerts.Resolved) 0 }}
{{ if gt (len .Alerts.Firing) 0 }}
<tr>
<td class="content-block">
<br />
<hr />
<br />
</td>
</tr>
{{ end }}
<tr>
<td class="content-block">
<strong>[{{ .Alerts.Resolved | len }}] Resolved</strong>
</td>
</tr>
{{ end }}
{{ range .Alerts.Resolved }}
<tr>
<td class="content-block">
<strong>Labels</strong><br />
{{ range .Labels.SortedPairs }}{{ .Name }} = {{ .Value }}<br />{{ end }}
{{ if gt (len .Annotations) 0 }}<strong>Annotations</strong><br />{{ end }}
{{ range .Annotations.SortedPairs }}{{ .Name }} = {{ .Value }}<br />{{ end }}
<a href="{{ .GeneratorURL }}">Source</a><br />
</td>
</tr>
{{ end }}
</table>
</td>
</tr>
</table>
</div>
</td>
<td></td>
</tr>
</table>
</body>
</html>`,
};

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