Compare commits

...

14 Commits

Author SHA1 Message Date
Vibhu Pandey
918c8942c4 feat(alertmanager): add service for alertmanager (#7136)
### Summary

- adds an alertmanager service
2025-02-18 07:36:31 +00:00
Shaheer Kochai
7e1301b8d2 fix: prefer local storage list options over url query options and handle an edge case (#7120)
* fix(LogsExplorer): prefer local storage options of url query options

* fix(LogsExplorer): don't add timestamp/body if local storage selectColumns exist & already migrated

* fix(LogsExplorer): improve query migration logic for local storage options

---------

Co-authored-by: Nityananda Gohain <nityanandagohain@gmail.com>
2025-02-18 07:07:14 +00:00
Nityananda Gohain
eba2049a4d fix: signoz shouldn't panic with postgres provider (#7138)
All necessary changes so that whatever initalize SQL commans run, they are moved to bun so that it works with both sqlite and postgres.
2025-02-18 11:08:09 +05:30
Vibhu Pandey
2a939e813d feat(sqlstorehook): add sqlstore logging hook (#7137)
### Summary

add sqlstore logging hook
2025-02-17 21:13:40 +05:30
Nityananda Gohain
c3951afdfd fix: refactor auth package (#7110)
* fix: refactor auth package

* fix: minor changes

* fix: refactor jwt

* fix: add tests and address comments

* fix: address comments

* fix: add uncomitted file

* fix: address comments

* fix: update tests
2025-02-17 18:16:41 +05:30
SagarRajput-7
fbf0e4efc7 fix: fixed paths to unblock lint check failing locally (#7114)
* fix: fixed paths to unblock lint check failing locally

* fix: fixed folder name path

* fix: fixed folder name path - api
2025-02-17 09:51:19 +00:00
SagarRajput-7
1f52139ed3 chore: added kakfa analytics (#7127) 2025-02-17 09:34:57 +00:00
SagarRajput-7
e86c7c970a chore: added mq overview page analytics (#7128) 2025-02-17 14:56:11 +05:30
Nityananda Gohain
8bfca9b564 fix: analytics middleware fixed (#7133) 2025-02-17 14:38:13 +05:30
SagarRajput-7
5a107f33f2 chore: added mq celery analytics (#7129) 2025-02-17 13:59:16 +05:30
Vibhu Pandey
a6cfb63036 fix(alertmanager): fix a flaky test (#7123) 2025-02-16 19:02:33 +05:30
Yunus M
042f31116a update project maintainers (#7116)
Co-authored-by: Shaheer Kochai <ashaheerki@gmail.com>
2025-02-15 13:52:05 +00:00
Srikanth Chekuri
e7587612e7 Revert "fix(otelcol): remove the v2 metrics exporter configuration (#… …7105)" (#7118) 2025-02-15 16:29:56 +05:30
SagarRajput-7
43a1fa53aa fix: fixed selected value getting reset for variables and refetch not going (#7115) 2025-02-15 00:23:33 +05:30
119 changed files with 2163 additions and 981 deletions

View File

@@ -219,12 +219,18 @@ Not sure how to get started? Just ping us on `#contributing` in our [slack commu
- [Nityananda Gohain](https://github.com/nityanandagohain)
- [Srikanth Chekuri](https://github.com/srikanthccv)
- [Vishal Sharma](https://github.com/makeavish)
- [Shivanshu Raj Shrivastava](https://github.com/shivanshuraj1333)
- [Ekansh Gupta](https://github.com/eKuG)
- [Aniket Agarwal](https://github.com/aniketio-ctrl)
#### Frontend
- [Yunus M](https://github.com/YounixM)
- [Vikrant Gupta](https://github.com/vikrantgupta25)
- [Sagar Rajput](https://github.com/SagarRajput-7)
- [Shaheer Kochai](https://github.com/ahmadshaheer)
- [Amlan Kumar Nandy](https://github.com/amlannandy)
- [Sahil Khan](https://github.com/sawhil)
#### DevOps

View File

@@ -224,7 +224,7 @@ services:
- ../common/signoz/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:0.111.27
image: signoz/signoz-otel-collector:0.111.28
command:
- --config=/etc/otel-collector-config.yaml
- --manager-config=/etc/manager-config.yaml
@@ -248,7 +248,7 @@ services:
- query-service
schema-migrator:
!!merge <<: *common
image: signoz/signoz-schema-migrator:0.111.24
image: signoz/signoz-schema-migrator:0.111.28
deploy:
restart_policy:
condition: on-failure

View File

@@ -160,7 +160,7 @@ services:
- ../common/signoz/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:0.111.27
image: signoz/signoz-otel-collector:0.111.28
command:
- --config=/etc/otel-collector-config.yaml
- --manager-config=/etc/manager-config.yaml
@@ -184,7 +184,7 @@ services:
- query-service
schema-migrator:
!!merge <<: *common
image: signoz/signoz-schema-migrator:0.111.24
image: signoz/signoz-schema-migrator:0.111.28
deploy:
restart_policy:
condition: on-failure

View File

@@ -66,6 +66,8 @@ exporters:
enabled: true
clickhousemetricswrite/prometheus:
endpoint: tcp://clickhouse:9000/signoz_metrics
signozclickhousemetrics:
dsn: tcp://clickhouse:9000/signoz_metrics
clickhouselogsexporter:
dsn: tcp://clickhouse:9000/signoz_logs
timeout: 10s
@@ -88,11 +90,11 @@ service:
metrics:
receivers: [otlp]
processors: [batch]
exporters: [clickhousemetricswrite]
exporters: [clickhousemetricswrite, signozclickhousemetrics]
metrics/prometheus:
receivers: [prometheus]
processors: [batch]
exporters: [clickhousemetricswrite/prometheus]
exporters: [clickhousemetricswrite/prometheus, signozclickhousemetrics]
logs:
receivers: [otlp]
processors: [batch]

View File

@@ -234,7 +234,7 @@ services:
# TODO: support otel-collector multiple replicas. Nginx/Traefik for loadbalancing?
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.27}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.28}
container_name: signoz-otel-collector
command:
- --config=/etc/otel-collector-config.yaml
@@ -260,7 +260,7 @@ services:
condition: service_healthy
schema-migrator-sync:
!!merge <<: *common
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.24}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.28}
container_name: schema-migrator-sync
command:
- sync
@@ -271,7 +271,7 @@ services:
condition: service_healthy
schema-migrator-async:
!!merge <<: *db-depend
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.24}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.28}
container_name: schema-migrator-async
command:
- async

View File

@@ -166,7 +166,7 @@ services:
- ../common/signoz/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.27}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.28}
container_name: signoz-otel-collector
command:
- --config=/etc/otel-collector-config.yaml
@@ -188,7 +188,7 @@ services:
condition: service_healthy
schema-migrator-sync:
!!merge <<: *common
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.24}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.28}
container_name: schema-migrator-sync
command:
- sync
@@ -199,7 +199,7 @@ services:
condition: service_healthy
schema-migrator-async:
!!merge <<: *db-depend
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.24}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.28}
container_name: schema-migrator-async
command:
- async

View File

@@ -66,6 +66,8 @@ exporters:
enabled: true
clickhousemetricswrite/prometheus:
endpoint: tcp://clickhouse:9000/signoz_metrics
signozclickhousemetrics:
dsn: tcp://clickhouse:9000/signoz_metrics
clickhouselogsexporter:
dsn: tcp://clickhouse:9000/signoz_logs
timeout: 10s
@@ -88,11 +90,11 @@ service:
metrics:
receivers: [otlp]
processors: [batch]
exporters: [clickhousemetricswrite]
exporters: [clickhousemetricswrite, signozclickhousemetrics]
metrics/prometheus:
receivers: [prometheus]
processors: [batch]
exporters: [clickhousemetricswrite/prometheus]
exporters: [clickhousemetricswrite/prometheus, signozclickhousemetrics]
logs:
receivers: [otlp]
processors: [batch]

36
ee/http/middleware/pat.go Normal file
View File

@@ -0,0 +1,36 @@
package middleware
import (
"net/http"
"go.signoz.io/signoz/pkg/types/authtypes"
)
type Pat struct {
uuid *authtypes.UUID
headers []string
}
func NewPat(headers []string) *Pat {
return &Pat{uuid: authtypes.NewUUID(), headers: headers}
}
func (p *Pat) Wrap(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var values []string
for _, header := range p.headers {
values = append(values, r.Header.Get(header))
}
ctx, err := p.uuid.ContextFromRequest(r.Context(), values...)
if err != nil {
next.ServeHTTP(w, r)
return
}
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}

View File

@@ -20,6 +20,7 @@ import (
basemodel "go.signoz.io/signoz/pkg/query-service/model"
rules "go.signoz.io/signoz/pkg/query-service/rules"
"go.signoz.io/signoz/pkg/query-service/version"
"go.signoz.io/signoz/pkg/types/authtypes"
)
type APIHandlerOptions struct {
@@ -41,6 +42,7 @@ type APIHandlerOptions struct {
FluxInterval time.Duration
UseLogsNewSchema bool
UseTraceNewSchema bool
JWT *authtypes.JWT
}
type APIHandler struct {

View File

@@ -50,7 +50,7 @@ func (ah *APIHandler) loginUser(w http.ResponseWriter, r *http.Request) {
}
// if all looks good, call auth
resp, err := baseauth.Login(ctx, &req)
resp, err := baseauth.Login(ctx, &req, ah.opts.JWT)
if ah.HandleError(w, err, http.StatusUnauthorized) {
return
}
@@ -253,7 +253,7 @@ func (ah *APIHandler) receiveGoogleAuth(w http.ResponseWriter, r *http.Request)
return
}
nextPage, err := ah.AppDao().PrepareSsoRedirect(ctx, redirectUri, identity.Email)
nextPage, err := ah.AppDao().PrepareSsoRedirect(ctx, redirectUri, identity.Email, ah.opts.JWT)
if err != nil {
zap.L().Error("[receiveGoogleAuth] failed to generate redirect URI after successful login ", zap.String("domain", domain.String()), zap.Error(err))
handleSsoError(w, r, redirectUri)
@@ -331,7 +331,7 @@ func (ah *APIHandler) receiveSAML(w http.ResponseWriter, r *http.Request) {
return
}
nextPage, err := ah.AppDao().PrepareSsoRedirect(ctx, redirectUri, email)
nextPage, err := ah.AppDao().PrepareSsoRedirect(ctx, redirectUri, email, ah.opts.JWT)
if err != nil {
zap.L().Error("[receiveSAML] failed to generate redirect URI after successful login ", zap.String("domain", domain.String()), zap.Error(err))
handleSsoError(w, r, redirectUri)

View File

@@ -37,7 +37,7 @@ func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseW
return
}
currentUser, err := auth.GetUserFromRequest(r)
currentUser, err := auth.GetUserFromReqContext(r.Context())
if err != nil {
RespondError(w, basemodel.UnauthorizedError(fmt.Errorf(
"couldn't deduce current user: %w", err,
@@ -183,7 +183,7 @@ func (ah *APIHandler) getOrCreateCloudIntegrationUser(
if apiErr != nil {
return nil, basemodel.WrapApiError(apiErr, "couldn't get viewer group for creating integration user")
}
newUser.GroupId = viewerGroup.Id
newUser.GroupId = viewerGroup.ID
passwordHash, err := auth.PasswordHash(uuid.NewString())
if err != nil {

View File

@@ -34,7 +34,7 @@ func (ah *APIHandler) createPAT(w http.ResponseWriter, r *http.Request) {
RespondError(w, model.BadRequest(err), nil)
return
}
user, err := auth.GetUserFromRequest(r)
user, err := auth.GetUserFromReqContext(r.Context())
if err != nil {
RespondError(w, &model.ApiError{
Typ: model.ErrorUnauthorized,
@@ -97,7 +97,7 @@ func (ah *APIHandler) updatePAT(w http.ResponseWriter, r *http.Request) {
return
}
user, err := auth.GetUserFromRequest(r)
user, err := auth.GetUserFromReqContext(r.Context())
if err != nil {
RespondError(w, &model.ApiError{
Typ: model.ErrorUnauthorized,
@@ -127,7 +127,7 @@ func (ah *APIHandler) updatePAT(w http.ResponseWriter, r *http.Request) {
func (ah *APIHandler) getPATs(w http.ResponseWriter, r *http.Request) {
ctx := context.Background()
user, err := auth.GetUserFromRequest(r)
user, err := auth.GetUserFromReqContext(r.Context())
if err != nil {
RespondError(w, &model.ApiError{
Typ: model.ErrorUnauthorized,
@@ -147,7 +147,7 @@ func (ah *APIHandler) getPATs(w http.ResponseWriter, r *http.Request) {
func (ah *APIHandler) revokePAT(w http.ResponseWriter, r *http.Request) {
ctx := context.Background()
id := mux.Vars(r)["id"]
user, err := auth.GetUserFromRequest(r)
user, err := auth.GetUserFromReqContext(r.Context())
if err != nil {
RespondError(w, &model.ApiError{
Typ: model.ErrorUnauthorized,

View File

@@ -1,25 +1,20 @@
package app
import (
"bufio"
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
_ "net/http/pprof" // http profiler
"regexp"
"time"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/jmoiron/sqlx"
"github.com/rs/cors"
"github.com/soheilhy/cmux"
eemiddleware "go.signoz.io/signoz/ee/http/middleware"
"go.signoz.io/signoz/ee/query-service/app/api"
"go.signoz.io/signoz/ee/query-service/app/db"
"go.signoz.io/signoz/ee/query-service/auth"
@@ -29,9 +24,8 @@ import (
"go.signoz.io/signoz/ee/query-service/interfaces"
"go.signoz.io/signoz/ee/query-service/rules"
"go.signoz.io/signoz/pkg/http/middleware"
baseauth "go.signoz.io/signoz/pkg/query-service/auth"
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
"go.signoz.io/signoz/pkg/signoz"
"go.signoz.io/signoz/pkg/types/authtypes"
"go.signoz.io/signoz/pkg/web"
licensepkg "go.signoz.io/signoz/ee/query-service/license"
@@ -80,6 +74,7 @@ type ServerOptions struct {
GatewayUrl string
UseLogsNewSchema bool
UseTraceNewSchema bool
Jwt *authtypes.JWT
}
// Server runs HTTP api service
@@ -110,7 +105,7 @@ func (s Server) HealthCheckStatus() chan healthcheck.Status {
// NewServer creates and initializes Server
func NewServer(serverOptions *ServerOptions) (*Server, error) {
modelDao, err := dao.InitDao(serverOptions.SigNoz.SQLStore.SQLxDB())
modelDao, err := dao.InitDao(serverOptions.SigNoz.SQLStore)
if err != nil {
return nil, err
}
@@ -133,7 +128,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
}
// initiate license manager
lm, err := licensepkg.StartManager(serverOptions.SigNoz.SQLStore.SQLxDB())
lm, err := licensepkg.StartManager(serverOptions.SigNoz.SQLStore.SQLxDB(), serverOptions.SigNoz.SQLStore.BunDB())
if err != nil {
return nil, err
}
@@ -202,14 +197,14 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
return nil, err
}
integrationsController, err := integrations.NewController(serverOptions.SigNoz.SQLStore.SQLxDB())
integrationsController, err := integrations.NewController(serverOptions.SigNoz.SQLStore)
if err != nil {
return nil, fmt.Errorf(
"couldn't create integrations controller: %w", err,
)
}
cloudIntegrationsController, err := cloudintegrations.NewController(serverOptions.SigNoz.SQLStore.SQLxDB())
cloudIntegrationsController, err := cloudintegrations.NewController(serverOptions.SigNoz.SQLStore)
if err != nil {
return nil, fmt.Errorf(
"couldn't create cloud provider integrations controller: %w", err,
@@ -269,6 +264,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
GatewayUrl: serverOptions.GatewayUrl,
UseLogsNewSchema: serverOptions.UseLogsNewSchema,
UseTraceNewSchema: serverOptions.UseTraceNewSchema,
JWT: serverOptions.Jwt,
}
apiHandler, err := api.NewAPIHandler(apiOpts)
@@ -311,6 +307,8 @@ func (s *Server) createPrivateServer(apiHandler *api.APIHandler) (*http.Server,
r := baseapp.NewRouter()
r.Use(middleware.NewAuth(zap.L(), s.serverOptions.Jwt, []string{"Authorization", "Sec-WebSocket-Protocol"}).Wrap)
r.Use(eemiddleware.NewPat([]string{"SIGNOZ-API-KEY"}).Wrap)
r.Use(middleware.NewTimeout(zap.L(),
s.serverOptions.Config.APIServer.Timeout.ExcludedRoutes,
s.serverOptions.Config.APIServer.Timeout.Default,
@@ -342,8 +340,8 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
r := baseapp.NewRouter()
// add auth middleware
getUserFromRequest := func(r *http.Request) (*basemodel.UserPayload, error) {
user, err := auth.GetUserFromRequest(r, apiHandler)
getUserFromRequest := func(ctx context.Context) (*basemodel.UserPayload, error) {
user, err := auth.GetUserFromRequestContext(ctx, apiHandler)
if err != nil {
return nil, err
@@ -357,6 +355,8 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
}
am := baseapp.NewAuthMiddleware(getUserFromRequest)
r.Use(middleware.NewAuth(zap.L(), s.serverOptions.Jwt, []string{"Authorization", "Sec-WebSocket-Protocol"}).Wrap)
r.Use(eemiddleware.NewPat([]string{"SIGNOZ-API-KEY"}).Wrap)
r.Use(middleware.NewTimeout(zap.L(),
s.serverOptions.Config.APIServer.Timeout.ExcludedRoutes,
s.serverOptions.Config.APIServer.Timeout.Default,
@@ -395,174 +395,6 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
}, nil
}
// TODO(remove): Implemented at pkg/http/middleware/logging.go
type loggingResponseWriter struct {
http.ResponseWriter
statusCode int
}
// TODO(remove): Implemented at pkg/http/middleware/logging.go
func NewLoggingResponseWriter(w http.ResponseWriter) *loggingResponseWriter {
// WriteHeader(int) is not called if our response implicitly returns 200 OK, so
// we default to that status code.
return &loggingResponseWriter{w, http.StatusOK}
}
// TODO(remove): Implemented at pkg/http/middleware/logging.go
func (lrw *loggingResponseWriter) WriteHeader(code int) {
lrw.statusCode = code
lrw.ResponseWriter.WriteHeader(code)
}
// TODO(remove): Implemented at pkg/http/middleware/logging.go
// Flush implements the http.Flush interface.
func (lrw *loggingResponseWriter) Flush() {
lrw.ResponseWriter.(http.Flusher).Flush()
}
// TODO(remove): Implemented at pkg/http/middleware/logging.go
// Support websockets
func (lrw *loggingResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
h, ok := lrw.ResponseWriter.(http.Hijacker)
if !ok {
return nil, nil, errors.New("hijack not supported")
}
return h.Hijack()
}
func extractQueryRangeData(path string, r *http.Request) (map[string]interface{}, bool) {
pathToExtractBodyFromV3 := "/api/v3/query_range"
pathToExtractBodyFromV4 := "/api/v4/query_range"
data := map[string]interface{}{}
var postData *v3.QueryRangeParamsV3
if (r.Method == "POST") && ((path == pathToExtractBodyFromV3) || (path == pathToExtractBodyFromV4)) {
if r.Body != nil {
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
return nil, false
}
r.Body.Close() // must close
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
json.Unmarshal(bodyBytes, &postData)
} else {
return nil, false
}
} else {
return nil, false
}
referrer := r.Header.Get("Referer")
dashboardMatched, err := regexp.MatchString(`/dashboard/[a-zA-Z0-9\-]+/(new|edit)(?:\?.*)?$`, referrer)
if err != nil {
zap.L().Error("error while matching the referrer", zap.Error(err))
}
alertMatched, err := regexp.MatchString(`/alerts/(new|edit)(?:\?.*)?$`, referrer)
if err != nil {
zap.L().Error("error while matching the alert: ", zap.Error(err))
}
logsExplorerMatched, err := regexp.MatchString(`/logs/logs-explorer(?:\?.*)?$`, referrer)
if err != nil {
zap.L().Error("error while matching the logs explorer: ", zap.Error(err))
}
traceExplorerMatched, err := regexp.MatchString(`/traces-explorer(?:\?.*)?$`, referrer)
if err != nil {
zap.L().Error("error while matching the trace explorer: ", zap.Error(err))
}
queryInfoResult := telemetry.GetInstance().CheckQueryInfo(postData)
if (queryInfoResult.MetricsUsed || queryInfoResult.LogsUsed || queryInfoResult.TracesUsed) && (queryInfoResult.FilterApplied) {
if queryInfoResult.MetricsUsed {
telemetry.GetInstance().AddActiveMetricsUser()
}
if queryInfoResult.LogsUsed {
telemetry.GetInstance().AddActiveLogsUser()
}
if queryInfoResult.TracesUsed {
telemetry.GetInstance().AddActiveTracesUser()
}
data["metricsUsed"] = queryInfoResult.MetricsUsed
data["logsUsed"] = queryInfoResult.LogsUsed
data["tracesUsed"] = queryInfoResult.TracesUsed
data["filterApplied"] = queryInfoResult.FilterApplied
data["groupByApplied"] = queryInfoResult.GroupByApplied
data["aggregateOperator"] = queryInfoResult.AggregateOperator
data["aggregateAttributeKey"] = queryInfoResult.AggregateAttributeKey
data["numberOfQueries"] = queryInfoResult.NumberOfQueries
data["queryType"] = queryInfoResult.QueryType
data["panelType"] = queryInfoResult.PanelType
userEmail, err := baseauth.GetEmailFromJwt(r.Context())
if err == nil {
// switch case to set data["screen"] based on the referrer
switch {
case dashboardMatched:
data["screen"] = "panel"
case alertMatched:
data["screen"] = "alert"
case logsExplorerMatched:
data["screen"] = "logs-explorer"
case traceExplorerMatched:
data["screen"] = "traces-explorer"
default:
data["screen"] = "unknown"
return data, true
}
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_QUERY_RANGE_API, data, userEmail, true, false)
}
}
return data, true
}
func getActiveLogs(path string, r *http.Request) {
// if path == "/api/v1/dashboards/{uuid}" {
// telemetry.GetInstance().AddActiveMetricsUser()
// }
if path == "/api/v1/logs" {
hasFilters := len(r.URL.Query().Get("q"))
if hasFilters > 0 {
telemetry.GetInstance().AddActiveLogsUser()
}
}
}
func (s *Server) analyticsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := baseauth.AttachJwtToContext(r.Context(), r)
r = r.WithContext(ctx)
route := mux.CurrentRoute(r)
path, _ := route.GetPathTemplate()
queryRangeData, metadataExists := extractQueryRangeData(path, r)
getActiveLogs(path, r)
lrw := NewLoggingResponseWriter(w)
next.ServeHTTP(lrw, r)
data := map[string]interface{}{"path": path, "statusCode": lrw.statusCode}
if metadataExists {
for key, value := range queryRangeData {
data[key] = value
}
}
if _, ok := telemetry.EnabledPaths()[path]; ok {
userEmail, err := baseauth.GetEmailFromJwt(r.Context())
if err == nil {
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_PATH, data, userEmail, true, false)
}
}
})
}
// initListeners initialises listeners of the server
func (s *Server) initListeners() error {
// listen on public port

View File

@@ -3,20 +3,20 @@ package auth
import (
"context"
"fmt"
"net/http"
"time"
"go.signoz.io/signoz/ee/query-service/app/api"
baseauth "go.signoz.io/signoz/pkg/query-service/auth"
basemodel "go.signoz.io/signoz/pkg/query-service/model"
"go.signoz.io/signoz/pkg/query-service/telemetry"
"go.signoz.io/signoz/pkg/types/authtypes"
"go.uber.org/zap"
)
func GetUserFromRequest(r *http.Request, apiHandler *api.APIHandler) (*basemodel.UserPayload, error) {
patToken := r.Header.Get("SIGNOZ-API-KEY")
if len(patToken) > 0 {
func GetUserFromRequestContext(ctx context.Context, apiHandler *api.APIHandler) (*basemodel.UserPayload, error) {
patToken, ok := authtypes.UUIDFromContext(ctx)
if ok && patToken != "" {
zap.L().Debug("Received a non-zero length PAT token")
ctx := context.Background()
dao := apiHandler.AppDao()
@@ -40,7 +40,7 @@ func GetUserFromRequest(r *http.Request, apiHandler *api.APIHandler) (*basemodel
}
telemetry.GetInstance().SetPatTokenUser()
dao.UpdatePATLastUsed(ctx, patToken, time.Now().Unix())
user.User.GroupId = group.Id
user.User.GroupId = group.ID
user.User.Id = pat.Id
return &basemodel.UserPayload{
User: user.User,
@@ -52,5 +52,5 @@ func GetUserFromRequest(r *http.Request, apiHandler *api.APIHandler) (*basemodel
return nil, err
}
}
return baseauth.GetUserFromRequest(r)
return baseauth.GetUserFromReqContext(ctx)
}

View File

@@ -1,10 +1,10 @@
package dao
import (
"github.com/jmoiron/sqlx"
"go.signoz.io/signoz/ee/query-service/dao/sqlite"
"go.signoz.io/signoz/pkg/sqlstore"
)
func InitDao(inputDB *sqlx.DB) (ModelDao, error) {
return sqlite.InitDB(inputDB)
func InitDao(sqlStore sqlstore.SQLStore) (ModelDao, error) {
return sqlite.InitDB(sqlStore)
}

View File

@@ -10,6 +10,7 @@ import (
basedao "go.signoz.io/signoz/pkg/query-service/dao"
baseint "go.signoz.io/signoz/pkg/query-service/interfaces"
basemodel "go.signoz.io/signoz/pkg/query-service/model"
"go.signoz.io/signoz/pkg/types/authtypes"
)
type ModelDao interface {
@@ -22,7 +23,7 @@ type ModelDao interface {
// auth methods
CanUsePassword(ctx context.Context, email string) (bool, basemodel.BaseApiError)
PrepareSsoRedirect(ctx context.Context, redirectUri, email string) (redirectURL string, apierr basemodel.BaseApiError)
PrepareSsoRedirect(ctx context.Context, redirectUri, email string, jwt *authtypes.JWT) (redirectURL string, apierr basemodel.BaseApiError)
GetDomainFromSsoResponse(ctx context.Context, relayState *url.URL) (*model.OrgDomain, error)
// org domain (auth domains) CRUD ops

View File

@@ -14,6 +14,7 @@ import (
baseconst "go.signoz.io/signoz/pkg/query-service/constants"
basemodel "go.signoz.io/signoz/pkg/query-service/model"
"go.signoz.io/signoz/pkg/query-service/utils"
"go.signoz.io/signoz/pkg/types/authtypes"
"go.uber.org/zap"
)
@@ -48,7 +49,7 @@ func (m *modelDao) createUserForSAMLRequest(ctx context.Context, email string) (
Password: hash,
CreatedAt: time.Now().Unix(),
ProfilePictureURL: "", // Currently unused
GroupId: group.Id,
GroupId: group.ID,
OrgId: domain.OrgId,
}
@@ -64,7 +65,7 @@ func (m *modelDao) createUserForSAMLRequest(ctx context.Context, email string) (
// PrepareSsoRedirect prepares redirect page link after SSO response
// is successfully parsed (i.e. valid email is available)
func (m *modelDao) PrepareSsoRedirect(ctx context.Context, redirectUri, email string) (redirectURL string, apierr basemodel.BaseApiError) {
func (m *modelDao) PrepareSsoRedirect(ctx context.Context, redirectUri, email string, jwt *authtypes.JWT) (redirectURL string, apierr basemodel.BaseApiError) {
userPayload, apierr := m.GetUserByEmail(ctx, email)
if !apierr.IsNil() {
@@ -85,7 +86,7 @@ func (m *modelDao) PrepareSsoRedirect(ctx context.Context, redirectUri, email st
user = &userPayload.User
}
tokenStore, err := baseauth.GenerateJWTForUser(user)
tokenStore, err := baseauth.GenerateJWTForUser(user, jwt)
if err != nil {
zap.L().Error("failed to generate token for SSO login user", zap.Error(err))
return "", model.InternalErrorStr("failed to generate token for the user")

View File

@@ -7,6 +7,7 @@ import (
basedao "go.signoz.io/signoz/pkg/query-service/dao"
basedsql "go.signoz.io/signoz/pkg/query-service/dao/sqlite"
baseint "go.signoz.io/signoz/pkg/query-service/interfaces"
"go.signoz.io/signoz/pkg/sqlstore"
)
type modelDao struct {
@@ -29,8 +30,8 @@ func (m *modelDao) checkFeature(key string) error {
}
// InitDB creates and extends base model DB repository
func InitDB(inputDB *sqlx.DB) (*modelDao, error) {
dao, err := basedsql.InitDB(inputDB)
func InitDB(sqlStore sqlstore.SQLStore) (*modelDao, error) {
dao, err := basedsql.InitDB(sqlStore)
if err != nil {
return nil, err
}

View File

@@ -9,21 +9,25 @@ import (
"github.com/jmoiron/sqlx"
"github.com/mattn/go-sqlite3"
"github.com/uptrace/bun"
"go.signoz.io/signoz/ee/query-service/model"
basemodel "go.signoz.io/signoz/pkg/query-service/model"
"go.signoz.io/signoz/pkg/types"
"go.uber.org/zap"
)
// Repo is license repo. stores license keys in a secured DB
type Repo struct {
db *sqlx.DB
db *sqlx.DB
bundb *bun.DB
}
// NewLicenseRepo initiates a new license repo
func NewLicenseRepo(db *sqlx.DB) Repo {
func NewLicenseRepo(db *sqlx.DB, bundb *bun.DB) Repo {
return Repo{
db: db,
db: db,
bundb: bundb,
}
}
@@ -165,24 +169,25 @@ func (r *Repo) UpdateLicenseV3(ctx context.Context, l *model.LicenseV3) error {
return nil
}
func (r *Repo) CreateFeature(req *basemodel.Feature) *basemodel.ApiError {
func (r *Repo) CreateFeature(req *types.FeatureStatus) *basemodel.ApiError {
_, err := r.db.Exec(
`INSERT INTO feature_status (name, active, usage, usage_limit, route)
VALUES (?, ?, ?, ?, ?);`,
req.Name, req.Active, req.Usage, req.UsageLimit, req.Route)
_, err := r.bundb.NewInsert().
Model(req).
Exec(context.Background())
if err != nil {
return &basemodel.ApiError{Typ: basemodel.ErrorInternal, Err: err}
}
return nil
}
func (r *Repo) GetFeature(featureName string) (basemodel.Feature, error) {
func (r *Repo) GetFeature(featureName string) (types.FeatureStatus, error) {
var feature types.FeatureStatus
var feature basemodel.Feature
err := r.bundb.NewSelect().
Model(&feature).
Where("name = ?", featureName).
Scan(context.Background())
err := r.db.Get(&feature,
`SELECT * FROM feature_status WHERE name = ?;`, featureName)
if err != nil {
return feature, err
}
@@ -205,18 +210,19 @@ func (r *Repo) GetAllFeatures() ([]basemodel.Feature, error) {
return feature, nil
}
func (r *Repo) UpdateFeature(req basemodel.Feature) error {
func (r *Repo) UpdateFeature(req types.FeatureStatus) error {
_, err := r.db.Exec(
`UPDATE feature_status SET active = ?, usage = ?, usage_limit = ?, route = ? WHERE name = ?;`,
req.Active, req.Usage, req.UsageLimit, req.Route, req.Name)
_, err := r.bundb.NewUpdate().
Model(&req).
Where("name = ?", req.Name).
Exec(context.Background())
if err != nil {
return err
}
return nil
}
func (r *Repo) InitFeatures(req basemodel.FeatureSet) error {
func (r *Repo) InitFeatures(req []types.FeatureStatus) error {
// get a feature by name, if it doesn't exist, create it. If it does exist, update it.
for _, feature := range req {
currentFeature, err := r.GetFeature(feature.Name)
@@ -229,7 +235,7 @@ func (r *Repo) InitFeatures(req basemodel.FeatureSet) error {
} else if err != nil {
return err
}
feature.Usage = currentFeature.Usage
feature.Usage = int(currentFeature.Usage)
if feature.Usage >= feature.UsageLimit && feature.UsageLimit != -1 {
feature.Active = false
}

View File

@@ -7,11 +7,13 @@ import (
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
"github.com/uptrace/bun"
"sync"
"go.signoz.io/signoz/pkg/query-service/auth"
baseconstants "go.signoz.io/signoz/pkg/query-service/constants"
"go.signoz.io/signoz/pkg/types"
"go.signoz.io/signoz/pkg/types/authtypes"
validate "go.signoz.io/signoz/ee/query-service/integrations/signozio"
"go.signoz.io/signoz/ee/query-service/model"
@@ -43,12 +45,12 @@ type Manager struct {
activeFeatures basemodel.FeatureSet
}
func StartManager(db *sqlx.DB, features ...basemodel.Feature) (*Manager, error) {
func StartManager(db *sqlx.DB, bundb *bun.DB, features ...basemodel.Feature) (*Manager, error) {
if LM != nil {
return LM, nil
}
repo := NewLicenseRepo(db)
repo := NewLicenseRepo(db, bundb)
m := &Manager{
repo: &repo,
}
@@ -237,10 +239,10 @@ func (lm *Manager) ValidateV3(ctx context.Context) (reterr error) {
func (lm *Manager) ActivateV3(ctx context.Context, licenseKey string) (licenseResponse *model.LicenseV3, errResponse *model.ApiError) {
defer func() {
if errResponse != nil {
userEmail, err := auth.GetEmailFromJwt(ctx)
if err == nil {
claims, ok := authtypes.ClaimsFromContext(ctx)
if ok {
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_LICENSE_ACT_FAILED,
map[string]interface{}{"err": errResponse.Err.Error()}, userEmail, true, false)
map[string]interface{}{"err": errResponse.Err.Error()}, claims.Email, true, false)
}
}
}()
@@ -282,15 +284,41 @@ func (lm *Manager) GetFeatureFlags() (basemodel.FeatureSet, error) {
}
func (lm *Manager) InitFeatures(features basemodel.FeatureSet) error {
return lm.repo.InitFeatures(features)
featureStatus := make([]types.FeatureStatus, len(features))
for i, f := range features {
featureStatus[i] = types.FeatureStatus{
Name: f.Name,
Active: f.Active,
Usage: int(f.Usage),
UsageLimit: int(f.UsageLimit),
Route: f.Route,
}
}
return lm.repo.InitFeatures(featureStatus)
}
func (lm *Manager) UpdateFeatureFlag(feature basemodel.Feature) error {
return lm.repo.UpdateFeature(feature)
return lm.repo.UpdateFeature(types.FeatureStatus{
Name: feature.Name,
Active: feature.Active,
Usage: int(feature.Usage),
UsageLimit: int(feature.UsageLimit),
Route: feature.Route,
})
}
func (lm *Manager) GetFeatureFlag(key string) (basemodel.Feature, error) {
return lm.repo.GetFeature(key)
featureStatus, err := lm.repo.GetFeature(key)
if err != nil {
return basemodel.Feature{}, err
}
return basemodel.Feature{
Name: featureStatus.Name,
Active: featureStatus.Active,
Usage: int64(featureStatus.Usage),
UsageLimit: int64(featureStatus.UsageLimit),
Route: featureStatus.Route,
}, nil
}
// GetRepo return the license repo

View File

@@ -20,6 +20,7 @@ import (
baseconst "go.signoz.io/signoz/pkg/query-service/constants"
"go.signoz.io/signoz/pkg/query-service/version"
"go.signoz.io/signoz/pkg/signoz"
"go.signoz.io/signoz/pkg/types/authtypes"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
@@ -154,6 +155,16 @@ func main() {
zap.L().Fatal("Failed to create signoz struct", zap.Error(err))
}
jwtSecret := os.Getenv("SIGNOZ_JWT_SECRET")
if len(jwtSecret) == 0 {
zap.L().Warn("No JWT secret key is specified.")
} else {
zap.L().Info("JWT secret key set successfully.")
}
jwt := authtypes.NewJWT(jwtSecret, 30*time.Minute, 30*24*time.Hour)
serverOptions := &app.ServerOptions{
Config: config,
SigNoz: signoz,
@@ -171,15 +182,7 @@ func main() {
GatewayUrl: gatewayUrl,
UseLogsNewSchema: useLogsNewSchema,
UseTraceNewSchema: useTraceNewSchema,
}
// Read the jwt secret key
auth.JwtSecret = os.Getenv("SIGNOZ_JWT_SECRET")
if len(auth.JwtSecret) == 0 {
zap.L().Warn("No JWT secret key is specified.")
} else {
zap.L().Info("JWT secret key set successfully.")
Jwt: jwt,
}
server, err := app.NewServer(serverOptions)

View File

@@ -15,6 +15,7 @@ import {
Typography,
} from 'antd';
import { FilterDropdownProps } from 'antd/lib/table/interface';
import logEvent from 'api/common/logEvent';
import {
getQueueOverview,
QueueOverviewResponse,
@@ -458,6 +459,7 @@ export default function CeleryOverviewTable({
const handleRowClick = (record: RowData): void => {
onRowClick(record);
logEvent('MQ Overview Page: Right Panel', { ...record });
};
const getFilteredData = useCallback(
@@ -481,6 +483,22 @@ export default function CeleryOverviewTable({
tableData,
]);
const prevTableDataRef = useRef<string>();
useEffect(() => {
if (tableData.length > 0) {
const currentTableData = JSON.stringify(tableData);
if (currentTableData !== prevTableDataRef.current) {
logEvent(`MQ Overview Page: List rendered`, {
dataRender: tableData.length,
});
prevTableDataRef.current = currentTableData;
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify(tableData)]);
return (
<div className="celery-overview-table-container">
<Input.Search

View File

@@ -2,6 +2,7 @@ import './CeleryTaskDetail.style.scss';
import { Color, Spacing } from '@signozhq/design-tokens';
import { Divider, Drawer, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import { PANEL_TYPES } from 'constants/queryBuilder';
import dayjs from 'dayjs';
import { useIsDarkMode } from 'hooks/useDarkMode';
@@ -98,6 +99,12 @@ export default function CeleryTaskDetail({
...rowData,
[taskData.entity]: taskData.value,
});
logEvent('MQ Celery: navigation to trace page', {
filters,
startTime,
endTime,
source: widgetData.title,
});
navigateToTrace(filters, startTime, endTime);
}}
start={startTime}

View File

@@ -42,10 +42,12 @@ import {
function CeleryTaskBar({
onClick,
queryEnabled,
checkIfDataExists,
}: {
onClick?: (task: CaptureDataProps) => void;
queryEnabled: boolean;
checkIfDataExists?: (isDataAvailable: boolean) => void;
}): JSX.Element {
const history = useHistory();
const { pathname } = useLocation();
@@ -187,6 +189,7 @@ function CeleryTaskBar({
onGraphClick(celerySlowestTasksTableWidgetData, ...args)
}
customSeries={getCustomSeries}
dataAvailable={checkIfDataExists}
/>
)}
{barState === CeleryTaskState.Failed && (
@@ -232,6 +235,7 @@ function CeleryTaskBar({
CeleryTaskBar.defaultProps = {
onClick: (): void => {},
checkIfDataExists: undefined,
};
export default CeleryTaskBar;

View File

@@ -35,6 +35,8 @@ function CeleryTaskGraph({
customErrorMessage,
start,
end,
checkIfDataExists,
analyticsEvent,
}: {
widgetData: Widgets;
onClick?: (task: CaptureDataProps) => void;
@@ -48,6 +50,8 @@ function CeleryTaskGraph({
customErrorMessage?: string;
start?: number;
end?: number;
checkIfDataExists?: (isDataAvailable: boolean) => void;
analyticsEvent?: string;
}): JSX.Element {
const history = useHistory();
const { pathname } = useLocation();
@@ -125,6 +129,8 @@ function CeleryTaskGraph({
customErrorMessage={customErrorMessage}
start={start}
end={end}
dataAvailable={checkIfDataExists}
analyticsEvent={analyticsEvent}
/>
</Card>
);
@@ -141,6 +147,8 @@ CeleryTaskGraph.defaultProps = {
customErrorMessage: undefined,
start: undefined,
end: undefined,
checkIfDataExists: undefined,
analyticsEvent: undefined,
};
export default CeleryTaskGraph;

View File

@@ -1,6 +1,7 @@
import './CeleryTaskGraph.style.scss';
import { Card, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import { CardContainer } from 'container/GridCardLayout/styles';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { ChevronDown, ChevronUp } from 'lucide-react';
@@ -92,6 +93,15 @@ export default function CeleryTaskGraphGrid({
}));
};
const checkIfDataExists = (isDataAvailable: boolean, title: string): void => {
if (isDataAvailable) {
logEvent(`MQ Celery: ${title} data exists`, {
graph: title,
isDataAvailable,
});
}
};
return (
<div className="celery-task-graph-grid-container">
<div className="metric-based-graphs">
@@ -124,6 +134,10 @@ export default function CeleryTaskGraphGrid({
widgetData={celeryActiveTasksData}
queryEnabled={queryEnabled}
customErrorMessage="Enable Flower metrics to view this graph"
checkIfDataExists={(isDataAvailable): void =>
checkIfDataExists(isDataAvailable, 'Active Tasks by worker')
}
analyticsEvent="MQ Celery: Flower metric not enabled"
/>
<Card className="celery-task-graph-worker-count">
<div className="worker-count-header">
@@ -173,8 +187,19 @@ export default function CeleryTaskGraphGrid({
</div>
{!collapsedSections.traceBasedGraphs && (
<>
<CeleryTaskBar queryEnabled={queryEnabled} onClick={onClick} />
<CeleryTaskLatencyGraph queryEnabled={queryEnabled} />
<CeleryTaskBar
queryEnabled={queryEnabled}
onClick={onClick}
checkIfDataExists={(isDataAvailable): void =>
checkIfDataExists(isDataAvailable, 'State Graph')
}
/>
<CeleryTaskLatencyGraph
queryEnabled={queryEnabled}
checkIfDataExists={(isDataAvailable): void =>
checkIfDataExists(isDataAvailable, 'Task Latency')
}
/>
<div className="celery-task-graph-grid-bottom">
{bottomWidgetData.map((widgetData, index) => (
<CeleryTaskGraph
@@ -184,6 +209,9 @@ export default function CeleryTaskGraphGrid({
queryEnabled={queryEnabled}
rightPanelTitle={rightPanelTitle[index]}
applyCeleryTaskFilter
checkIfDataExists={(isDataAvailable): void =>
checkIfDataExists(isDataAvailable, rightPanelTitle[index])
}
/>
))}
</div>

View File

@@ -1,6 +1,7 @@
import './CeleryTaskGraph.style.scss';
import { Col, Row } from 'antd';
import logEvent from 'api/common/logEvent';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { ViewMenuAction } from 'container/GridCardLayout/config';
@@ -40,8 +41,10 @@ export enum CeleryTaskGraphState {
function CeleryTaskLatencyGraph({
queryEnabled,
checkIfDataExists,
}: {
queryEnabled: boolean;
checkIfDataExists?: (isDataAvailable: boolean) => void;
}): JSX.Element {
const history = useHistory();
const { pathname } = useLocation();
@@ -61,6 +64,10 @@ function CeleryTaskLatencyGraph({
const handleTabClick = (key: CeleryTaskGraphState): void => {
setGraphState(key as CeleryTaskGraphState);
logEvent('MQ Celery: Task latency graph tab clicked', {
taskName: urlQuery.get(QueryParams.taskName),
graphState: key,
});
};
const onDragSelect = useCallback(
@@ -195,6 +202,7 @@ function CeleryTaskLatencyGraph({
onDragSelect={onDragSelect}
onClickHandler={onGraphClick('Celery_p99_latency')}
isQueryEnabled={queryEnabled}
dataAvailable={checkIfDataExists}
/>
</>
)}
@@ -215,6 +223,7 @@ function CeleryTaskLatencyGraph({
onDragSelect={onDragSelect}
onClickHandler={onGraphClick('Celery_p95_latency')}
isQueryEnabled={queryEnabled}
dataAvailable={checkIfDataExists}
/>
</>
)}
@@ -234,6 +243,7 @@ function CeleryTaskLatencyGraph({
onDragSelect={onDragSelect}
onClickHandler={onGraphClick('Celery_p90_latency')}
isQueryEnabled={queryEnabled}
dataAvailable={checkIfDataExists}
/>
</>
)}
@@ -243,3 +253,7 @@ function CeleryTaskLatencyGraph({
}
export default CeleryTaskLatencyGraph;
CeleryTaskLatencyGraph.defaultProps = {
checkIfDataExists: undefined,
};

View File

@@ -2,6 +2,7 @@
import './CeleryTaskGraph.style.scss';
import { Col, Row } from 'antd';
import logEvent from 'api/common/logEvent';
import { QueryParams } from 'constants/query';
import useUrlQuery from 'hooks/useUrlQuery';
import { Dispatch, SetStateAction, useMemo } from 'react';
@@ -44,12 +45,16 @@ function CeleryTaskStateGraphConfig({
{ label: 'Successful', key: CeleryTaskState.Successful },
];
const urlQuery = useUrlQuery();
const handleTabClick = (key: CeleryTaskState): void => {
setBarState(key as CeleryTaskState);
logEvent('MQ Celery: State graph tab clicked', {
taskName: urlQuery.get(QueryParams.taskName),
graphState: key,
});
};
const urlQuery = useUrlQuery();
const selectedFilters = useMemo(
() =>
getFiltersFromQueryParams(

View File

@@ -3,7 +3,7 @@ import './AccountActions.style.scss';
import { Color } from '@signozhq/design-tokens';
import { Button, Select, Skeleton } from 'antd';
import { SelectProps } from 'antd/lib';
import { useAwsAccounts } from 'hooks/integrations/aws/useAwsAccounts';
import { useAwsAccounts } from 'hooks/integration/aws/useAwsAccounts';
import useUrlQuery from 'hooks/useUrlQuery';
import { Check, ChevronDown } from 'lucide-react';
import { useEffect, useMemo, useState } from 'react';

View File

@@ -6,7 +6,7 @@ import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import {
getRegionPreviewText,
useAccountSettingsModal,
} from 'hooks/integrations/aws/useAccountSettingsModal';
} from 'hooks/integration/aws/useAccountSettingsModal';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import { Dispatch, SetStateAction, useCallback } from 'react';

View File

@@ -3,7 +3,7 @@ import './CloudAccountSetupModal.style.scss';
import { Color } from '@signozhq/design-tokens';
import SignozModal from 'components/SignozModal/SignozModal';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useIntegrationModal } from 'hooks/integrations/aws/useIntegrationModal';
import { useIntegrationModal } from 'hooks/integration/aws/useIntegrationModal';
import { SquareArrowOutUpRight } from 'lucide-react';
import { useCallback } from 'react';
import { useQueryClient } from 'react-query';

View File

@@ -1,6 +1,6 @@
import { Form } from 'antd';
import cx from 'classnames';
import { useAccountStatus } from 'hooks/integrations/aws/useAccountStatus';
import { useAccountStatus } from 'hooks/integration/aws/useAccountStatus';
import { useRef } from 'react';
import { AccountStatusResponse } from 'types/api/integrations/aws';
import { regions } from 'utils/regions';

View File

@@ -1,7 +1,7 @@
import './RegionSelector.style.scss';
import { Checkbox } from 'antd';
import { useRegionSelection } from 'hooks/integrations/aws/useRegionSelection';
import { useRegionSelection } from 'hooks/integration/aws/useRegionSelection';
import { Dispatch, SetStateAction } from 'react';
import { regions } from 'utils/regions';

View File

@@ -7,7 +7,7 @@ import {
ServiceConfig,
SupportedSignals,
} from 'container/CloudIntegrationPage/ServicesSection/types';
import { useUpdateServiceConfig } from 'hooks/integrations/aws/useUpdateServiceConfig';
import { useUpdateServiceConfig } from 'hooks/integration/aws/useUpdateServiceConfig';
import { useCallback, useMemo, useState } from 'react';
import { useQueryClient } from 'react-query';

View File

@@ -5,7 +5,7 @@ import CloudServiceDashboards from 'container/CloudIntegrationPage/ServicesSecti
import CloudServiceDataCollected from 'container/CloudIntegrationPage/ServicesSection/CloudServiceDataCollected';
import { IServiceStatus } from 'container/CloudIntegrationPage/ServicesSection/types';
import dayjs from 'dayjs';
import { useServiceDetails } from 'hooks/integrations/aws/useServiceDetails';
import { useServiceDetails } from 'hooks/integration/aws/useServiceDetails';
import useUrlQuery from 'hooks/useUrlQuery';
import { useMemo, useState } from 'react';

View File

@@ -1,5 +1,5 @@
import Spinner from 'components/Spinner';
import { useGetAccountServices } from 'hooks/integrations/aws/useGetAccountServices';
import { useGetAccountServices } from 'hooks/integration/aws/useGetAccountServices';
import useUrlQuery from 'hooks/useUrlQuery';
import { useCallback, useEffect, useMemo } from 'react';
import { useNavigate } from 'react-router-dom-v5-compat';

View File

@@ -3,7 +3,7 @@ import './ServicesTabs.style.scss';
import { Color } from '@signozhq/design-tokens';
import type { SelectProps, TabsProps } from 'antd';
import { Select, Tabs } from 'antd';
import { getAwsServices } from 'api/integrations/aws';
import { getAwsServices } from 'api/integration/aws';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import useUrlQuery from 'hooks/useUrlQuery';
import { ChevronDown } from 'lucide-react';

View File

@@ -1,3 +1,4 @@
import logEvent from 'api/common/logEvent';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
@@ -44,6 +45,7 @@ function GridCardGraph({
customErrorMessage,
start,
end,
analyticsEvent,
}: GridCardGraphProps): JSX.Element {
const dispatch = useDispatch();
const [errorMessage, setErrorMessage] = useState<string>();
@@ -219,6 +221,11 @@ function GridCardGraph({
setIsInternalServerError(
String(error.message).includes('API responded with 500'),
);
if (analyticsEvent) {
logEvent(analyticsEvent, {
error: error.message,
});
}
}
setDashboardQueryRangeCalled(true);
},
@@ -283,6 +290,7 @@ GridCardGraph.defaultProps = {
threshold: undefined,
headerMenuList: [MenuItemKeys.View],
version: 'v3',
analyticsEvent: undefined,
};
export default memo(GridCardGraph);

View File

@@ -58,6 +58,7 @@ export interface GridCardGraphProps {
customErrorMessage?: string;
start?: number;
end?: number;
analyticsEvent?: string;
}
export interface GetGraphVisibilityStateOnLegendClickProps {

View File

@@ -150,11 +150,13 @@ function VariableItem({
let allSelected = false;
// The default value for multi-select is ALL and first value for
// single select
if (variableData.multiSelect) {
value = newOptionsData;
allSelected = true;
} else {
[value] = newOptionsData;
if (valueNotInList) {
if (variableData.multiSelect) {
value = newOptionsData;
allSelected = true;
} else {
[value] = newOptionsData;
}
}
if (variableData && variableData?.name && variableData?.id) {
@@ -226,6 +228,9 @@ function VariableItem({
}
setErrorMessage(message);
}
setVariablesToGetUpdated((prev) =>
prev.filter((v) => v !== variableData.name),
);
},
},
);

View File

@@ -1,7 +1,7 @@
import { Form } from 'antd';
import { FormInstance } from 'antd/lib';
import { CloudAccount } from 'container/CloudIntegrationPage/ServicesSection/types';
import { useUpdateAccountConfig } from 'hooks/integrations/aws/useUpdateAccountConfig';
import { useUpdateAccountConfig } from 'hooks/integration/aws/useUpdateAccountConfig';
import { isEqual } from 'lodash-es';
import {
Dispatch,

View File

@@ -1,4 +1,4 @@
import { getAwsAccounts } from 'api/integrations/aws';
import { getAwsAccounts } from 'api/integration/aws';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { CloudAccount } from 'container/CloudIntegrationPage/ServicesSection/types';
import { useQuery, UseQueryResult } from 'react-query';

View File

@@ -1,4 +1,4 @@
import { getConnectionParams } from 'api/integrations/aws';
import { getConnectionParams } from 'api/integration/aws';
import { AxiosError } from 'axios';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';

View File

@@ -1,4 +1,4 @@
import { generateConnectionUrl } from 'api/integrations/aws';
import { generateConnectionUrl } from 'api/integration/aws';
import { AxiosError } from 'axios';
import { useMutation, UseMutationResult } from 'react-query';
import {

View File

@@ -1,4 +1,4 @@
import { getAwsServices } from 'api/integrations/aws';
import { getAwsServices } from 'api/integration/aws';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { Service } from 'container/CloudIntegrationPage/ServicesSection/types';
import { useQuery, UseQueryResult } from 'react-query';

View File

@@ -1,4 +1,4 @@
import { getServiceDetails } from 'api/integrations/aws';
import { getServiceDetails } from 'api/integration/aws';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { ServiceData } from 'container/CloudIntegrationPage/ServicesSection/types';
import { useQuery, UseQueryResult } from 'react-query';

View File

@@ -1,4 +1,4 @@
import { updateAccountConfig } from 'api/integrations/aws';
import { updateAccountConfig } from 'api/integration/aws';
import { useMutation, UseMutationResult } from 'react-query';
import {
AccountConfigPayload,

View File

@@ -1,4 +1,4 @@
import { updateServiceConfig } from 'api/integrations/aws';
import { updateServiceConfig } from 'api/integration/aws';
import { useMutation, UseMutationResult } from 'react-query';
interface UpdateServiceConfigPayload {

View File

@@ -1,5 +1,6 @@
import { Color } from '@signozhq/design-tokens';
import { Card } from 'antd';
import logEvent from 'api/common/logEvent';
import { useGetGraphCustomSeries } from 'components/CeleryTask/useGetGraphCustomSeries';
import { useNavigateToTraces } from 'components/CeleryTask/useNavigateToTraces';
import { QueryParams } from 'constants/query';
@@ -9,7 +10,7 @@ import { Button } from 'container/MetricsApplication/Tabs/styles';
import { onGraphClickHandler } from 'container/MetricsApplication/Tabs/util';
import useUrlQuery from 'hooks/useUrlQuery';
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory, useLocation } from 'react-router-dom';
import { UpdateTimeInterval } from 'store/actions';
@@ -123,6 +124,26 @@ export default function OverviewRightPanelGraph({
},
});
const [requestRateStatus, setRequestRateStatus] = useState<boolean | null>(
null,
);
const [errorRateStatus, setErrorRateStatus] = useState<boolean | null>(null);
const [avgLatencyStatus, setAvgLatencyStatus] = useState<boolean | null>(null);
useEffect(() => {
if (
requestRateStatus !== null &&
errorRateStatus !== null &&
avgLatencyStatus !== null
) {
logEvent('MQ Overview Page: Right Drawer - graphs', {
requestRate: requestRateStatus,
errorRate: errorRateStatus,
avgLatency: avgLatencyStatus,
});
}
}, [requestRateStatus, errorRateStatus, avgLatencyStatus]);
return (
<Card className="overview-right-panel-graph-card">
<div className="request-rate-card">
@@ -130,7 +151,9 @@ export default function OverviewRightPanelGraph({
type="default"
size="small"
id="Celery_request_rate_button"
onClick={(): void => goToTraces(requestRateWidget)}
onClick={(): void => {
goToTraces(requestRateWidget);
}}
>
View Traces
</Button>
@@ -140,6 +163,9 @@ export default function OverviewRightPanelGraph({
onDragSelect={onDragSelect}
onClickHandler={handleGraphClick('Celery_request_rate')}
customSeries={getCustomSeries}
dataAvailable={(isDataAvailable: boolean): void => {
setRequestRateStatus(isDataAvailable);
}}
/>
</div>
<div className="error-rate-card">
@@ -157,6 +183,9 @@ export default function OverviewRightPanelGraph({
onDragSelect={onDragSelect}
onClickHandler={handleGraphClick('Celery_error_rate')}
customSeries={getCustomSeries}
dataAvailable={(isDataAvailable: boolean): void => {
setErrorRateStatus(isDataAvailable);
}}
/>
</div>
<div className="avg-latency-card">
@@ -173,6 +202,9 @@ export default function OverviewRightPanelGraph({
headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect}
onClickHandler={handleGraphClick('Celery_avg_latency')}
dataAvailable={(isDataAvailable: boolean): void => {
setAvgLatencyStatus(isDataAvailable);
}}
/>
</div>
</Card>

View File

@@ -2,6 +2,7 @@ import './ValueInfo.styles.scss';
import { FileSearchOutlined } from '@ant-design/icons';
import { Button, Card, Col, Row } from 'antd';
import logEvent from 'api/common/logEvent';
import { useNavigateToTraces } from 'components/CeleryTask/useNavigateToTraces';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { PANEL_TYPES } from 'constants/queryBuilder';
@@ -113,6 +114,9 @@ export default function ValueInfo({
return 'red';
};
const mqAnalyticsTitle =
'MQ Overview Page: Right drawer navigation to trace page';
return (
<Card className="value-info-card">
<Row gutter={16}>
@@ -133,7 +137,15 @@ export default function ValueInfo({
icon={<FileSearchOutlined />}
className="trace-button"
disabled={isLoading}
onClick={(): void => navigateToTrace(filters ?? [])}
onClick={(): void => {
logEvent(mqAnalyticsTitle, {
filters,
minTime,
maxTime,
source: 'request rate',
});
navigateToTrace(filters ?? []);
}}
>
View Traces
</Button>
@@ -155,7 +167,13 @@ export default function ValueInfo({
icon={<FileSearchOutlined />}
className="trace-button"
disabled={isLoading}
onClick={(): void =>
onClick={(): void => {
logEvent(mqAnalyticsTitle, {
filters,
minTime,
maxTime,
source: 'error rate',
});
navigateToTrace([
...(filters ?? []),
{
@@ -171,8 +189,8 @@ export default function ValueInfo({
op: '=',
value: 'true',
},
])
}
]);
}}
>
View Traces
</Button>
@@ -194,7 +212,15 @@ export default function ValueInfo({
icon={<FileSearchOutlined />}
className="trace-button"
disabled={isLoading}
onClick={(): void => navigateToTrace(filters ?? [])}
onClick={(): void => {
logEvent(mqAnalyticsTitle, {
filters,
minTime,
maxTime,
source: 'average latency',
});
navigateToTrace(filters ?? []);
}}
>
View Traces
</Button>

View File

@@ -1,15 +1,30 @@
import './CeleryTask.styles.scss';
import logEvent from 'api/common/logEvent';
import CeleryTaskConfigOptions from 'components/CeleryTask/CeleryTaskConfigOptions/CeleryTaskConfigOptions';
import CeleryTaskDetail, {
CaptureDataProps,
} from 'components/CeleryTask/CeleryTaskDetail/CeleryTaskDetail';
import CeleryTaskGraphGrid from 'components/CeleryTask/CeleryTaskGraph/CeleryTaskGraphGrid';
import { QueryParams } from 'constants/query';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { useState } from 'react';
import useUrlQuery from 'hooks/useUrlQuery';
import { useEffect, useRef, useState } from 'react';
export default function CeleryTask(): JSX.Element {
const [task, setTask] = useState<CaptureDataProps | null>(null);
const loggedRef = useRef(false);
const taskName = useUrlQuery().get(QueryParams.taskName);
useEffect(() => {
if (taskName && !loggedRef.current) {
logEvent('MQ Celery: Task name filter', {
taskName,
});
loggedRef.current = true;
}
}, [taskName]);
const onTaskClick = (captureData: CaptureDataProps): void => {
setTask(captureData);

View File

@@ -94,10 +94,7 @@ function LogsExplorer(): JSX.Element {
if (!data) return null;
try {
const parsed = JSON.parse(data);
const hasValidColumns = Array.isArray(parsed?.selectColumns);
return hasValidColumns ? parsed.selectColumns : null;
return JSON.parse(data);
} catch {
return null;
}
@@ -132,16 +129,11 @@ function LogsExplorer(): JSX.Element {
// Skip if already migrated
if (query.version) return query;
// Case 1: query has columns
if (query.selectColumns.length > 0) {
return {
...query,
version: 1,
selectColumns: mergeWithRequiredColumns(query.selectColumns),
};
if (logListOptionsFromLocalStorage?.version) {
return logListOptionsFromLocalStorage;
}
// Case 2: No query columns in but we have localStorage columns
// Case 1: we have localStorage columns
if (logListOptionsFromLocalStorage?.selectColumns?.length > 0) {
return {
...query,
@@ -152,6 +144,15 @@ function LogsExplorer(): JSX.Element {
};
}
// Case 2: No query columns in localStorage in but query has columns
if (query.selectColumns.length > 0) {
return {
...query,
version: 1,
selectColumns: mergeWithRequiredColumns(query.selectColumns),
};
}
// Case 3: No columns anywhere, use defaults
return {
...query,
@@ -159,7 +160,7 @@ function LogsExplorer(): JSX.Element {
selectColumns: defaultLogsSelectedColumns,
};
},
[mergeWithRequiredColumns, logListOptionsFromLocalStorage?.selectColumns],
[mergeWithRequiredColumns, logListOptionsFromLocalStorage],
);
useEffect(() => {

View File

@@ -2,6 +2,7 @@
import '../MQDetails.style.scss';
import { Table, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import { MessagingQueueServicePayload } from 'api/messagingQueues/getConsumerLagDetails';
import { getKafkaSpanEval } from 'api/messagingQueues/getKafkaSpanEval';
import axios from 'axios';
@@ -15,7 +16,7 @@ import {
MessagingQueuesViewType,
RowData,
} from 'pages/MessagingQueues/MessagingQueuesUtils';
import { useEffect, useMemo, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useMutation } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
@@ -93,6 +94,9 @@ export function getColumns(
className="traceid-text"
onClick={(): void => {
window.open(`${ROUTES.TRACE}/${item}`, '_blank');
logEvent(`MQ Kafka: Drop Rate - traceid navigation`, {
item,
});
}}
>
{item}
@@ -227,6 +231,22 @@ function DropRateView(): JSX.Element {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [minTime, maxTime, evaluationTime]);
const prevTableDataRef = useRef<string>();
useEffect(() => {
if (tableData.length > 0) {
const currentTableData = JSON.stringify(tableData);
if (currentTableData !== prevTableDataRef.current) {
logEvent(`MQ Kafka: Drop Rate View`, {
dataRender: tableData.length,
});
prevTableDataRef.current = currentTableData;
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify(tableData)]);
return (
<div className={cx('mq-overview-container', 'droprate-view')}>
<div className="mq-overview-title">

View File

@@ -3,6 +3,7 @@
import './MQTables.styles.scss';
import { Skeleton, Table, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import {
MessagingQueueServicePayload,
MessagingQueuesPayloadProps,
@@ -23,11 +24,12 @@ import {
MessagingQueueServiceDetailType,
MessagingQueuesViewType,
MessagingQueuesViewTypeOptions,
ProducerLatencyOptions,
RowData,
SelectedTimelineQuery,
setConfigDetail,
} from 'pages/MessagingQueues/MessagingQueuesUtils';
import { useEffect, useMemo, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useMutation } from 'react-query';
import { useHistory, useLocation } from 'react-router-dom';
import { ErrorResponse, SuccessResponse } from 'types/api';
@@ -130,6 +132,7 @@ function MessagingQueuesTable({
tableApi,
validConfigPresent = false,
type = 'Detail',
option = ProducerLatencyOptions.Producers,
}: {
currentTab?: MessagingQueueServiceDetailType;
selectedView: MessagingQueuesViewTypeOptions;
@@ -141,6 +144,7 @@ function MessagingQueuesTable({
>;
validConfigPresent?: boolean;
type?: 'Detail' | 'Overview';
option?: ProducerLatencyOptions;
}): JSX.Element {
const [columns, setColumns] = useState<any[]>([]);
const [tableData, setTableData] = useState<any[]>([]);
@@ -262,6 +266,43 @@ function MessagingQueuesTable({
configDetailQueryData?.topic || ''
} ${configDetailQueryData?.partition || ''}`;
const prevTableDataRef = useRef<string>();
useEffect(() => {
if (tableData.length > 0 && type === 'Overview') {
const currentTableData = JSON.stringify(tableData);
if (currentTableData !== prevTableDataRef.current) {
logEvent(`MQ Kafka: ${MessagingQueuesViewType[selectedView].label}`, {
dataRender: tableData.length,
});
prevTableDataRef.current = currentTableData;
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [option, JSON.stringify(tableData), selectedView]);
useEffect(() => {
if (tableData.length > 0 && type === 'Detail') {
const currentTableData = JSON.stringify(tableData);
if (currentTableData !== prevTableDataRef.current) {
logEvent(
`MQ Kafka: ${MessagingQueuesViewType[selectedView].label} - details`,
{
dataRender: tableData.length,
activeTab: currentTab,
topic: configDetailQueryData?.topic,
partition: configDetailQueryData?.partition,
serviceName: configDetailQueryData?.service_name,
},
);
prevTableDataRef.current = currentTableData;
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentTab, JSON.stringify(tableData), selectedView]);
return (
<div className="mq-tables-container">
{!validConfigPresent ? (

View File

@@ -112,6 +112,7 @@ function MessagingQueueOverview({
tableApi={getTableApi(selectedView)}
validConfigPresent
type="Overview"
option={option}
/>
</div>
);

View File

@@ -26,12 +26,14 @@ interface MetricSectionProps {
title: string;
description: string;
graphCount: Widgets[];
checkIfDataExists?: (isDataAvailable: boolean) => void;
}
function MetricSection({
title,
description,
graphCount,
checkIfDataExists,
}: MetricSectionProps): JSX.Element {
const isDarkMode = useIsDarkMode();
@@ -50,6 +52,7 @@ function MetricSection({
<MetricPageGridGraph
key={`graph-${widgetData.id}`}
widgetData={widgetData}
checkIfDataExists={checkIfDataExists}
/>
))}
</div>
@@ -57,7 +60,15 @@ function MetricSection({
);
}
function MetricColumnGraphs(): JSX.Element {
MetricSection.defaultProps = {
checkIfDataExists: (): void => {},
};
function MetricColumnGraphs({
checkIfDataExists,
}: {
checkIfDataExists: (isDataAvailable: boolean) => void;
}): JSX.Element {
const { t } = useTranslation('messagingQueues');
const metricsData = [
@@ -106,6 +117,7 @@ function MetricColumnGraphs(): JSX.Element {
title={metric.title}
description={metric.description}
graphCount={metric?.graphCount || []}
checkIfDataExists={checkIfDataExists}
/>
))}
</div>

View File

@@ -1,11 +1,12 @@
import './MetricPage.styles.scss';
import { Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import { CardContainer } from 'container/GridCardLayout/styles';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { ChevronDown, ChevronUp } from 'lucide-react';
import { useState } from 'react';
import { useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Widgets } from 'types/api/dashboard/getAll';
@@ -28,6 +29,7 @@ interface CollapsibleMetricSectionProps {
graphCount: Widgets[];
isCollapsed: boolean;
onToggle: () => void;
checkIfDataExists?: (isDataAvailable: boolean) => void;
}
function CollapsibleMetricSection({
@@ -36,6 +38,7 @@ function CollapsibleMetricSection({
graphCount,
isCollapsed,
onToggle,
checkIfDataExists,
}: CollapsibleMetricSectionProps): JSX.Element {
const isDarkMode = useIsDarkMode();
@@ -63,6 +66,7 @@ function CollapsibleMetricSection({
<MetricPageGridGraph
key={`graph-${widgetData.id}`}
widgetData={widgetData}
checkIfDataExists={checkIfDataExists}
/>
))}
</div>
@@ -72,6 +76,10 @@ function CollapsibleMetricSection({
);
}
CollapsibleMetricSection.defaultProps = {
checkIfDataExists: undefined,
};
function MetricPage(): JSX.Element {
const [collapsedSections, setCollapsedSections] = useState<{
[key: string]: boolean;
@@ -114,9 +122,27 @@ function MetricPage(): JSX.Element {
},
];
const [renderedGraphCount, setRenderedGraphCount] = useState(0);
const hasLoggedRef = useRef(false);
const checkIfDataExists = (isDataAvailable: boolean): void => {
if (isDataAvailable) {
const newCount = renderedGraphCount + 1;
setRenderedGraphCount(newCount);
// Only log when first graph has rendered and we haven't logged yet
if (newCount === 1 && !hasLoggedRef.current) {
logEvent('MQ Kafka: Metric view', {
graphRendered: true,
});
hasLoggedRef.current = true;
}
}
};
return (
<div className="metric-page">
<MetricColumnGraphs />
<MetricColumnGraphs checkIfDataExists={checkIfDataExists} />
{metricSections.map(({ key, title, description, graphCount }) => (
<CollapsibleMetricSection
key={key}
@@ -125,6 +151,7 @@ function MetricPage(): JSX.Element {
graphCount={graphCount}
isCollapsed={collapsedSections[key]}
onToggle={(): void => toggleCollapse(key)}
checkIfDataExists={checkIfDataExists}
/>
))}
</div>

View File

@@ -15,8 +15,10 @@ import { Widgets } from 'types/api/dashboard/getAll';
function MetricPageGridGraph({
widgetData,
checkIfDataExists,
}: {
widgetData: Widgets;
checkIfDataExists?: (isDataAvailable: boolean) => void;
}): JSX.Element {
const history = useHistory();
const { pathname } = useLocation();
@@ -51,9 +53,14 @@ function MetricPageGridGraph({
widget={widgetData}
headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect}
dataAvailable={checkIfDataExists}
/>
</Card>
);
}
MetricPageGridGraph.defaultProps = {
checkIfDataExists: (): void => {},
};
export default MetricPageGridGraph;

7
go.mod
View File

@@ -13,17 +13,17 @@ require (
github.com/SigNoz/zap_otlp/zap_otlp_encoder v0.0.0-20230822164844-1b861a431974
github.com/SigNoz/zap_otlp/zap_otlp_sync v0.0.0-20230822164844-1b861a431974
github.com/antonmedv/expr v1.15.3
github.com/auth0/go-jwt-middleware v1.0.1
github.com/cespare/xxhash/v2 v2.3.0
github.com/coreos/go-oidc/v3 v3.11.0
github.com/dustin/go-humanize v1.0.1
github.com/go-co-op/gocron v1.30.1
github.com/go-kit/log v0.2.1
github.com/go-openapi/runtime v0.28.0
github.com/go-openapi/strfmt v0.23.0
github.com/go-redis/redis/v8 v8.11.5
github.com/go-redis/redismock/v8 v8.11.5
github.com/go-viper/mapstructure/v2 v2.1.0
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.6.0
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.1
@@ -111,7 +111,6 @@ require (
github.com/expr-lang/expr v1.16.9 // indirect
github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/form3tech-oss/jwt-go v3.2.5+incompatible // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-faster/city v1.0.1 // indirect
github.com/go-faster/errors v0.7.1 // indirect
@@ -126,13 +125,11 @@ require (
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/loads v0.22.0 // indirect
github.com/go-openapi/spec v0.21.0 // indirect
github.com/go-openapi/strfmt v0.23.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-openapi/validate v0.24.0 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/gofrs/uuid v4.4.0+incompatible // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect

16
go.sum
View File

@@ -124,8 +124,6 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/auth0/go-jwt-middleware v1.0.1 h1:/fsQ4vRr4zod1wKReUH+0A3ySRjGiT9G34kypO/EKwI=
github.com/auth0/go-jwt-middleware v1.0.1/go.mod h1:YSeUX3z6+TF2H+7padiEqNJ73Zy9vXW72U//IgN0BIM=
github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU=
github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
@@ -246,9 +244,6 @@ github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
github.com/form3tech-oss/jwt-go v3.2.5+incompatible h1:/l4kBbb4/vGSsdtB5nUe8L7B9mImVMaBPw9L/0TBHU8=
github.com/form3tech-oss/jwt-go v3.2.5+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
@@ -337,8 +332,6 @@ github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRx
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@@ -444,13 +437,10 @@ github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/gophercloud/gophercloud v1.14.0 h1:Bt9zQDhPrbd4qX7EILGmy+i7GP35cc+AAL2+wIJpUE8=
github.com/gophercloud/gophercloud v1.14.0/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
@@ -864,9 +854,6 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY=
github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY=
github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60=
github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js=
@@ -920,8 +907,6 @@ github.com/uptrace/bun/dialect/pgdialect v1.2.9 h1:caf5uFbOGiXvadV6pA5gn87k0awFF
github.com/uptrace/bun/dialect/pgdialect v1.2.9/go.mod h1:m7L9JtOp/Lt8HccET70ULxplMweE/u0S9lNUSxz2duo=
github.com/uptrace/bun/dialect/sqlitedialect v1.2.9 h1:HLzGWXBh07sT8zhVPy6veYbbGrAtYq0KzyRHXBj+GjA=
github.com/uptrace/bun/dialect/sqlitedialect v1.2.9/go.mod h1:dUR+ecoCWA0FIa9vhQVRnGtYYPpuCLJoEEtX9E1aiBU=
github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc=
github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
github.com/vjeantet/grok v1.0.1 h1:2rhIR7J4gThTgcZ1m2JY4TrJZNgjn985U28kT2wQrJ4=
@@ -1366,7 +1351,6 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=

View File

@@ -0,0 +1,25 @@
package alertmanager
import (
"context"
"go.signoz.io/signoz/pkg/errors"
"go.signoz.io/signoz/pkg/factory"
"go.signoz.io/signoz/pkg/types/alertmanagertypes"
)
var (
ErrCodeAlertmanagerNotFound = errors.MustNewCode("alertmanager_not_found")
)
type Alertmanager interface {
factory.Service
// GetAlerts gets the alerts from the alertmanager per organization.
GetAlerts(context.Context, string, alertmanagertypes.GettableAlertsParams) (alertmanagertypes.GettableAlerts, error)
// PutAlerts puts the alerts into the alertmanager per organization.
PutAlerts(context.Context, string, alertmanagertypes.PostableAlerts) error
// TestReceiver sends a test alert to a receiver.
TestReceiver(context.Context, string, alertmanagertypes.Receiver) error
}

View File

@@ -0,0 +1,100 @@
package sqlalertmanagerstore
import (
"context"
"database/sql"
"go.signoz.io/signoz/pkg/errors"
"go.signoz.io/signoz/pkg/sqlstore"
"go.signoz.io/signoz/pkg/types/alertmanagertypes"
)
type config struct {
sqlstore sqlstore.SQLStore
}
func NewConfigStore(sqlstore sqlstore.SQLStore) alertmanagertypes.ConfigStore {
return &config{sqlstore: sqlstore}
}
// Get implements alertmanagertypes.ConfigStore.
func (store *config) Get(ctx context.Context, orgID string) (*alertmanagertypes.Config, error) {
storeableConfig := new(alertmanagertypes.StoreableConfig)
err := store.
sqlstore.
BunDB().
NewSelect().
Model(storeableConfig).
Where("org_id = ?", orgID).
Scan(ctx)
if err != nil {
if err == sql.ErrNoRows {
return nil, errors.Newf(errors.TypeNotFound, alertmanagertypes.ErrCodeAlertmanagerConfigNotFound, "cannot find alertmanager config for orgID %s", orgID)
}
return nil, err
}
cfg, err := alertmanagertypes.NewConfigFromStoreableConfig(storeableConfig)
if err != nil {
return nil, err
}
return cfg, nil
}
// Set implements alertmanagertypes.ConfigStore.
func (store *config) Set(ctx context.Context, config *alertmanagertypes.Config) error {
tx, err := store.sqlstore.BunDB().BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback() //nolint:errcheck
if _, err = tx.
NewInsert().
Model(config.StoreableConfig()).
On("CONFLICT (org_id) DO UPDATE").
Set("config = ?", string(config.StoreableConfig().Config)).
Set("updated_at = ?", config.StoreableConfig().UpdatedAt).
Exec(ctx); err != nil {
return err
}
channels := config.Channels()
if len(channels) != 0 {
if _, err = tx.NewInsert().
Model(&channels).
On("CONFLICT (name) DO UPDATE").
Set("data = EXCLUDED.data").
Set("updated_at = EXCLUDED.updated_at").
Exec(ctx); err != nil {
return err
}
}
if err = tx.Commit(); err != nil {
return err
}
return nil
}
func (store *config) ListOrgs(ctx context.Context) ([]string, error) {
var orgIDs []string
err := store.
sqlstore.
BunDB().
NewSelect().
Table("organizations").
ColumnExpr("id").
Scan(ctx, &orgIDs)
if err != nil {
return nil, err
}
return orgIDs, nil
}

View File

@@ -0,0 +1,69 @@
package sqlalertmanagerstore
import (
"context"
"database/sql"
"go.signoz.io/signoz/pkg/errors"
"go.signoz.io/signoz/pkg/sqlstore"
"go.signoz.io/signoz/pkg/types/alertmanagertypes"
)
type state struct {
sqlstore sqlstore.SQLStore
}
func NewStateStore(sqlstore sqlstore.SQLStore) alertmanagertypes.StateStore {
return &state{sqlstore: sqlstore}
}
// Get implements alertmanagertypes.StateStore.
func (store *state) Get(ctx context.Context, orgID string) (*alertmanagertypes.StoreableState, error) {
storeableState := new(alertmanagertypes.StoreableState)
err := store.
sqlstore.
BunDB().
NewSelect().
Model(storeableState).
Where("org_id = ?", orgID).
Scan(ctx)
if err != nil {
if err == sql.ErrNoRows {
return nil, errors.Newf(errors.TypeNotFound, alertmanagertypes.ErrCodeAlertmanagerStateNotFound, "cannot find alertmanager state for org %s", orgID)
}
return nil, err
}
return storeableState, nil
}
// Set implements alertmanagertypes.StateStore.
func (store *state) Set(ctx context.Context, orgID string, storeableState *alertmanagertypes.StoreableState) error {
tx, err := store.sqlstore.BunDB().BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback() //nolint:errcheck
_, err = tx.
NewInsert().
Model(storeableState).
On("CONFLICT (org_id) DO UPDATE").
Set("silences = EXCLUDED.silences").
Set("nflog = EXCLUDED.nflog").
Set("updated_at = EXCLUDED.updated_at").
Where("org_id = ?", orgID).
Exec(ctx)
if err != nil {
return err
}
if err := tx.Commit(); err != nil {
return err
}
return nil
}

254
pkg/alertmanager/api.go Normal file
View File

@@ -0,0 +1,254 @@
package alertmanager
import (
"context"
"io"
"net/http"
"strconv"
"time"
"github.com/gorilla/mux"
"go.signoz.io/signoz/pkg/errors"
"go.signoz.io/signoz/pkg/http/render"
"go.signoz.io/signoz/pkg/types/alertmanagertypes"
"go.signoz.io/signoz/pkg/types/authtypes"
)
type API struct {
configStore alertmanagertypes.ConfigStore
alertmanager Alertmanager
}
func NewAPI(configStore alertmanagertypes.ConfigStore, alertmanager Alertmanager) *API {
return &API{
configStore: configStore,
alertmanager: alertmanager,
}
}
func (api *API) GetAlerts(req *http.Request, rw http.ResponseWriter) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
claims, ok := authtypes.ClaimsFromContext(ctx)
if !ok {
render.Error(rw, errors.Newf(errors.TypeUnauthenticated, errors.CodeUnauthenticated, "unauthenticated"))
return
}
params, err := alertmanagertypes.NewGettableAlertsParams(req)
if err != nil {
render.Error(rw, err)
return
}
alerts, err := api.alertmanager.GetAlerts(ctx, claims.OrgID, params)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, alerts)
}
func (api *API) TestReceiver(req *http.Request, rw http.ResponseWriter) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
claims, ok := authtypes.ClaimsFromContext(ctx)
if !ok {
render.Error(rw, errors.Newf(errors.TypeUnauthenticated, errors.CodeUnauthenticated, "unauthenticated"))
return
}
body, err := io.ReadAll(req.Body)
if err != nil {
render.Error(rw, err)
return
}
defer req.Body.Close() //nolint:errcheck
receiver, err := alertmanagertypes.NewReceiver(string(body))
if err != nil {
render.Error(rw, err)
return
}
err = api.alertmanager.TestReceiver(ctx, claims.OrgID, receiver)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusNoContent, nil)
}
func (api *API) GetChannels(req *http.Request, rw http.ResponseWriter) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
claims, ok := authtypes.ClaimsFromContext(ctx)
if !ok {
render.Error(rw, errors.Newf(errors.TypeUnauthenticated, errors.CodeUnauthenticated, "unauthenticated"))
return
}
config, err := api.configStore.Get(ctx, claims.OrgID)
if err != nil {
render.Error(rw, err)
return
}
channels := config.Channels()
channelList := make([]*alertmanagertypes.Channel, 0, len(channels))
for _, channel := range channels {
channelList = append(channelList, channel)
}
render.Success(rw, http.StatusOK, channelList)
}
func (api *API) GetChannelByID(req *http.Request, rw http.ResponseWriter) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
claims, ok := authtypes.ClaimsFromContext(ctx)
if !ok {
render.Error(rw, errors.Newf(errors.TypeUnauthenticated, errors.CodeUnauthenticated, "unauthenticated"))
return
}
vars := mux.Vars(req)
if vars == nil {
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is required in path"))
return
}
idString, ok := vars["id"]
if !ok {
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is required in path"))
return
}
id, err := strconv.Atoi(idString)
if err != nil {
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is not a valid integer"))
return
}
config, err := api.configStore.Get(ctx, claims.OrgID)
if err != nil {
render.Error(rw, err)
return
}
channels := config.Channels()
channel, err := alertmanagertypes.GetChannelByID(channels, id)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, channel)
}
func (api *API) UpdateChannelByID(req *http.Request, rw http.ResponseWriter) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
claims, ok := authtypes.ClaimsFromContext(ctx)
if !ok {
render.Error(rw, errors.Newf(errors.TypeUnauthenticated, errors.CodeUnauthenticated, "unauthenticated"))
return
}
body, err := io.ReadAll(req.Body)
if err != nil {
render.Error(rw, err)
return
}
defer req.Body.Close() //nolint:errcheck
receiver, err := alertmanagertypes.NewReceiver(string(body))
if err != nil {
render.Error(rw, err)
return
}
config, err := api.configStore.Get(ctx, claims.OrgID)
if err != nil {
render.Error(rw, err)
return
}
err = config.UpdateReceiver(alertmanagertypes.NewRouteFromReceiver(receiver), receiver)
if err != nil {
render.Error(rw, err)
return
}
err = api.configStore.Set(ctx, config)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusNoContent, nil)
}
func (api *API) DeleteChannelByID(req *http.Request, rw http.ResponseWriter) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
claims, ok := authtypes.ClaimsFromContext(ctx)
if !ok {
render.Error(rw, errors.Newf(errors.TypeUnauthenticated, errors.CodeUnauthenticated, "unauthenticated"))
return
}
vars := mux.Vars(req)
if vars == nil {
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is required in path"))
return
}
idString, ok := vars["id"]
if !ok {
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is required in path"))
return
}
id, err := strconv.Atoi(idString)
if err != nil {
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is not a valid integer"))
return
}
config, err := api.configStore.Get(ctx, claims.OrgID)
if err != nil {
render.Error(rw, err)
return
}
channels := config.Channels()
channel, err := alertmanagertypes.GetChannelByID(channels, id)
if err != nil {
render.Error(rw, err)
return
}
err = config.DeleteReceiver(channel.Name)
if err != nil {
render.Error(rw, err)
return
}
err = api.configStore.Set(ctx, config)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusNoContent, nil)
}

View File

@@ -0,0 +1,42 @@
package alertmanager
import (
"time"
"go.signoz.io/signoz/pkg/alertmanager/server"
"go.signoz.io/signoz/pkg/factory"
)
type Config struct {
// Config is the config for the alertmanager server.
server.Config `mapstructure:",squash"`
// Provider is the provider for the alertmanager service.
Provider string `mapstructure:"provider"`
// Internal is the internal alertmanager configuration.
Internal Internal `mapstructure:"internal"`
}
type Internal struct {
// PollInterval is the interval at which the alertmanager is synced.
PollInterval time.Duration `mapstructure:"poll_interval"`
}
func NewConfigFactory() factory.ConfigFactory {
return factory.NewConfigFactory(factory.MustNewName("alertmanager"), newConfig)
}
func newConfig() factory.Config {
return Config{
Config: server.NewConfig(),
Provider: "internal",
Internal: Internal{
PollInterval: 15 * time.Second,
},
}
}
func (c Config) Validate() error {
return nil
}

View File

@@ -0,0 +1,70 @@
package internalalertmanager
import (
"context"
"time"
"go.signoz.io/signoz/pkg/alertmanager"
"go.signoz.io/signoz/pkg/alertmanager/alertmanagerstore/sqlalertmanagerstore"
"go.signoz.io/signoz/pkg/factory"
"go.signoz.io/signoz/pkg/sqlstore"
"go.signoz.io/signoz/pkg/types/alertmanagertypes"
)
type provider struct {
service *alertmanager.Service
config alertmanager.Config
settings factory.ScopedProviderSettings
}
func NewFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[alertmanager.Alertmanager, alertmanager.Config] {
return factory.NewProviderFactory(factory.MustNewName("internal"), func(ctx context.Context, settings factory.ProviderSettings, config alertmanager.Config) (alertmanager.Alertmanager, error) {
return New(ctx, settings, config, sqlstore)
})
}
func New(ctx context.Context, providerSettings factory.ProviderSettings, config alertmanager.Config, sqlstore sqlstore.SQLStore) (alertmanager.Alertmanager, error) {
settings := factory.NewScopedProviderSettings(providerSettings, "go.signoz.io/signoz/pkg/alertmanager/internalalertmanager")
return &provider{
service: alertmanager.New(
ctx,
settings,
config,
sqlalertmanagerstore.NewStateStore(sqlstore),
sqlalertmanagerstore.NewConfigStore(sqlstore),
),
settings: settings,
config: config,
}, nil
}
func (provider *provider) Start(ctx context.Context) error {
ticker := time.NewTicker(provider.config.Internal.PollInterval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return nil
case <-ticker.C:
if err := provider.service.SyncServers(ctx); err != nil {
provider.settings.Logger().ErrorContext(ctx, "failed to sync alertmanager servers", "error", err)
}
}
}
}
func (provider *provider) Stop(ctx context.Context) error {
return provider.service.Stop(ctx)
}
func (provider *provider) GetAlerts(ctx context.Context, orgID string, params alertmanagertypes.GettableAlertsParams) (alertmanagertypes.GettableAlerts, error) {
return provider.service.GetAlerts(ctx, orgID, params)
}
func (provider *provider) PutAlerts(ctx context.Context, orgID string, alerts alertmanagertypes.PostableAlerts) error {
return provider.service.PutAlerts(ctx, orgID, alerts)
}
func (provider *provider) TestReceiver(ctx context.Context, orgID string, receiver alertmanagertypes.Receiver) error {
return provider.service.TestReceiver(ctx, orgID, receiver)
}

View File

@@ -34,7 +34,7 @@ type Server struct {
logger *slog.Logger
// registry is the prometheus registry for the alertmanager
registry *prometheus.Registry
registry prometheus.Registerer
// srvConfig is the server config for the alertmanager
srvConfig Config
@@ -64,7 +64,7 @@ type Server struct {
stopc chan struct{}
}
func New(ctx context.Context, logger *slog.Logger, registry *prometheus.Registry, srvConfig Config, orgID string, stateStore alertmanagertypes.StateStore) (*Server, error) {
func New(ctx context.Context, logger *slog.Logger, registry prometheus.Registerer, srvConfig Config, orgID string, stateStore alertmanagertypes.StateStore) (*Server, error) {
server := &Server{
logger: logger.With("pkg", "go.signoz.io/pkg/alertmanager/server"),
registry: registry,
@@ -77,20 +77,18 @@ func New(ctx context.Context, logger *slog.Logger, registry *prometheus.Registry
server.marker = alertmanagertypes.NewMarker(server.registry)
// get silences for initial state
silencesstate, err := server.stateStore.Get(ctx, server.orgID, alertmanagertypes.SilenceStateName)
state, err := server.stateStore.Get(ctx, server.orgID)
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
return nil, err
}
// get nflog for initial state
nflogstate, err := server.stateStore.Get(ctx, server.orgID, alertmanagertypes.NFLogStateName)
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
return nil, err
silencesSnapshot := ""
if state != nil {
silencesSnapshot = state.Silences
}
// Initialize silences
server.silences, err = silence.New(silence.Options{
SnapshotReader: strings.NewReader(silencesstate),
SnapshotReader: strings.NewReader(silencesSnapshot),
Retention: srvConfig.Silences.Retention,
Limits: silence.Limits{
MaxSilences: func() int { return srvConfig.Silences.Max },
@@ -103,9 +101,14 @@ func New(ctx context.Context, logger *slog.Logger, registry *prometheus.Registry
return nil, err
}
nflogSnapshot := ""
if state != nil {
nflogSnapshot = state.NFLog
}
// Initialize notification log
server.nflog, err = nflog.New(nflog.Options{
SnapshotReader: strings.NewReader(nflogstate),
SnapshotReader: strings.NewReader(nflogSnapshot),
Retention: server.srvConfig.NFLog.Retention,
Metrics: server.registry,
Logger: server.logger,
@@ -125,7 +128,21 @@ func New(ctx context.Context, logger *slog.Logger, registry *prometheus.Registry
// Don't return here - we need to snapshot our state first.
}
return server.stateStore.Set(ctx, server.orgID, alertmanagertypes.SilenceStateName, server.silences)
state, err := server.stateStore.Get(ctx, server.orgID)
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
return 0, err
}
if state == nil {
state = alertmanagertypes.NewStoreableState(server.orgID)
}
c, err := state.Set(alertmanagertypes.SilenceStateName, server.silences)
if err != nil {
return 0, err
}
return c, server.stateStore.Set(ctx, server.orgID, state)
})
}()
@@ -140,7 +157,21 @@ func New(ctx context.Context, logger *slog.Logger, registry *prometheus.Registry
// Don't return without saving the current state.
}
return server.stateStore.Set(ctx, server.orgID, alertmanagertypes.NFLogStateName, server.nflog)
state, err := server.stateStore.Get(ctx, server.orgID)
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
return 0, err
}
if state == nil {
state = alertmanagertypes.NewStoreableState(server.orgID)
}
c, err := state.Set(alertmanagertypes.NFLogStateName, server.nflog)
if err != nil {
return 0, err
}
return c, server.stateStore.Set(ctx, server.orgID, state)
})
}()

137
pkg/alertmanager/service.go Normal file
View File

@@ -0,0 +1,137 @@
package alertmanager
import (
"context"
"sync"
"go.signoz.io/signoz/pkg/alertmanager/server"
"go.signoz.io/signoz/pkg/errors"
"go.signoz.io/signoz/pkg/factory"
"go.signoz.io/signoz/pkg/types/alertmanagertypes"
)
type Service struct {
// config is the config for the alertmanager service
config Config
// stateStore is the state store for the alertmanager service
stateStore alertmanagertypes.StateStore
// configStore is the config store for the alertmanager service
configStore alertmanagertypes.ConfigStore
// settings is the settings for the alertmanager service
settings factory.ScopedProviderSettings
// Map of organization id to alertmanager server
servers map[string]*server.Server
// Mutex to protect the servers map
serversMtx sync.RWMutex
}
func New(ctx context.Context, settings factory.ScopedProviderSettings, config Config, stateStore alertmanagertypes.StateStore, configStore alertmanagertypes.ConfigStore) *Service {
service := &Service{
config: config,
stateStore: stateStore,
configStore: configStore,
settings: settings,
servers: make(map[string]*server.Server),
serversMtx: sync.RWMutex{},
}
return service
}
func (service *Service) SyncServers(ctx context.Context) error {
orgIDs, err := service.configStore.ListOrgs(ctx)
if err != nil {
return err
}
service.serversMtx.Lock()
for _, orgID := range orgIDs {
config, err := service.getConfig(ctx, orgID)
if err != nil {
service.settings.Logger().Error("failed to get alertmanagerconfig for org", "orgID", orgID, "error", err)
continue
}
service.servers[orgID], err = server.New(ctx, service.settings.Logger(), service.settings.PrometheusRegisterer(), server.Config{}, orgID, service.stateStore)
if err != nil {
service.settings.Logger().Error("failed to create alertmanagerserver", "orgID", orgID, "error", err)
continue
}
err = service.servers[orgID].SetConfig(ctx, config)
if err != nil {
service.settings.Logger().Error("failed to set config for alertmanager server", "orgID", orgID, "error", err)
continue
}
}
service.serversMtx.Unlock()
return nil
}
func (service *Service) GetAlerts(ctx context.Context, orgID string, params alertmanagertypes.GettableAlertsParams) (alertmanagertypes.GettableAlerts, error) {
server, err := service.getServer(orgID)
if err != nil {
return nil, err
}
return server.GetAlerts(ctx, params)
}
func (service *Service) PutAlerts(ctx context.Context, orgID string, alerts alertmanagertypes.PostableAlerts) error {
server, err := service.getServer(orgID)
if err != nil {
return err
}
return server.PutAlerts(ctx, alerts)
}
func (service *Service) TestReceiver(ctx context.Context, orgID string, receiver alertmanagertypes.Receiver) error {
server, err := service.getServer(orgID)
if err != nil {
return err
}
return server.TestReceiver(ctx, receiver)
}
func (service *Service) Stop(ctx context.Context) error {
for _, server := range service.servers {
server.Stop(ctx)
}
return nil
}
func (service *Service) getConfig(ctx context.Context, orgID string) (*alertmanagertypes.Config, error) {
config, err := service.configStore.Get(ctx, orgID)
if err != nil {
if !errors.Ast(err, errors.TypeNotFound) {
return nil, err
}
config, err = alertmanagertypes.NewDefaultConfig(service.config.Global, service.config.Route, orgID)
if err != nil {
return nil, err
}
return config, err
}
return config, nil
}
func (service *Service) getServer(orgID string) (*server.Server, error) {
server, ok := service.servers[orgID]
if !ok {
return nil, errors.Newf(errors.TypeNotFound, ErrCodeAlertmanagerNotFound, "alertmanager not found for org %s", orgID)
}
return server, nil
}

View File

@@ -5,6 +5,16 @@ import (
"regexp"
)
var (
CodeInvalidInput code = code{"invalid_input"}
CodeInternal = code{"internal"}
CodeUnsupported = code{"unsupported"}
CodeNotFound = code{"not_found"}
CodeMethodNotAllowed = code{"method_not_allowed"}
CodeAlreadyExists = code{"already_exists"}
CodeUnauthenticated = code{"unauthenticated"}
)
var (
codeRegex = regexp.MustCompile(`^[a-z_]+$`)
)

View File

@@ -7,12 +7,10 @@ import (
"net/http"
"regexp"
// TODO(remove): Remove auth packages
"go.signoz.io/signoz/pkg/query-service/auth"
"github.com/gorilla/mux"
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
"go.signoz.io/signoz/pkg/query-service/telemetry"
"go.signoz.io/signoz/pkg/types/authtypes"
"go.uber.org/zap"
)
@@ -30,18 +28,16 @@ func NewAnalytics(logger *zap.Logger) *Analytics {
func (a *Analytics) Wrap(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := auth.AttachJwtToContext(r.Context(), r)
r = r.WithContext(ctx)
route := mux.CurrentRoute(r)
path, _ := route.GetPathTemplate()
queryRangeData, metadataExists := a.extractQueryRangeData(path, r)
a.getActiveLogs(path, r)
badResponseBuffer := new(bytes.Buffer)
writer := newBadResponseLoggingWriter(w, badResponseBuffer)
next.ServeHTTP(writer, r)
queryRangeData, metadataExists := a.extractQueryRangeData(path, r)
a.getActiveLogs(path, r)
data := map[string]interface{}{"path": path, "statusCode": writer.StatusCode()}
if metadataExists {
for key, value := range queryRangeData {
@@ -50,9 +46,9 @@ func (a *Analytics) Wrap(next http.Handler) http.Handler {
}
if _, ok := telemetry.EnabledPaths()[path]; ok {
userEmail, err := auth.GetEmailFromJwt(r.Context())
if err == nil {
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_PATH, data, userEmail, true, false)
claims, ok := authtypes.ClaimsFromContext(r.Context())
if ok {
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_PATH, data, claims.Email, true, false)
}
}
@@ -138,8 +134,8 @@ func (a *Analytics) extractQueryRangeData(path string, r *http.Request) (map[str
data["queryType"] = queryInfoResult.QueryType
data["panelType"] = queryInfoResult.PanelType
userEmail, err := auth.GetEmailFromJwt(r.Context())
if err == nil {
claims, ok := authtypes.ClaimsFromContext(r.Context())
if ok {
// switch case to set data["screen"] based on the referrer
switch {
case dashboardMatched:
@@ -154,7 +150,7 @@ func (a *Analytics) extractQueryRangeData(path string, r *http.Request) (map[str
data["screen"] = "unknown"
return data, true
}
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_QUERY_RANGE_API, data, userEmail, true, false)
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_QUERY_RANGE_API, data, claims.Email, true, false)
}
}
return data, true

View File

@@ -0,0 +1,44 @@
package middleware
import (
"net/http"
"go.signoz.io/signoz/pkg/types/authtypes"
"go.uber.org/zap"
)
type Auth struct {
logger *zap.Logger
jwt *authtypes.JWT
headers []string
}
func NewAuth(logger *zap.Logger, jwt *authtypes.JWT, headers []string) *Auth {
if logger == nil {
panic("cannot build auth middleware, logger is empty")
}
return &Auth{logger: logger, jwt: jwt, headers: headers}
}
func (a *Auth) Wrap(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var values []string
for _, header := range a.headers {
values = append(values, r.Header.Get(header))
}
ctx, err := a.jwt.ContextFromRequest(
r.Context(),
values...)
if err != nil {
next.ServeHTTP(w, r)
return
}
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}

View File

@@ -11,8 +11,8 @@ import (
"github.com/gorilla/mux"
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
"go.signoz.io/signoz/pkg/query-service/auth"
"go.signoz.io/signoz/pkg/query-service/common"
"go.signoz.io/signoz/pkg/types/authtypes"
"go.uber.org/zap"
)
@@ -133,7 +133,11 @@ func (middleware *Logging) getLogCommentKVs(r *http.Request) map[string]string {
client = "api"
}
email, _ := auth.GetEmailFromJwt(r.Context())
var email string
claims, ok := authtypes.ClaimsFromContext(r.Context())
if ok {
email = claims.Email
}
kvs := map[string]string{
"path": path,

View File

@@ -12,10 +12,10 @@ import (
)
type AuthMiddleware struct {
GetUserFromRequest func(r *http.Request) (*model.UserPayload, error)
GetUserFromRequest func(r context.Context) (*model.UserPayload, error)
}
func NewAuthMiddleware(f func(r *http.Request) (*model.UserPayload, error)) *AuthMiddleware {
func NewAuthMiddleware(f func(ctx context.Context) (*model.UserPayload, error)) *AuthMiddleware {
return &AuthMiddleware{
GetUserFromRequest: f,
}
@@ -29,7 +29,7 @@ func (am *AuthMiddleware) OpenAccess(f func(http.ResponseWriter, *http.Request))
func (am *AuthMiddleware) ViewAccess(f func(http.ResponseWriter, *http.Request)) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user, err := am.GetUserFromRequest(r)
user, err := am.GetUserFromRequest(r.Context())
if err != nil {
RespondError(w, &model.ApiError{
Typ: model.ErrorUnauthorized,
@@ -53,7 +53,7 @@ func (am *AuthMiddleware) ViewAccess(f func(http.ResponseWriter, *http.Request))
func (am *AuthMiddleware) EditAccess(f func(http.ResponseWriter, *http.Request)) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user, err := am.GetUserFromRequest(r)
user, err := am.GetUserFromRequest(r.Context())
if err != nil {
RespondError(w, &model.ApiError{
Typ: model.ErrorUnauthorized,
@@ -76,7 +76,7 @@ func (am *AuthMiddleware) EditAccess(f func(http.ResponseWriter, *http.Request))
func (am *AuthMiddleware) SelfAccess(f func(http.ResponseWriter, *http.Request)) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user, err := am.GetUserFromRequest(r)
user, err := am.GetUserFromRequest(r.Context())
if err != nil {
RespondError(w, &model.ApiError{
Typ: model.ErrorUnauthorized,
@@ -100,7 +100,7 @@ func (am *AuthMiddleware) SelfAccess(f func(http.ResponseWriter, *http.Request))
func (am *AuthMiddleware) AdminAccess(f func(http.ResponseWriter, *http.Request)) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user, err := am.GetUserFromRequest(r)
user, err := am.GetUserFromRequest(r.Context())
if err != nil {
RespondError(w, &model.ApiError{
Typ: model.ErrorUnauthorized,

View File

@@ -34,6 +34,7 @@ import (
"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
"github.com/jmoiron/sqlx"
"go.signoz.io/signoz/pkg/cache"
"go.signoz.io/signoz/pkg/types/authtypes"
promModel "github.com/prometheus/common/model"
"go.uber.org/zap"
@@ -43,7 +44,6 @@ import (
"go.signoz.io/signoz/pkg/query-service/app/resource"
"go.signoz.io/signoz/pkg/query-service/app/services"
"go.signoz.io/signoz/pkg/query-service/app/traces/tracedetail"
"go.signoz.io/signoz/pkg/query-service/auth"
"go.signoz.io/signoz/pkg/query-service/common"
"go.signoz.io/signoz/pkg/query-service/constants"
chErrors "go.signoz.io/signoz/pkg/query-service/errors"
@@ -1164,23 +1164,23 @@ func (r *ClickHouseReader) SearchTracesV2(ctx context.Context, params *model.Sea
if traceSummary.NumSpans > uint64(params.MaxSpansInTrace) {
zap.L().Error("Max spans allowed in a trace limit reached", zap.Int("MaxSpansInTrace", params.MaxSpansInTrace),
zap.Uint64("Count", traceSummary.NumSpans))
userEmail, err := auth.GetEmailFromJwt(ctx)
if err == nil {
claims, ok := authtypes.ClaimsFromContext(ctx)
if ok {
data := map[string]interface{}{
"traceSize": traceSummary.NumSpans,
"maxSpansInTraceLimit": params.MaxSpansInTrace,
}
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_MAX_SPANS_ALLOWED_LIMIT_REACHED, data, userEmail, true, false)
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_MAX_SPANS_ALLOWED_LIMIT_REACHED, data, claims.Email, true, false)
}
return nil, fmt.Errorf("max spans allowed in trace limit reached, please contact support for more details")
}
userEmail, err := auth.GetEmailFromJwt(ctx)
if err == nil {
claims, ok := authtypes.ClaimsFromContext(ctx)
if ok {
data := map[string]interface{}{
"traceSize": traceSummary.NumSpans,
}
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_TRACE_DETAIL_API, data, userEmail, true, false)
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_TRACE_DETAIL_API, data, claims.Email, true, false)
}
var startTime, endTime, durationNano uint64
@@ -1266,13 +1266,13 @@ func (r *ClickHouseReader) SearchTracesV2(ctx context.Context, params *model.Sea
}
end = time.Now()
zap.L().Debug("smartTraceAlgo took: ", zap.Duration("duration", end.Sub(start)))
userEmail, err := auth.GetEmailFromJwt(ctx)
if err == nil {
claims, ok := authtypes.ClaimsFromContext(ctx)
if ok {
data := map[string]interface{}{
"traceSize": len(searchScanResponses),
"spansRenderLimit": params.SpansRenderLimit,
}
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_LARGE_TRACE_OPENED, data, userEmail, true, false)
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_LARGE_TRACE_OPENED, data, claims.Email, true, false)
}
} else {
for i, item := range searchSpanResponses {
@@ -1306,23 +1306,23 @@ func (r *ClickHouseReader) SearchTraces(ctx context.Context, params *model.Searc
if countSpans > uint64(params.MaxSpansInTrace) {
zap.L().Error("Max spans allowed in a trace limit reached", zap.Int("MaxSpansInTrace", params.MaxSpansInTrace),
zap.Uint64("Count", countSpans))
userEmail, err := auth.GetEmailFromJwt(ctx)
if err == nil {
claims, ok := authtypes.ClaimsFromContext(ctx)
if ok {
data := map[string]interface{}{
"traceSize": countSpans,
"maxSpansInTraceLimit": params.MaxSpansInTrace,
}
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_MAX_SPANS_ALLOWED_LIMIT_REACHED, data, userEmail, true, false)
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_MAX_SPANS_ALLOWED_LIMIT_REACHED, data, claims.Email, true, false)
}
return nil, fmt.Errorf("max spans allowed in trace limit reached, please contact support for more details")
}
userEmail, err := auth.GetEmailFromJwt(ctx)
if err == nil {
claims, ok := authtypes.ClaimsFromContext(ctx)
if ok {
data := map[string]interface{}{
"traceSize": countSpans,
}
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_TRACE_DETAIL_API, data, userEmail, true, false)
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_TRACE_DETAIL_API, data, claims.Email, true, false)
}
var startTime, endTime, durationNano uint64
@@ -1379,13 +1379,13 @@ func (r *ClickHouseReader) SearchTraces(ctx context.Context, params *model.Searc
}
end = time.Now()
zap.L().Debug("smartTraceAlgo took: ", zap.Duration("duration", end.Sub(start)))
userEmail, err := auth.GetEmailFromJwt(ctx)
if err == nil {
claims, ok := authtypes.ClaimsFromContext(ctx)
if ok {
data := map[string]interface{}{
"traceSize": len(searchScanResponses),
"spansRenderLimit": params.SpansRenderLimit,
}
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_LARGE_TRACE_OPENED, data, userEmail, true, false)
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_LARGE_TRACE_OPENED, data, claims.Email, true, false)
}
} else {
for i, item := range searchSpanResponses {
@@ -1455,7 +1455,7 @@ func (r *ClickHouseReader) GetWaterfallSpansForTraceWithMetadata(ctx context.Con
var serviceNameIntervalMap = map[string][]tracedetail.Interval{}
var hasMissingSpans bool
userEmail , emailErr := auth.GetEmailFromJwt(ctx)
claims, claimsPresent := authtypes.ClaimsFromContext(ctx)
cachedTraceData, err := r.GetWaterfallSpansForTraceWithMetadataCache(ctx, traceID)
if err == nil {
startTime = cachedTraceData.StartTime
@@ -1468,8 +1468,8 @@ func (r *ClickHouseReader) GetWaterfallSpansForTraceWithMetadata(ctx context.Con
totalErrorSpans = cachedTraceData.TotalErrorSpans
hasMissingSpans = cachedTraceData.HasMissingSpans
if emailErr == nil {
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_TRACE_DETAIL_API, map[string]interface{}{"traceSize": totalSpans}, userEmail, true, false)
if claimsPresent {
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_TRACE_DETAIL_API, map[string]interface{}{"traceSize": totalSpans}, claims.Email, true, false)
}
}
@@ -1485,8 +1485,8 @@ func (r *ClickHouseReader) GetWaterfallSpansForTraceWithMetadata(ctx context.Con
}
totalSpans = uint64(len(searchScanResponses))
if emailErr == nil {
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_TRACE_DETAIL_API, map[string]interface{}{"traceSize": totalSpans}, userEmail, true, false)
if claimsPresent {
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_TRACE_DETAIL_API, map[string]interface{}{"traceSize": totalSpans}, claims.Email, true, false)
}
processingBeforeCache := time.Now()
@@ -1531,8 +1531,8 @@ func (r *ClickHouseReader) GetWaterfallSpansForTraceWithMetadata(ctx context.Con
if startTime == 0 || startTimeUnixNano < startTime {
startTime = startTimeUnixNano
}
if endTime == 0 || (startTimeUnixNano + jsonItem.DurationNano ) > endTime {
endTime = (startTimeUnixNano + jsonItem.DurationNano )
if endTime == 0 || (startTimeUnixNano+jsonItem.DurationNano) > endTime {
endTime = (startTimeUnixNano + jsonItem.DurationNano)
}
if durationNano == 0 || jsonItem.DurationNano > durationNano {
durationNano = jsonItem.DurationNano
@@ -1709,12 +1709,12 @@ func (r *ClickHouseReader) GetFlamegraphSpansForTrace(ctx context.Context, trace
}
// metadata calculation
startTimeUnixNano := uint64(item.TimeUnixNano.UnixNano())
startTimeUnixNano := uint64(item.TimeUnixNano.UnixNano())
if startTime == 0 || startTimeUnixNano < startTime {
startTime = startTimeUnixNano
}
if endTime == 0 || ( startTimeUnixNano + jsonItem.DurationNano ) > endTime {
endTime = (startTimeUnixNano + jsonItem.DurationNano )
if endTime == 0 || (startTimeUnixNano+jsonItem.DurationNano) > endTime {
endTime = (startTimeUnixNano + jsonItem.DurationNano)
}
if durationNano == 0 || jsonItem.DurationNano > durationNano {
durationNano = jsonItem.DurationNano
@@ -1778,7 +1778,7 @@ func (r *ClickHouseReader) GetFlamegraphSpansForTrace(ctx context.Context, trace
trace.Spans = selectedSpansForRequest
trace.StartTimestampMillis = startTime / 1000000
trace.EndTimestampMillis = endTime / 1000000
trace.EndTimestampMillis = endTime / 1000000
return trace, nil
}
@@ -3464,9 +3464,9 @@ func (r *ClickHouseReader) GetLogs(ctx context.Context, params *model.LogsFilter
"lenFilters": lenFilters,
}
if lenFilters != 0 {
userEmail, err := auth.GetEmailFromJwt(ctx)
if err == nil {
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_LOGS_FILTERS, data, userEmail, true, false)
claims, ok := authtypes.ClaimsFromContext(ctx)
if ok {
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_LOGS_FILTERS, data, claims.Email, true, false)
}
}
@@ -3506,9 +3506,9 @@ func (r *ClickHouseReader) TailLogs(ctx context.Context, client *model.LogsTailC
"lenFilters": lenFilters,
}
if lenFilters != 0 {
userEmail, err := auth.GetEmailFromJwt(ctx)
if err == nil {
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_LOGS_FILTERS, data, userEmail, true, false)
claims, ok := authtypes.ClaimsFromContext(ctx)
if ok {
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_LOGS_FILTERS, data, claims.Email, true, false)
}
}
@@ -3598,9 +3598,9 @@ func (r *ClickHouseReader) AggregateLogs(ctx context.Context, params *model.Logs
"lenFilters": lenFilters,
}
if lenFilters != 0 {
userEmail, err := auth.GetEmailFromJwt(ctx)
if err == nil {
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_LOGS_FILTERS, data, userEmail, true, false)
claims, ok := authtypes.ClaimsFromContext(ctx)
if ok {
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_LOGS_FILTERS, data, claims.Email, true, false)
}
}

View File

@@ -8,9 +8,9 @@ import (
"strings"
"time"
"github.com/jmoiron/sqlx"
"go.signoz.io/signoz/pkg/query-service/app/dashboards"
"go.signoz.io/signoz/pkg/query-service/model"
"go.signoz.io/signoz/pkg/sqlstore"
"golang.org/x/exp/maps"
)
@@ -30,15 +30,15 @@ type Controller struct {
serviceConfigRepo serviceConfigRepository
}
func NewController(db *sqlx.DB) (
func NewController(sqlStore sqlstore.SQLStore) (
*Controller, error,
) {
accountsRepo, err := newCloudProviderAccountsRepository(db)
accountsRepo, err := newCloudProviderAccountsRepository(sqlStore.SQLxDB())
if err != nil {
return nil, fmt.Errorf("couldn't create cloud provider accounts repo: %w", err)
}
serviceConfigRepo, err := newServiceConfigRepository(db)
serviceConfigRepo, err := newServiceConfigRepository(sqlStore.SQLxDB())
if err != nil {
return nil, fmt.Errorf("couldn't create cloud provider service config repo: %w", err)
}

View File

@@ -12,8 +12,8 @@ import (
func TestRegenerateConnectionUrlWithUpdatedConfig(t *testing.T) {
require := require.New(t)
testDB, _ := utils.NewTestSqliteDB(t)
controller, err := NewController(testDB)
sqlStore, _ := utils.NewTestSqliteDB(t)
controller, err := NewController(sqlStore)
require.NoError(err)
// should be able to generate connection url for
@@ -56,8 +56,8 @@ func TestRegenerateConnectionUrlWithUpdatedConfig(t *testing.T) {
func TestAgentCheckIns(t *testing.T) {
require := require.New(t)
testDB, _ := utils.NewTestSqliteDB(t)
controller, err := NewController(testDB)
sqlStore, _ := utils.NewTestSqliteDB(t)
controller, err := NewController(sqlStore)
require.NoError(err)
// An agent should be able to check in from a cloud account even
@@ -139,8 +139,8 @@ func TestAgentCheckIns(t *testing.T) {
func TestCantDisconnectNonExistentAccount(t *testing.T) {
require := require.New(t)
testDB, _ := utils.NewTestSqliteDB(t)
controller, err := NewController(testDB)
sqlStore, _ := utils.NewTestSqliteDB(t)
controller, err := NewController(sqlStore)
require.NoError(err)
// Attempting to disconnect a non-existent account should return error
@@ -154,8 +154,8 @@ func TestCantDisconnectNonExistentAccount(t *testing.T) {
func TestConfigureService(t *testing.T) {
require := require.New(t)
testDB, _ := utils.NewTestSqliteDB(t)
controller, err := NewController(testDB)
sqlStore, _ := utils.NewTestSqliteDB(t)
controller, err := NewController(sqlStore)
require.NoError(err)
testCloudAccountId := "546311234"

View File

@@ -10,10 +10,10 @@ import (
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
"go.signoz.io/signoz/pkg/query-service/auth"
"go.signoz.io/signoz/pkg/query-service/model"
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
"go.signoz.io/signoz/pkg/query-service/telemetry"
"go.signoz.io/signoz/pkg/types/authtypes"
"go.uber.org/zap"
)
@@ -125,13 +125,13 @@ func CreateView(ctx context.Context, view v3.SavedView) (string, error) {
createdAt := time.Now()
updatedAt := time.Now()
email, err := auth.GetEmailFromJwt(ctx)
if err != nil {
return "", err
claims, ok := authtypes.ClaimsFromContext(ctx)
if !ok {
return "", fmt.Errorf("error in getting email from context")
}
createBy := email
updatedBy := email
createBy := claims.Email
updatedBy := claims.Email
_, err = db.Exec(
"INSERT INTO saved_views (uuid, name, category, created_at, created_by, updated_at, updated_by, source_page, tags, data, extra_data) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
@@ -186,13 +186,13 @@ func UpdateView(ctx context.Context, uuid_ string, view v3.SavedView) error {
return fmt.Errorf("error in marshalling explorer query data: %s", err.Error())
}
email, err := auth.GetEmailFromJwt(ctx)
if err != nil {
return err
claims, ok := authtypes.ClaimsFromContext(ctx)
if !ok {
return fmt.Errorf("error in getting email from context")
}
updatedAt := time.Now()
updatedBy := email
updatedBy := claims.Email
_, err = db.Exec("UPDATE saved_views SET updated_at = ?, updated_by = ?, name = ?, category = ?, source_page = ?, tags = ?, data = ?, extra_data = ? WHERE uuid = ?",
updatedAt, updatedBy, view.Name, view.Category, view.SourcePage, strings.Join(view.Tags, ","), data, view.ExtraData, uuid_)

View File

@@ -49,6 +49,7 @@ import (
"go.signoz.io/signoz/pkg/query-service/contextlinks"
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
"go.signoz.io/signoz/pkg/query-service/postprocess"
"go.signoz.io/signoz/pkg/types/authtypes"
"go.uber.org/zap"
@@ -126,6 +127,8 @@ type APIHandler struct {
jobsRepo *inframetrics.JobsRepo
pvcsRepo *inframetrics.PvcsRepo
JWT *authtypes.JWT
}
type APIHandlerOpts struct {
@@ -165,6 +168,8 @@ type APIHandlerOpts struct {
UseLogsNewSchema bool
UseTraceNewSchema bool
JWT *authtypes.JWT
}
// NewAPIHandler returns an APIHandler
@@ -237,6 +242,7 @@ func NewAPIHandler(opts APIHandlerOpts) (*APIHandler, error) {
statefulsetsRepo: statefulsetsRepo,
jobsRepo: jobsRepo,
pvcsRepo: pvcsRepo,
JWT: opts.JWT,
}
logsQueryBuilder := logsv3.PrepareLogsQuery
@@ -1616,9 +1622,9 @@ func (aH *APIHandler) submitFeedback(w http.ResponseWriter, r *http.Request) {
"email": email,
"message": message,
}
userEmail, err := auth.GetEmailFromJwt(r.Context())
if err == nil {
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_INPRODUCT_FEEDBACK, data, userEmail, true, false)
claims, ok := authtypes.ClaimsFromContext(r.Context())
if ok {
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_INPRODUCT_FEEDBACK, data, claims.Email, true, false)
}
}
@@ -1628,9 +1634,9 @@ func (aH *APIHandler) registerEvent(w http.ResponseWriter, r *http.Request) {
if aH.HandleError(w, err, http.StatusBadRequest) {
return
}
userEmail, err := auth.GetEmailFromJwt(r.Context())
if err == nil {
telemetry.GetInstance().SendEvent(request.EventName, request.Attributes, userEmail, request.RateLimited, true)
claims, ok := authtypes.ClaimsFromContext(r.Context())
if ok {
telemetry.GetInstance().SendEvent(request.EventName, request.Attributes, claims.Email, request.RateLimited, true)
aH.WriteJSON(w, r, map[string]string{"data": "Event Processed Successfully"})
} else {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
@@ -1734,9 +1740,9 @@ func (aH *APIHandler) getServices(w http.ResponseWriter, r *http.Request) {
data := map[string]interface{}{
"number": len(*result),
}
userEmail, err := auth.GetEmailFromJwt(r.Context())
if err == nil {
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_NUMBER_OF_SERVICES, data, userEmail, true, false)
claims, ok := authtypes.ClaimsFromContext(r.Context())
if ok {
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_NUMBER_OF_SERVICES, data, claims.Email, true, false)
}
if (data["number"] != 0) && (data["number"] != telemetry.DEFAULT_NUMBER_OF_SERVICES) {
@@ -2160,7 +2166,7 @@ func (aH *APIHandler) loginUser(w http.ResponseWriter, r *http.Request) {
// req.RefreshToken = c.Value
// }
resp, err := auth.Login(context.Background(), req)
resp, err := auth.Login(context.Background(), req, aH.JWT)
if aH.HandleError(w, err, http.StatusUnauthorized) {
return
}
@@ -2279,13 +2285,13 @@ func (aH *APIHandler) deleteUser(w http.ResponseWriter, r *http.Request) {
RespondError(w, apiErr, "Failed to get admin group")
return
}
adminUsers, apiErr := dao.DB().GetUsersByGroup(ctx, adminGroup.Id)
adminUsers, apiErr := dao.DB().GetUsersByGroup(ctx, adminGroup.ID)
if apiErr != nil {
RespondError(w, apiErr, "Failed to get admin group users")
return
}
if user.GroupId == adminGroup.Id && len(adminUsers) == 1 {
if user.GroupId == adminGroup.ID && len(adminUsers) == 1 {
RespondError(w, &model.ApiError{
Typ: model.ErrorInternal,
Err: errors.New("cannot delete the last admin user")}, nil)
@@ -2397,7 +2403,7 @@ func (aH *APIHandler) editRole(w http.ResponseWriter, r *http.Request) {
}
}
apiErr = dao.DB().UpdateUserGroup(context.Background(), user.Id, newGroup.Id)
apiErr = dao.DB().UpdateUserGroup(context.Background(), user.Id, newGroup.ID)
if apiErr != nil {
RespondError(w, apiErr, "Failed to add user to group")
return
@@ -2442,11 +2448,11 @@ func (aH *APIHandler) editOrg(w http.ResponseWriter, r *http.Request) {
"isAnonymous": req.IsAnonymous,
"organizationName": req.Name,
}
userEmail, err := auth.GetEmailFromJwt(r.Context())
if err != nil {
zap.L().Error("failed to get user email from jwt", zap.Error(err))
claims, ok := authtypes.ClaimsFromContext(r.Context())
if !ok {
zap.L().Error("failed to get user email from jwt")
}
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_ORG_SETTINGS, data, userEmail, true, false)
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_ORG_SETTINGS, data, claims.Email, true, false)
aH.WriteJSON(w, r, map[string]string{"data": "org updated successfully"})
}
@@ -5006,8 +5012,8 @@ func sendQueryResultEvents(r *http.Request, result []*v3.Result, queryRangeParam
if len(result) > 0 && (len(result[0].Series) > 0 || len(result[0].List) > 0) {
userEmail, err := auth.GetEmailFromJwt(r.Context())
if err == nil {
claims, ok := authtypes.ClaimsFromContext(r.Context())
if ok {
queryInfoResult := telemetry.GetInstance().CheckQueryInfo(queryRangeParams)
if queryInfoResult.LogsUsed || queryInfoResult.MetricsUsed || queryInfoResult.TracesUsed {
@@ -5047,7 +5053,7 @@ func sendQueryResultEvents(r *http.Request, result []*v3.Result, queryRangeParam
"filterApplied": queryInfoResult.FilterApplied,
"dashboardId": dashboardID,
"widgetId": widgetID,
}, userEmail, true, false)
}, claims.Email, true, false)
}
if alertMatched {
var alertID string
@@ -5074,7 +5080,7 @@ func sendQueryResultEvents(r *http.Request, result []*v3.Result, queryRangeParam
"aggregateAttributeKey": queryInfoResult.AggregateAttributeKey,
"filterApplied": queryInfoResult.FilterApplied,
"alertId": alertID,
}, userEmail, true, false)
}, claims.Email, true, false)
}
}
}

View File

@@ -4,21 +4,21 @@ import (
"context"
"fmt"
"github.com/jmoiron/sqlx"
"go.signoz.io/signoz/pkg/query-service/agentConf"
"go.signoz.io/signoz/pkg/query-service/app/dashboards"
"go.signoz.io/signoz/pkg/query-service/app/logparsingpipeline"
"go.signoz.io/signoz/pkg/query-service/model"
"go.signoz.io/signoz/pkg/sqlstore"
)
type Controller struct {
mgr *Manager
}
func NewController(db *sqlx.DB) (
func NewController(sqlStore sqlstore.SQLStore) (
*Controller, error,
) {
mgr, err := NewManager(db)
mgr, err := NewManager(sqlStore.SQLxDB())
if err != nil {
return nil, fmt.Errorf("couldn't create integrations manager: %w", err)
}

View File

@@ -16,7 +16,7 @@ import (
func NewTestIntegrationsManager(t *testing.T) *Manager {
testDB := utils.NewQueryServiceDBForTests(t)
installedIntegrationsRepo, err := NewInstalledIntegrationsSqliteRepo(testDB)
installedIntegrationsRepo, err := NewInstalledIntegrationsSqliteRepo(testDB.SQLxDB())
if err != nil {
t.Fatalf("could not init sqlite DB for installed integrations: %v", err)
}

View File

@@ -11,10 +11,10 @@ import (
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
"go.signoz.io/signoz/pkg/query-service/agentConf"
"go.signoz.io/signoz/pkg/query-service/auth"
"go.signoz.io/signoz/pkg/query-service/constants"
"go.signoz.io/signoz/pkg/query-service/model"
"go.signoz.io/signoz/pkg/query-service/utils"
"go.signoz.io/signoz/pkg/types/authtypes"
"go.uber.org/zap"
)
@@ -50,9 +50,9 @@ func (ic *LogParsingPipelineController) ApplyPipelines(
postable []PostablePipeline,
) (*PipelinesResponse, *model.ApiError) {
// get user id from context
userId, authErr := auth.ExtractUserIdFromContext(ctx)
if authErr != nil {
return nil, model.UnauthorizedError(errors.Wrap(authErr, "failed to get userId from context"))
claims, ok := authtypes.ClaimsFromContext(ctx)
if !ok {
return nil, model.UnauthorizedError(fmt.Errorf("failed to get userId from context"))
}
var pipelines []Pipeline
@@ -84,7 +84,7 @@ func (ic *LogParsingPipelineController) ApplyPipelines(
}
// prepare config by calling gen func
cfg, err := agentConf.StartNewVersion(ctx, userId, agentConf.ElementTypeLogPipelines, elements)
cfg, err := agentConf.StartNewVersion(ctx, claims.UserID, agentConf.ElementTypeLogPipelines, elements)
if err != nil || cfg == nil {
return nil, err
}

View File

@@ -9,8 +9,8 @@ import (
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
"go.signoz.io/signoz/pkg/query-service/auth"
"go.signoz.io/signoz/pkg/query-service/model"
"go.signoz.io/signoz/pkg/types/authtypes"
"go.uber.org/zap"
)
@@ -45,14 +45,9 @@ func (r *Repo) insertPipeline(
))
}
jwt, ok := auth.ExtractJwtFromContext(ctx)
claims, ok := authtypes.ClaimsFromContext(ctx)
if !ok {
return nil, model.UnauthorizedError(err)
}
claims, err := auth.ParseJWT(jwt)
if err != nil {
return nil, model.UnauthorizedError(err)
return nil, model.UnauthorizedError(fmt.Errorf("failed to get email from context"))
}
insertRow := &Pipeline{
@@ -66,7 +61,7 @@ func (r *Repo) insertPipeline(
Config: postable.Config,
RawConfig: string(rawConfig),
Creator: Creator{
CreatedBy: claims["email"].(string),
CreatedBy: claims.Email,
CreatedAt: time.Now(),
},
}

View File

@@ -166,7 +166,7 @@ type testbed struct {
func newTestbed(t *testing.T) *testbed {
testDB := utils.NewQueryServiceDBForTests(t)
_, err := model.InitDB(testDB)
_, err := model.InitDB(testDB.SQLxDB())
if err != nil {
t.Fatalf("could not init opamp model: %v", err)
}

View File

@@ -1,21 +1,15 @@
package app
import (
"bufio"
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
_ "net/http/pprof" // http profiler
"regexp"
"time"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/jmoiron/sqlx"
"github.com/rs/cors"
@@ -30,8 +24,8 @@ import (
"go.signoz.io/signoz/pkg/query-service/app/opamp"
opAmpModel "go.signoz.io/signoz/pkg/query-service/app/opamp/model"
"go.signoz.io/signoz/pkg/query-service/app/preferences"
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
"go.signoz.io/signoz/pkg/signoz"
"go.signoz.io/signoz/pkg/types/authtypes"
"go.signoz.io/signoz/pkg/web"
"go.signoz.io/signoz/pkg/query-service/app/explorer"
@@ -68,6 +62,7 @@ type ServerOptions struct {
UseLogsNewSchema bool
UseTraceNewSchema bool
SigNoz *signoz.SigNoz
Jwt *authtypes.JWT
}
// Server runs HTTP, Mux and a grpc server
@@ -96,7 +91,7 @@ func (s Server) HealthCheckStatus() chan healthcheck.Status {
// NewServer creates and initializes Server
func NewServer(serverOptions *ServerOptions) (*Server, error) {
var err error
if err := dao.InitDao(serverOptions.SigNoz.SQLStore.SQLxDB()); err != nil {
if err := dao.InitDao(serverOptions.SigNoz.SQLStore); err != nil {
return nil, err
}
@@ -168,12 +163,12 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
return nil, err
}
integrationsController, err := integrations.NewController(serverOptions.SigNoz.SQLStore.SQLxDB())
integrationsController, err := integrations.NewController(serverOptions.SigNoz.SQLStore)
if err != nil {
return nil, fmt.Errorf("couldn't create integrations controller: %w", err)
}
cloudIntegrationsController, err := cloudintegrations.NewController(serverOptions.SigNoz.SQLStore.SQLxDB())
cloudIntegrationsController, err := cloudintegrations.NewController(serverOptions.SigNoz.SQLStore)
if err != nil {
return nil, fmt.Errorf("couldn't create cloud provider integrations controller: %w", err)
}
@@ -200,6 +195,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
FluxInterval: fluxInterval,
UseLogsNewSchema: serverOptions.UseLogsNewSchema,
UseTraceNewSchema: serverOptions.UseTraceNewSchema,
JWT: serverOptions.Jwt,
})
if err != nil {
return nil, err
@@ -254,6 +250,7 @@ func (s *Server) createPrivateServer(api *APIHandler) (*http.Server, error) {
r := NewRouter()
r.Use(middleware.NewAuth(zap.L(), s.serverOptions.Jwt, []string{"Authorization", "Sec-WebSocket-Protocol"}).Wrap)
r.Use(middleware.NewTimeout(zap.L(),
s.serverOptions.Config.APIServer.Timeout.ExcludedRoutes,
s.serverOptions.Config.APIServer.Timeout.Default,
@@ -284,6 +281,7 @@ func (s *Server) createPublicServer(api *APIHandler, web web.Web) (*http.Server,
r := NewRouter()
r.Use(middleware.NewAuth(zap.L(), s.serverOptions.Jwt, []string{"Authorization", "Sec-WebSocket-Protocol"}).Wrap)
r.Use(middleware.NewTimeout(zap.L(),
s.serverOptions.Config.APIServer.Timeout.ExcludedRoutes,
s.serverOptions.Config.APIServer.Timeout.Default,
@@ -293,8 +291,8 @@ func (s *Server) createPublicServer(api *APIHandler, web web.Web) (*http.Server,
r.Use(middleware.NewLogging(zap.L(), s.serverOptions.Config.APIServer.Logging.ExcludedRoutes).Wrap)
// add auth middleware
getUserFromRequest := func(r *http.Request) (*model.UserPayload, error) {
user, err := auth.GetUserFromRequest(r)
getUserFromRequest := func(ctx context.Context) (*model.UserPayload, error) {
user, err := auth.GetUserFromReqContext(ctx)
if err != nil {
return nil, err
@@ -338,176 +336,6 @@ func (s *Server) createPublicServer(api *APIHandler, web web.Web) (*http.Server,
}, nil
}
// TODO(remove): Implemented at pkg/http/middleware/logging.go
type loggingResponseWriter struct {
http.ResponseWriter
statusCode int
}
// TODO(remove): Implemented at pkg/http/middleware/logging.go
func NewLoggingResponseWriter(w http.ResponseWriter) *loggingResponseWriter {
// WriteHeader(int) is not called if our response implicitly returns 200 OK, so
// we default to that status code.
return &loggingResponseWriter{w, http.StatusOK}
}
// TODO(remove): Implemented at pkg/http/middleware/logging.go
func (lrw *loggingResponseWriter) WriteHeader(code int) {
lrw.statusCode = code
lrw.ResponseWriter.WriteHeader(code)
}
// TODO(remove): Implemented at pkg/http/middleware/logging.go
// Flush implements the http.Flush interface.
func (lrw *loggingResponseWriter) Flush() {
lrw.ResponseWriter.(http.Flusher).Flush()
}
// TODO(remove): Implemented at pkg/http/middleware/logging.go
// Support websockets
func (lrw *loggingResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
h, ok := lrw.ResponseWriter.(http.Hijacker)
if !ok {
return nil, nil, errors.New("hijack not supported")
}
return h.Hijack()
}
func extractQueryRangeV3Data(path string, r *http.Request) (map[string]interface{}, bool) {
pathToExtractBodyFromV3 := "/api/v3/query_range"
pathToExtractBodyFromV4 := "/api/v4/query_range"
data := map[string]interface{}{}
var postData *v3.QueryRangeParamsV3
if (r.Method == "POST") && ((path == pathToExtractBodyFromV3) || (path == pathToExtractBodyFromV4)) {
if r.Body != nil {
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
return nil, false
}
r.Body.Close() // must close
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
json.Unmarshal(bodyBytes, &postData)
} else {
return nil, false
}
} else {
return nil, false
}
referrer := r.Header.Get("Referer")
dashboardMatched, err := regexp.MatchString(`/dashboard/[a-zA-Z0-9\-]+/(new|edit)(?:\?.*)?$`, referrer)
if err != nil {
zap.L().Error("error while matching the referrer", zap.Error(err))
}
alertMatched, err := regexp.MatchString(`/alerts/(new|edit)(?:\?.*)?$`, referrer)
if err != nil {
zap.L().Error("error while matching the alert: ", zap.Error(err))
}
logsExplorerMatched, err := regexp.MatchString(`/logs/logs-explorer(?:\?.*)?$`, referrer)
if err != nil {
zap.L().Error("error while matching the logs explorer: ", zap.Error(err))
}
traceExplorerMatched, err := regexp.MatchString(`/traces-explorer(?:\?.*)?$`, referrer)
if err != nil {
zap.L().Error("error while matching the trace explorer: ", zap.Error(err))
}
queryInfoResult := telemetry.GetInstance().CheckQueryInfo(postData)
if (queryInfoResult.MetricsUsed || queryInfoResult.LogsUsed || queryInfoResult.TracesUsed) && (queryInfoResult.FilterApplied) {
if queryInfoResult.MetricsUsed {
telemetry.GetInstance().AddActiveMetricsUser()
}
if queryInfoResult.LogsUsed {
telemetry.GetInstance().AddActiveLogsUser()
}
if queryInfoResult.TracesUsed {
telemetry.GetInstance().AddActiveTracesUser()
}
data["metricsUsed"] = queryInfoResult.MetricsUsed
data["logsUsed"] = queryInfoResult.LogsUsed
data["tracesUsed"] = queryInfoResult.TracesUsed
data["filterApplied"] = queryInfoResult.FilterApplied
data["groupByApplied"] = queryInfoResult.GroupByApplied
data["aggregateOperator"] = queryInfoResult.AggregateOperator
data["aggregateAttributeKey"] = queryInfoResult.AggregateAttributeKey
data["numberOfQueries"] = queryInfoResult.NumberOfQueries
data["queryType"] = queryInfoResult.QueryType
data["panelType"] = queryInfoResult.PanelType
userEmail, err := auth.GetEmailFromJwt(r.Context())
if err == nil {
// switch case to set data["screen"] based on the referrer
switch {
case dashboardMatched:
data["screen"] = "panel"
case alertMatched:
data["screen"] = "alert"
case logsExplorerMatched:
data["screen"] = "logs-explorer"
case traceExplorerMatched:
data["screen"] = "traces-explorer"
default:
data["screen"] = "unknown"
return data, true
}
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_QUERY_RANGE_API, data, userEmail, true, false)
}
}
return data, true
}
func getActiveLogs(path string, r *http.Request) {
// if path == "/api/v1/dashboards/{uuid}" {
// telemetry.GetInstance().AddActiveMetricsUser()
// }
if path == "/api/v1/logs" {
hasFilters := len(r.URL.Query().Get("q"))
if hasFilters > 0 {
telemetry.GetInstance().AddActiveLogsUser()
}
}
}
func (s *Server) analyticsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := auth.AttachJwtToContext(r.Context(), r)
r = r.WithContext(ctx)
route := mux.CurrentRoute(r)
path, _ := route.GetPathTemplate()
queryRangeV3data, metadataExists := extractQueryRangeV3Data(path, r)
getActiveLogs(path, r)
lrw := NewLoggingResponseWriter(w)
next.ServeHTTP(lrw, r)
data := map[string]interface{}{"path": path, "statusCode": lrw.statusCode}
if metadataExists {
for key, value := range queryRangeV3data {
data[key] = value
}
}
// if telemetry.GetInstance().IsSampled() {
if _, ok := telemetry.EnabledPaths()[path]; ok {
userEmail, err := auth.GetEmailFromJwt(r.Context())
if err == nil {
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_PATH, data, userEmail, true, false)
}
}
// }
})
}
// initListeners initialises listeners of the server
func (s *Server) initListeners() error {
// listen on public port

View File

@@ -8,7 +8,6 @@ import (
"text/template"
"time"
"github.com/golang-jwt/jwt"
"github.com/google/uuid"
"github.com/pkg/errors"
@@ -18,6 +17,7 @@ import (
"go.signoz.io/signoz/pkg/query-service/telemetry"
"go.signoz.io/signoz/pkg/query-service/utils"
smtpservice "go.signoz.io/signoz/pkg/query-service/utils/smtpService"
"go.signoz.io/signoz/pkg/types/authtypes"
"go.uber.org/zap"
"golang.org/x/crypto/bcrypt"
)
@@ -75,17 +75,12 @@ func Invite(ctx context.Context, req *model.InviteRequest) (*model.InviteRespons
return nil, errors.Wrap(err, "invalid invite request")
}
jwtAdmin, ok := ExtractJwtFromContext(ctx)
claims, ok := authtypes.ClaimsFromContext(ctx)
if !ok {
return nil, errors.Wrap(err, "failed to extract admin jwt token")
return nil, errors.Wrap(err, "failed to extract admin user id")
}
adminUser, err := validateUser(jwtAdmin)
if err != nil {
return nil, errors.Wrap(err, "failed to validate admin jwt token")
}
au, apiErr := dao.DB().GetUser(ctx, adminUser.Id)
au, apiErr := dao.DB().GetUser(ctx, claims.UserID)
if apiErr != nil {
return nil, errors.Wrap(err, "failed to query admin user from the DB")
}
@@ -123,17 +118,12 @@ func InviteUsers(ctx context.Context, req *model.BulkInviteRequest) (*model.Bulk
FailedInvites: []model.FailedInvite{},
}
jwtAdmin, ok := ExtractJwtFromContext(ctx)
claims, ok := authtypes.ClaimsFromContext(ctx)
if !ok {
return nil, errors.New("failed to extract admin jwt token")
return nil, errors.New("failed to extract admin user id")
}
adminUser, err := validateUser(jwtAdmin)
if err != nil {
return nil, errors.Wrap(err, "failed to validate admin jwt token")
}
au, apiErr := dao.DB().GetUser(ctx, adminUser.Id)
au, apiErr := dao.DB().GetUser(ctx, claims.UserID)
if apiErr != nil {
return nil, errors.Wrap(apiErr.Err, "failed to query admin user from the DB")
}
@@ -431,7 +421,7 @@ func RegisterFirstUser(ctx context.Context, req *RegisterRequest) (*model.User,
Password: hash,
CreatedAt: time.Now().Unix(),
ProfilePictureURL: "", // Currently unused
GroupId: group.Id,
GroupId: group.ID,
OrgId: org.Id,
}
@@ -509,7 +499,7 @@ func RegisterInvitedUser(ctx context.Context, req *RegisterRequest, nopassword b
Password: hash,
CreatedAt: time.Now().Unix(),
ProfilePictureURL: "", // Currently unused
GroupId: group.Id,
GroupId: group.ID,
OrgId: invite.OrgId,
}
@@ -550,16 +540,16 @@ func Register(ctx context.Context, req *RegisterRequest) (*model.User, *model.Ap
}
// Login method returns access and refresh tokens on successful login, else it errors out.
func Login(ctx context.Context, request *model.LoginRequest) (*model.LoginResponse, error) {
func Login(ctx context.Context, request *model.LoginRequest, jwt *authtypes.JWT) (*model.LoginResponse, error) {
zap.L().Debug("Login method called for user", zap.String("email", request.Email))
user, err := authenticateLogin(ctx, request)
user, err := authenticateLogin(ctx, request, jwt)
if err != nil {
zap.L().Error("Failed to authenticate login request", zap.Error(err))
return nil, err
}
userjwt, err := GenerateJWTForUser(&user.User)
userjwt, err := GenerateJWTForUser(&user.User, jwt)
if err != nil {
zap.L().Error("Failed to generate JWT against login creds", zap.Error(err))
return nil, err
@@ -576,20 +566,36 @@ func Login(ctx context.Context, request *model.LoginRequest) (*model.LoginRespon
}, nil
}
// authenticateLogin is responsible for querying the DB and validating the credentials.
func authenticateLogin(ctx context.Context, req *model.LoginRequest) (*model.UserPayload, error) {
func claimsToUserPayload(claims authtypes.Claims) (*model.UserPayload, error) {
user := &model.UserPayload{
User: model.User{
Id: claims.UserID,
GroupId: claims.GroupID,
Email: claims.Email,
OrgId: claims.OrgID,
},
}
return user, nil
}
// authenticateLogin is responsible for querying the DB and validating the credentials.
func authenticateLogin(ctx context.Context, req *model.LoginRequest, jwt *authtypes.JWT) (*model.UserPayload, error) {
// If refresh token is valid, then simply authorize the login request.
if len(req.RefreshToken) > 0 {
user, err := validateUser(req.RefreshToken)
// parse the refresh token
claims, err := jwt.Claims(req.RefreshToken)
if err != nil {
return nil, errors.Wrap(err, "failed to validate refresh token")
return nil, errors.Wrap(err, "failed to parse refresh token")
}
if user.OrgId == "" {
if claims.OrgID == "" {
return nil, model.UnauthorizedError(errors.New("orgId is missing in the claims"))
}
user, err := claimsToUserPayload(claims)
if err != nil {
return nil, errors.Wrap(err, "failed to convert claims to user payload")
}
return user, nil
}
@@ -618,34 +624,17 @@ func passwordMatch(hash, password string) bool {
return err == nil
}
func GenerateJWTForUser(user *model.User) (model.UserJwtObject, error) {
func GenerateJWTForUser(user *model.User, jwt *authtypes.JWT) (model.UserJwtObject, error) {
j := model.UserJwtObject{}
var err error
j.AccessJwtExpiry = time.Now().Add(JwtExpiry).Unix()
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"id": user.Id,
"gid": user.GroupId,
"email": user.Email,
"exp": j.AccessJwtExpiry,
"orgId": user.OrgId,
})
j.AccessJwt, err = token.SignedString([]byte(JwtSecret))
j.AccessJwtExpiry = time.Now().Add(jwt.JwtExpiry).Unix()
j.AccessJwt, err = jwt.AccessToken(user.OrgId, user.Id, user.GroupId, user.Email)
if err != nil {
return j, errors.Errorf("failed to encode jwt: %v", err)
}
j.RefreshJwtExpiry = time.Now().Add(JwtRefresh).Unix()
token = jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"id": user.Id,
"gid": user.GroupId,
"email": user.Email,
"exp": j.RefreshJwtExpiry,
"orgId": user.OrgId,
})
j.RefreshJwt, err = token.SignedString([]byte(JwtSecret))
j.RefreshJwtExpiry = time.Now().Add(jwt.JwtRefresh).Unix()
j.RefreshJwt, err = jwt.RefreshToken(user.OrgId, user.Id, user.GroupId, user.Email)
if err != nil {
return j, errors.Errorf("failed to encode jwt: %v", err)
}

View File

@@ -1,134 +0,0 @@
package auth
import (
"context"
"fmt"
"net/http"
"time"
jwtmiddleware "github.com/auth0/go-jwt-middleware"
"github.com/golang-jwt/jwt"
"github.com/pkg/errors"
"go.signoz.io/signoz/pkg/query-service/model"
"go.uber.org/zap"
)
var (
JwtSecret string
JwtExpiry = 30 * time.Minute
JwtRefresh = 30 * 24 * time.Hour
)
func ParseJWT(jwtStr string) (jwt.MapClaims, error) {
// TODO[@vikrantgupta25] : to update this to the claims check function for better integrity of JWT
// reference - https://pkg.go.dev/github.com/golang-jwt/jwt/v5#Parser.ParseWithClaims
token, err := jwt.Parse(jwtStr, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, errors.Errorf("unknown signing algo: %v", token.Header["alg"])
}
return []byte(JwtSecret), nil
})
if err != nil {
return nil, errors.Wrapf(err, "failed to parse jwt token")
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok || !token.Valid {
return nil, errors.Errorf("Not a valid jwt claim")
}
return claims, nil
}
func validateUser(tok string) (*model.UserPayload, error) {
claims, err := ParseJWT(tok)
if err != nil {
return nil, err
}
now := time.Now().Unix()
if !claims.VerifyExpiresAt(now, true) {
return nil, model.ErrorTokenExpired
}
var orgId string
if claims["orgId"] != nil {
orgId = claims["orgId"].(string)
}
return &model.UserPayload{
User: model.User{
Id: claims["id"].(string),
GroupId: claims["gid"].(string),
Email: claims["email"].(string),
OrgId: orgId,
},
}, nil
}
// AttachJwtToContext attached the jwt token from the request header to the context.
func AttachJwtToContext(ctx context.Context, r *http.Request) context.Context {
token, err := ExtractJwtFromRequest(r)
if err != nil {
zap.L().Error("Error while getting token from header", zap.Error(err))
return ctx
}
return context.WithValue(ctx, AccessJwtKey, token)
}
func ExtractJwtFromContext(ctx context.Context) (string, bool) {
jwtToken, ok := ctx.Value(AccessJwtKey).(string)
return jwtToken, ok
}
func ExtractJwtFromRequest(r *http.Request) (string, error) {
authHeaderJwt, err := jwtmiddleware.FromAuthHeader(r)
if err != nil {
return "", err
}
if len(authHeaderJwt) > 0 {
return authHeaderJwt, nil
}
// We expect websocket connections to send auth JWT in the
// `Sec-Websocket-Protocol` header.
//
// The standard js websocket API doesn't allow setting headers
// other than the `Sec-WebSocket-Protocol` header, which is often
// used for auth purposes as a result.
return r.Header.Get("Sec-WebSocket-Protocol"), nil
}
func ExtractUserIdFromContext(ctx context.Context) (string, error) {
userId := ""
jwt, ok := ExtractJwtFromContext(ctx)
if !ok {
return "", model.InternalError(fmt.Errorf("failed to extract jwt from context"))
}
claims, err := ParseJWT(jwt)
if err != nil {
return "", model.InternalError(fmt.Errorf("failed get claims from jwt %v", err))
}
if v, ok := claims["id"]; ok {
userId = v.(string)
}
return userId, nil
}
func GetEmailFromJwt(ctx context.Context) (string, error) {
jwt, ok := ExtractJwtFromContext(ctx)
if !ok {
return "", model.InternalError(fmt.Errorf("failed to extract jwt from context"))
}
claims, err := ParseJWT(jwt)
if err != nil {
return "", model.InternalError(fmt.Errorf("failed get claims from jwt %v", err))
}
return claims["email"].(string), nil
}

View File

@@ -2,12 +2,12 @@ package auth
import (
"context"
"net/http"
"github.com/pkg/errors"
"go.signoz.io/signoz/pkg/query-service/constants"
"go.signoz.io/signoz/pkg/query-service/dao"
"go.signoz.io/signoz/pkg/query-service/model"
"go.signoz.io/signoz/pkg/types/authtypes"
)
type Group struct {
@@ -31,7 +31,7 @@ func InitAuthCache(ctx context.Context) error {
if err != nil {
return errors.Wrapf(err.Err, "failed to get group %s", groupName)
}
*dest = group.Id
*dest = group.ID
return nil
}
@@ -48,15 +48,19 @@ func InitAuthCache(ctx context.Context) error {
return nil
}
func GetUserFromRequest(r *http.Request) (*model.UserPayload, error) {
accessJwt, err := ExtractJwtFromRequest(r)
if err != nil {
return nil, err
func GetUserFromReqContext(ctx context.Context) (*model.UserPayload, error) {
claims, ok := authtypes.ClaimsFromContext(ctx)
if !ok {
return nil, errors.New("no claims found in context")
}
user, err := validateUser(accessJwt)
if err != nil {
return nil, err
user := &model.UserPayload{
User: model.User{
Id: claims.UserID,
GroupId: claims.GroupID,
Email: claims.Email,
OrgId: claims.OrgID,
},
}
return user, nil
}

View File

@@ -1,15 +1,15 @@
package dao
import (
"github.com/jmoiron/sqlx"
"go.signoz.io/signoz/pkg/query-service/dao/sqlite"
"go.signoz.io/signoz/pkg/sqlstore"
)
var db ModelDao
func InitDao(inputDB *sqlx.DB) error {
func InitDao(sqlStore sqlstore.SQLStore) error {
var err error
db, err = sqlite.InitDB(inputDB)
db, err = sqlite.InitDB(sqlStore)
if err != nil {
return err
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"go.signoz.io/signoz/pkg/query-service/model"
"go.signoz.io/signoz/pkg/types"
)
type ModelDao interface {
@@ -22,7 +23,7 @@ type Queries interface {
GetUsersWithOpts(ctx context.Context, limit int) ([]model.UserPayload, *model.ApiError)
GetGroup(ctx context.Context, id string) (*model.Group, *model.ApiError)
GetGroupByName(ctx context.Context, name string) (*model.Group, *model.ApiError)
GetGroupByName(ctx context.Context, name string) (*types.Group, *model.ApiError)
GetGroups(ctx context.Context) ([]model.Group, *model.ApiError)
GetOrgs(ctx context.Context) ([]model.Organization, *model.ApiError)
@@ -50,7 +51,7 @@ type Mutations interface {
UpdateUserFlags(ctx context.Context, userId string, flags map[string]string) (model.UserFlag, *model.ApiError)
CreateGroup(ctx context.Context, group *model.Group) (*model.Group, *model.ApiError)
CreateGroup(ctx context.Context, group *types.Group) (*types.Group, *model.ApiError)
DeleteGroup(ctx context.Context, id string) *model.ApiError
CreateOrg(ctx context.Context, org *model.Organization) (*model.Organization, *model.ApiError)

View File

@@ -5,19 +5,23 @@ import (
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
"github.com/uptrace/bun"
"go.signoz.io/signoz/pkg/query-service/constants"
"go.signoz.io/signoz/pkg/query-service/model"
"go.signoz.io/signoz/pkg/query-service/telemetry"
"go.signoz.io/signoz/pkg/sqlstore"
"go.signoz.io/signoz/pkg/types"
"go.uber.org/zap"
)
type ModelDaoSqlite struct {
db *sqlx.DB
db *sqlx.DB
bundb *bun.DB
}
// InitDB sets up setting up the connection pool global variable.
func InitDB(db *sqlx.DB) (*ModelDaoSqlite, error) {
mds := &ModelDaoSqlite{db: db}
func InitDB(sqlStore sqlstore.SQLStore) (*ModelDaoSqlite, error) {
mds := &ModelDaoSqlite{db: sqlStore.SQLxDB(), bundb: sqlStore.BunDB()}
ctx := context.Background()
if err := mds.initializeOrgPreferences(ctx); err != nil {
@@ -97,7 +101,7 @@ func (mds *ModelDaoSqlite) initializeRBAC(ctx context.Context) error {
}
func (mds *ModelDaoSqlite) createGroupIfNotPresent(ctx context.Context,
name string) (*model.Group, error) {
name string) (*types.Group, error) {
group, err := mds.GetGroupByName(ctx, name)
if err != nil {
@@ -108,7 +112,7 @@ func (mds *ModelDaoSqlite) createGroupIfNotPresent(ctx context.Context,
}
zap.L().Debug("group is not found, creating it", zap.String("group_name", name))
group, cErr := mds.CreateGroup(ctx, &model.Group{Name: name})
group, cErr := mds.CreateGroup(ctx, &types.Group{Name: name})
if cErr != nil {
return nil, cErr.Err
}

View File

@@ -10,6 +10,7 @@ import (
"github.com/pkg/errors"
"go.signoz.io/signoz/pkg/query-service/model"
"go.signoz.io/signoz/pkg/query-service/telemetry"
"go.signoz.io/signoz/pkg/types"
)
func (mds *ModelDaoSqlite) CreateInviteEntry(ctx context.Context,
@@ -436,12 +437,13 @@ func (mds *ModelDaoSqlite) GetUsersByGroup(ctx context.Context,
}
func (mds *ModelDaoSqlite) CreateGroup(ctx context.Context,
group *model.Group) (*model.Group, *model.ApiError) {
group *types.Group) (*types.Group, *model.ApiError) {
group.Id = uuid.NewString()
group.ID = uuid.NewString()
q := `INSERT INTO groups (id, name) VALUES (?, ?);`
if _, err := mds.db.ExecContext(ctx, q, group.Id, group.Name); err != nil {
if _, err := mds.bundb.NewInsert().
Model(group).
Exec(ctx); err != nil {
return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err}
}
@@ -478,10 +480,14 @@ func (mds *ModelDaoSqlite) GetGroup(ctx context.Context,
}
func (mds *ModelDaoSqlite) GetGroupByName(ctx context.Context,
name string) (*model.Group, *model.ApiError) {
name string) (*types.Group, *model.ApiError) {
groups := []model.Group{}
if err := mds.db.Select(&groups, `SELECT id, name FROM groups WHERE name=?`, name); err != nil {
groups := []types.Group{}
err := mds.bundb.NewSelect().
Model(&groups).
Where("name = ?", name).
Scan(ctx)
if err != nil {
return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err}
}

View File

@@ -17,6 +17,7 @@ import (
"go.signoz.io/signoz/pkg/query-service/constants"
"go.signoz.io/signoz/pkg/query-service/version"
"go.signoz.io/signoz/pkg/signoz"
"go.signoz.io/signoz/pkg/types/authtypes"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
@@ -98,6 +99,17 @@ func main() {
zap.L().Fatal("Failed to create signoz struct", zap.Error(err))
}
// Read the jwt secret key
jwtSecret := os.Getenv("SIGNOZ_JWT_SECRET")
if len(jwtSecret) == 0 {
zap.L().Warn("No JWT secret key is specified.")
} else {
zap.L().Info("JWT secret key set successfully.")
}
jwt := authtypes.NewJWT(jwtSecret, 30*time.Minute, 30*24*time.Hour)
serverOptions := &app.ServerOptions{
Config: config,
HTTPHostPort: constants.HTTPHostPort,
@@ -114,15 +126,7 @@ func main() {
UseLogsNewSchema: useLogsNewSchema,
UseTraceNewSchema: useTraceNewSchema,
SigNoz: signoz,
}
// Read the jwt secret key
auth.JwtSecret = os.Getenv("SIGNOZ_JWT_SECRET")
if len(auth.JwtSecret) == 0 {
zap.L().Warn("No JWT secret key is specified.")
} else {
zap.L().Info("JWT secret key set successfully.")
Jwt: jwt,
}
server, err := app.NewServer(serverOptions)

View File

@@ -10,11 +10,12 @@ import (
"time"
"github.com/jmoiron/sqlx"
"go.signoz.io/signoz/pkg/query-service/auth"
"github.com/pkg/errors"
"go.signoz.io/signoz/pkg/query-service/common"
am "go.signoz.io/signoz/pkg/query-service/integrations/alertManager"
"go.signoz.io/signoz/pkg/query-service/model"
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
"go.signoz.io/signoz/pkg/types/authtypes"
"go.uber.org/zap"
)
@@ -267,10 +268,13 @@ func (r *ruleDB) GetPlannedMaintenanceByID(ctx context.Context, id string) (*Pla
func (r *ruleDB) CreatePlannedMaintenance(ctx context.Context, maintenance PlannedMaintenance) (int64, error) {
email, _ := auth.GetEmailFromJwt(ctx)
maintenance.CreatedBy = email
claims, ok := authtypes.ClaimsFromContext(ctx)
if !ok {
return 0, errors.New("no claims found in context")
}
maintenance.CreatedBy = claims.Email
maintenance.CreatedAt = time.Now()
maintenance.UpdatedBy = email
maintenance.UpdatedBy = claims.Email
maintenance.UpdatedAt = time.Now()
query := "INSERT INTO planned_maintenance (name, description, schedule, alert_ids, created_at, created_by, updated_at, updated_by) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)"
@@ -298,8 +302,11 @@ func (r *ruleDB) DeletePlannedMaintenance(ctx context.Context, id string) (strin
}
func (r *ruleDB) EditPlannedMaintenance(ctx context.Context, maintenance PlannedMaintenance, id string) (string, error) {
email, _ := auth.GetEmailFromJwt(ctx)
maintenance.UpdatedBy = email
claims, ok := authtypes.ClaimsFromContext(ctx)
if !ok {
return "", errors.New("no claims found in context")
}
maintenance.UpdatedBy = claims.Email
maintenance.UpdatedAt = time.Now()
query := "UPDATE planned_maintenance SET name=$1, description=$2, schedule=$3, alert_ids=$4, updated_at=$5, updated_by=$6 WHERE id=$7"

View File

@@ -11,6 +11,7 @@ import (
mockhouse "github.com/srikanthccv/ClickHouse-go-mock"
"github.com/stretchr/testify/require"
"go.signoz.io/signoz/pkg/http/middleware"
"go.signoz.io/signoz/pkg/query-service/app"
"go.signoz.io/signoz/pkg/query-service/auth"
"go.signoz.io/signoz/pkg/query-service/constants"
@@ -292,20 +293,23 @@ func NewFilterSuggestionsTestBed(t *testing.T) *FilterSuggestionsTestBed {
testDB := utils.NewQueryServiceDBForTests(t)
fm := featureManager.StartManager()
reader, mockClickhouse := NewMockClickhouseReader(t, testDB, fm)
reader, mockClickhouse := NewMockClickhouseReader(t, testDB.SQLxDB(), fm)
mockClickhouse.MatchExpectationsInOrder(false)
apiHandler, err := app.NewAPIHandler(app.APIHandlerOpts{
Reader: reader,
AppDao: dao.DB(),
FeatureFlags: fm,
JWT: jwt,
})
if err != nil {
t.Fatalf("could not create a new ApiHandler: %v", err)
}
router := app.NewRouter()
am := app.NewAuthMiddleware(auth.GetUserFromRequest)
//add the jwt middleware
router.Use(middleware.NewAuth(zap.L(), jwt, []string{"Authorization", "Sec-WebSocket-Protocol"}).Wrap)
am := app.NewAuthMiddleware(auth.GetUserFromReqContext)
apiHandler.RegisterRoutes(router, am)
apiHandler.RegisterQueryRangeV3Routes(router, am)

View File

@@ -11,7 +11,6 @@ import (
"github.com/google/uuid"
"github.com/gorilla/mux"
"github.com/jmoiron/sqlx"
"github.com/knadh/koanf/parsers/yaml"
"github.com/open-telemetry/opamp-go/protobufs"
"github.com/pkg/errors"
@@ -22,13 +21,13 @@ import (
"go.signoz.io/signoz/pkg/query-service/app/logparsingpipeline"
"go.signoz.io/signoz/pkg/query-service/app/opamp"
opampModel "go.signoz.io/signoz/pkg/query-service/app/opamp/model"
"go.signoz.io/signoz/pkg/query-service/auth"
"go.signoz.io/signoz/pkg/query-service/constants"
"go.signoz.io/signoz/pkg/query-service/dao"
"go.signoz.io/signoz/pkg/query-service/model"
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
"go.signoz.io/signoz/pkg/query-service/queryBuilderToExpr"
"go.signoz.io/signoz/pkg/query-service/utils"
"go.signoz.io/signoz/pkg/sqlstore"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
)
@@ -450,18 +449,18 @@ type LogPipelinesTestBed struct {
}
// testDB can be injected for sharing a DB across multiple integration testbeds.
func NewTestbedWithoutOpamp(t *testing.T, testDB *sqlx.DB) *LogPipelinesTestBed {
if testDB == nil {
testDB = utils.NewQueryServiceDBForTests(t)
func NewTestbedWithoutOpamp(t *testing.T, sqlStore sqlstore.SQLStore) *LogPipelinesTestBed {
if sqlStore == nil {
sqlStore = utils.NewQueryServiceDBForTests(t)
}
ic, err := integrations.NewController(testDB)
ic, err := integrations.NewController(sqlStore)
if err != nil {
t.Fatalf("could not create integrations controller: %v", err)
}
controller, err := logparsingpipeline.NewLogParsingPipelinesController(
testDB, ic.GetPipelinesForInstalledIntegrations,
sqlStore.SQLxDB(), ic.GetPipelinesForInstalledIntegrations,
)
if err != nil {
t.Fatalf("could not create a logparsingpipelines controller: %v", err)
@@ -470,6 +469,7 @@ func NewTestbedWithoutOpamp(t *testing.T, testDB *sqlx.DB) *LogPipelinesTestBed
apiHandler, err := app.NewAPIHandler(app.APIHandlerOpts{
AppDao: dao.DB(),
LogsParsingPipelineController: controller,
JWT: jwt,
})
if err != nil {
t.Fatalf("could not create a new ApiHandler: %v", err)
@@ -481,7 +481,7 @@ func NewTestbedWithoutOpamp(t *testing.T, testDB *sqlx.DB) *LogPipelinesTestBed
}
// Mock an available opamp agent
testDB, err = opampModel.InitDB(testDB)
testDB, err := opampModel.InitDB(sqlStore.SQLxDB())
require.Nil(t, err, "failed to init opamp model")
agentConfMgr, err := agentConf.Initiate(&agentConf.ManagerOptions{
@@ -499,7 +499,7 @@ func NewTestbedWithoutOpamp(t *testing.T, testDB *sqlx.DB) *LogPipelinesTestBed
}
}
func NewLogPipelinesTestBed(t *testing.T, testDB *sqlx.DB) *LogPipelinesTestBed {
func NewLogPipelinesTestBed(t *testing.T, testDB sqlstore.SQLStore) *LogPipelinesTestBed {
testbed := NewTestbedWithoutOpamp(t, testDB)
opampServer := opamp.InitializeServer(nil, testbed.agentConfMgr)
@@ -540,7 +540,12 @@ func (tb *LogPipelinesTestBed) PostPipelinesToQSExpectingStatusCode(
}
respWriter := httptest.NewRecorder()
ctx := auth.AttachJwtToContext(req.Context(), req)
ctx, err := tb.apiHandler.JWT.ContextFromRequest(req.Context(), req.Header.Get("Authorization"))
if err != nil {
tb.t.Fatalf("couldn't get jwt from request: %v", err)
}
req = req.WithContext(ctx)
tb.apiHandler.CreateLogsPipeline(respWriter, req)

View File

@@ -9,9 +9,9 @@ import (
"time"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
mockhouse "github.com/srikanthccv/ClickHouse-go-mock"
"github.com/stretchr/testify/require"
"go.signoz.io/signoz/pkg/http/middleware"
"go.signoz.io/signoz/pkg/query-service/app"
"go.signoz.io/signoz/pkg/query-service/app/cloudintegrations"
"go.signoz.io/signoz/pkg/query-service/auth"
@@ -19,6 +19,8 @@ import (
"go.signoz.io/signoz/pkg/query-service/featureManager"
"go.signoz.io/signoz/pkg/query-service/model"
"go.signoz.io/signoz/pkg/query-service/utils"
"go.signoz.io/signoz/pkg/sqlstore"
"go.uber.org/zap"
)
func TestAWSIntegrationAccountLifecycle(t *testing.T) {
@@ -342,7 +344,7 @@ type CloudIntegrationsTestBed struct {
}
// testDB can be injected for sharing a DB across multiple integration testbeds.
func NewCloudIntegrationsTestBed(t *testing.T, testDB *sqlx.DB) *CloudIntegrationsTestBed {
func NewCloudIntegrationsTestBed(t *testing.T, testDB sqlstore.SQLStore) *CloudIntegrationsTestBed {
if testDB == nil {
testDB = utils.NewQueryServiceDBForTests(t)
}
@@ -353,7 +355,7 @@ func NewCloudIntegrationsTestBed(t *testing.T, testDB *sqlx.DB) *CloudIntegratio
}
fm := featureManager.StartManager()
reader, mockClickhouse := NewMockClickhouseReader(t, testDB, fm)
reader, mockClickhouse := NewMockClickhouseReader(t, testDB.SQLxDB(), fm)
mockClickhouse.MatchExpectationsInOrder(false)
apiHandler, err := app.NewAPIHandler(app.APIHandlerOpts{
@@ -361,13 +363,15 @@ func NewCloudIntegrationsTestBed(t *testing.T, testDB *sqlx.DB) *CloudIntegratio
AppDao: dao.DB(),
CloudIntegrationsController: controller,
FeatureFlags: fm,
JWT: jwt,
})
if err != nil {
t.Fatalf("could not create a new ApiHandler: %v", err)
}
router := app.NewRouter()
am := app.NewAuthMiddleware(auth.GetUserFromRequest)
router.Use(middleware.NewAuth(zap.L(), jwt, []string{"Authorization", "Sec-WebSocket-Protocol"}).Wrap)
am := app.NewAuthMiddleware(auth.GetUserFromReqContext)
apiHandler.RegisterRoutes(router, am)
apiHandler.RegisterCloudIntegrationsRoutes(router, am)

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