From 072693d57dc802d87d3f38146f2870ab68387da0 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Wed, 21 Aug 2024 17:55:16 +0530 Subject: [PATCH 1/7] fix: nan and inf values in formula result (#5733) --- pkg/query-service/postprocess/formula.go | 4 + pkg/query-service/postprocess/formula_test.go | 171 +++++++----------- 2 files changed, 65 insertions(+), 110 deletions(-) diff --git a/pkg/query-service/postprocess/formula.go b/pkg/query-service/postprocess/formula.go index 6c800b47c4..4d928a6a48 100644 --- a/pkg/query-service/postprocess/formula.go +++ b/pkg/query-service/postprocess/formula.go @@ -146,6 +146,10 @@ func joinAndCalculate( return nil, fmt.Errorf("expected float64, got %T", newValue) } + if math.IsNaN(val) || math.IsInf(val, 0) { + continue + } + resultSeries.Points = append(resultSeries.Points, v3.Point{ Timestamp: timestamp, Value: val, diff --git a/pkg/query-service/postprocess/formula_test.go b/pkg/query-service/postprocess/formula_test.go index d80519b105..22fc9f61db 100644 --- a/pkg/query-service/postprocess/formula_test.go +++ b/pkg/query-service/postprocess/formula_test.go @@ -1,7 +1,6 @@ package postprocess import ( - "math" "reflect" "testing" @@ -204,9 +203,10 @@ func TestFindUniqueLabelSets(t *testing.T) { func TestProcessResults(t *testing.T) { tests := []struct { - name string - results []*v3.Result - want *v3.Result + name string + results []*v3.Result + want *v3.Result + expression string }{ { name: "test1", @@ -288,12 +288,68 @@ func TestProcessResults(t *testing.T) { }, }, }, + expression: "A + B", + }, + { + name: "test2", + results: []*v3.Result{ + { + QueryName: "A", + Series: []*v3.Series{ + { + Labels: map[string]string{}, + Points: []v3.Point{ + { + Timestamp: 1, + Value: 10, + }, + { + Timestamp: 2, + Value: 0, + }, + }, + }, + }, + }, + { + QueryName: "B", + Series: []*v3.Series{ + { + Labels: map[string]string{}, + Points: []v3.Point{ + { + Timestamp: 1, + Value: 0, + }, + { + Timestamp: 3, + Value: 10, + }, + }, + }, + }, + }, + }, + want: &v3.Result{ + Series: []*v3.Series{ + { + Labels: map[string]string{}, + Points: []v3.Point{ + { + Timestamp: 3, + Value: 0, + }, + }, + }, + }, + }, + expression: "A/B", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - expression, err := govaluate.NewEvaluableExpression("A + B") + expression, err := govaluate.NewEvaluableExpression(tt.expression) if err != nil { t.Errorf("Error parsing expression: %v", err) } @@ -835,10 +891,6 @@ func TestFormula(t *testing.T) { Timestamp: 5, Value: 1, }, - { - Timestamp: 7, - Value: math.Inf(1), - }, }, }, { @@ -855,10 +907,6 @@ func TestFormula(t *testing.T) { Timestamp: 2, Value: 0.6923076923076923, }, - { - Timestamp: 3, - Value: math.Inf(1), - }, { Timestamp: 4, Value: 1, @@ -997,62 +1045,6 @@ func TestFormula(t *testing.T) { }, want: &v3.Result{ Series: []*v3.Series{ - { - Labels: map[string]string{ - "host_name": "ip-10-420-69-1", - "state": "running", - }, - Points: []v3.Point{ - { - Timestamp: 1, - Value: math.Inf(0), - }, - { - Timestamp: 2, - Value: math.Inf(0), - }, - { - Timestamp: 4, - Value: math.Inf(0), - }, - { - Timestamp: 5, - Value: math.Inf(0), - }, - { - Timestamp: 7, - Value: math.Inf(0), - }, - }, - }, - { - Labels: map[string]string{ - "host_name": "ip-10-420-69-2", - "state": "idle", - }, - Points: []v3.Point{ - { - Timestamp: 1, - Value: math.Inf(0), - }, - { - Timestamp: 2, - Value: math.Inf(0), - }, - { - Timestamp: 3, - Value: math.Inf(0), - }, - { - Timestamp: 4, - Value: math.Inf(0), - }, - { - Timestamp: 5, - Value: math.Inf(0), - }, - }, - }, { Labels: map[string]string{ "host_name": "ip-10-420-69-1", @@ -1262,39 +1254,6 @@ func TestFormula(t *testing.T) { Timestamp: 5, Value: 1, }, - { - Timestamp: 7, - Value: math.Inf(1), - }, - }, - }, - { - Labels: map[string]string{ - "host_name": "ip-10-420-69-2", - "state": "idle", - "os.type": "linux", - }, - Points: []v3.Point{ - { - Timestamp: 1, - Value: math.Inf(0), - }, - { - Timestamp: 2, - Value: math.Inf(0), - }, - { - Timestamp: 3, - Value: math.Inf(0), - }, - { - Timestamp: 4, - Value: math.Inf(0), - }, - { - Timestamp: 5, - Value: math.Inf(0), - }, }, }, { @@ -1537,10 +1496,6 @@ func TestFormula(t *testing.T) { Timestamp: 5, Value: 51, }, - { - Timestamp: 7, - Value: math.Inf(1), - }, }, }, { @@ -1558,10 +1513,6 @@ func TestFormula(t *testing.T) { Timestamp: 2, Value: 45.6923076923076923, }, - { - Timestamp: 3, - Value: math.Inf(1), - }, { Timestamp: 4, Value: 41, From e7b5410c5b250c5a5553781f7d5705b7c33a11be Mon Sep 17 00:00:00 2001 From: Vibhu Pandey Date: Thu, 22 Aug 2024 14:24:02 +0530 Subject: [PATCH 2/7] feat(packages): add registry and http packages (#5740) ### Summary Add packages for Registry and HTTP #### Related Issues / PR's https://github.com/SigNoz/signoz/pull/5710 --- .gitignore | 3 + ee/query-service/app/server.go | 8 ++ pkg/http/doc.go | 3 + pkg/http/middleware/doc.go | 2 + pkg/http/middleware/logging.go | 72 ++++++++++++++++ pkg/http/middleware/middleware.go | 20 +++++ pkg/http/middleware/response.go | 122 +++++++++++++++++++++++++++ pkg/http/middleware/timeout.go | 78 +++++++++++++++++ pkg/http/middleware/timeout_test.go | 80 ++++++++++++++++++ pkg/http/server/config.go | 27 ++++++ pkg/http/server/doc.go | 2 + pkg/http/server/server.go | 79 +++++++++++++++++ pkg/query-service/app/server.go | 9 ++ pkg/query-service/app/server_test.go | 1 + pkg/registry/doc.go | 3 + pkg/registry/registry.go | 84 ++++++++++++++++++ pkg/registry/registry_test.go | 56 ++++++++++++ pkg/registry/service.go | 16 ++++ pkg/registry/service_test.go | 49 +++++++++++ 19 files changed, 714 insertions(+) create mode 100644 pkg/http/doc.go create mode 100644 pkg/http/middleware/doc.go create mode 100644 pkg/http/middleware/logging.go create mode 100644 pkg/http/middleware/middleware.go create mode 100644 pkg/http/middleware/response.go create mode 100644 pkg/http/middleware/timeout.go create mode 100644 pkg/http/middleware/timeout_test.go create mode 100644 pkg/http/server/config.go create mode 100644 pkg/http/server/doc.go create mode 100644 pkg/http/server/server.go create mode 100644 pkg/registry/doc.go create mode 100644 pkg/registry/registry.go create mode 100644 pkg/registry/registry_test.go create mode 100644 pkg/registry/service.go create mode 100644 pkg/registry/service_test.go diff --git a/.gitignore b/.gitignore index 8fe54dcf3d..2bd9238255 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,6 @@ e2e/.auth # go vendor/ **/main/** + +# git-town +.git-branches.toml diff --git a/ee/query-service/app/server.go b/ee/query-service/app/server.go index 082ddcd358..d77e71cc28 100644 --- a/ee/query-service/app/server.go +++ b/ee/query-service/app/server.go @@ -376,6 +376,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e }, nil } +// TODO(remove): Implemented at pkg/http/middleware/logging.go // loggingMiddleware is used for logging public api calls func loggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -387,6 +388,7 @@ func loggingMiddleware(next http.Handler) http.Handler { }) } +// TODO(remove): Implemented at pkg/http/middleware/logging.go // loggingMiddlewarePrivate is used for logging private api calls // from internal services like alert manager func loggingMiddlewarePrivate(next http.Handler) http.Handler { @@ -399,27 +401,32 @@ func loggingMiddlewarePrivate(next http.Handler) http.Handler { }) } +// 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) @@ -565,6 +572,7 @@ func (s *Server) analyticsMiddleware(next http.Handler) http.Handler { }) } +// TODO(remove): Implemented at pkg/http/middleware/timeout.go func setTimeoutMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() diff --git a/pkg/http/doc.go b/pkg/http/doc.go new file mode 100644 index 0000000000..5e62c61b15 --- /dev/null +++ b/pkg/http/doc.go @@ -0,0 +1,3 @@ +// package http contains all http related functions such +// as servers, middlewares, routers and renders. +package http diff --git a/pkg/http/middleware/doc.go b/pkg/http/middleware/doc.go new file mode 100644 index 0000000000..911746777c --- /dev/null +++ b/pkg/http/middleware/doc.go @@ -0,0 +1,2 @@ +// package middleware contains an implementation of all middlewares. +package middleware diff --git a/pkg/http/middleware/logging.go b/pkg/http/middleware/logging.go new file mode 100644 index 0000000000..ef755f6648 --- /dev/null +++ b/pkg/http/middleware/logging.go @@ -0,0 +1,72 @@ +package middleware + +import ( + "bytes" + "net" + "net/http" + "time" + + "github.com/gorilla/mux" + semconv "go.opentelemetry.io/otel/semconv/v1.26.0" + "go.uber.org/zap" +) + +const ( + logMessage string = "::RECEIVED-REQUEST::" +) + +type Logging struct { + logger *zap.Logger +} + +func NewLogging(logger *zap.Logger) *Logging { + if logger == nil { + panic("cannot build logging, logger is empty") + } + + return &Logging{ + logger: logger.Named(pkgname), + } +} + +func (middleware *Logging) Wrap(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + ctx := req.Context() + start := time.Now() + host, port, _ := net.SplitHostPort(req.Host) + path, err := mux.CurrentRoute(req).GetPathTemplate() + if err != nil { + path = req.URL.Path + } + + fields := []zap.Field{ + zap.Any("context", ctx), + zap.String(string(semconv.ClientAddressKey), req.RemoteAddr), + zap.String(string(semconv.UserAgentOriginalKey), req.UserAgent()), + zap.String(string(semconv.ServerAddressKey), host), + zap.String(string(semconv.ServerPortKey), port), + zap.Int64(string(semconv.HTTPRequestSizeKey), req.ContentLength), + zap.String(string(semconv.HTTPRouteKey), path), + } + + buf := new(bytes.Buffer) + writer := newBadResponseLoggingWriter(rw, buf) + next.ServeHTTP(writer, req) + + statusCode, err := writer.StatusCode(), writer.WriteError() + fields = append(fields, + zap.Int(string(semconv.HTTPResponseStatusCodeKey), statusCode), + zap.Duration(string(semconv.HTTPServerRequestDurationName), time.Since(start)), + ) + if err != nil { + fields = append(fields, zap.Error(err)) + middleware.logger.Error(logMessage, fields...) + } else { + if buf.Len() != 0 { + fields = append(fields, zap.String("response.body", buf.String())) + } + + middleware.logger.Info(logMessage, fields...) + } + }) +} diff --git a/pkg/http/middleware/middleware.go b/pkg/http/middleware/middleware.go new file mode 100644 index 0000000000..6313089aa4 --- /dev/null +++ b/pkg/http/middleware/middleware.go @@ -0,0 +1,20 @@ +package middleware + +import "net/http" + +const ( + pkgname string = "go.signoz.io/pkg/http/middleware" +) + +// Wrapper is an interface implemented by all middlewares +type Wrapper interface { + Wrap(http.Handler) http.Handler +} + +// WrapperFunc is to Wrapper as http.HandlerFunc is to http.Handler +type WrapperFunc func(http.Handler) http.Handler + +// WrapperFunc implements Wrapper +func (m WrapperFunc) Wrap(next http.Handler) http.Handler { + return m(next) +} diff --git a/pkg/http/middleware/response.go b/pkg/http/middleware/response.go new file mode 100644 index 0000000000..deb0f3dd81 --- /dev/null +++ b/pkg/http/middleware/response.go @@ -0,0 +1,122 @@ +package middleware + +import ( + "bufio" + "fmt" + "io" + "net" + "net/http" +) + +const ( + maxResponseBodyInLogs = 4096 // At most 4k bytes from response bodies in our logs. +) + +type badResponseLoggingWriter interface { + http.ResponseWriter + // Get the status code. + StatusCode() int + // Get the error while writing. + WriteError() error +} + +func newBadResponseLoggingWriter(rw http.ResponseWriter, buffer io.Writer) badResponseLoggingWriter { + b := nonFlushingBadResponseLoggingWriter{ + rw: rw, + buffer: buffer, + logBody: false, + bodyBytesLeft: maxResponseBodyInLogs, + statusCode: http.StatusOK, + } + + if f, ok := rw.(http.Flusher); ok { + return &flushingBadResponseLoggingWriter{b, f} + } + + return &b +} + +type nonFlushingBadResponseLoggingWriter struct { + rw http.ResponseWriter + buffer io.Writer + logBody bool + bodyBytesLeft int + statusCode int + writeError error // The error returned when downstream Write() fails. +} + +// Extends nonFlushingBadResponseLoggingWriter that implements http.Flusher +type flushingBadResponseLoggingWriter struct { + nonFlushingBadResponseLoggingWriter + f http.Flusher +} + +// Unwrap method is used by http.ResponseController to get access to original http.ResponseWriter. +func (writer *nonFlushingBadResponseLoggingWriter) Unwrap() http.ResponseWriter { + return writer.rw +} + +// Header returns the header map that will be sent by WriteHeader. +// Implements ResponseWriter. +func (writer *nonFlushingBadResponseLoggingWriter) Header() http.Header { + return writer.rw.Header() +} + +// WriteHeader writes the HTTP response header. +func (writer *nonFlushingBadResponseLoggingWriter) WriteHeader(statusCode int) { + writer.statusCode = statusCode + if statusCode >= 500 || statusCode == 400 { + writer.logBody = true + } + writer.rw.WriteHeader(statusCode) +} + +// Writes HTTP response data. +func (writer *nonFlushingBadResponseLoggingWriter) Write(data []byte) (int, error) { + if writer.statusCode == 0 { + // WriteHeader has (probably) not been called, so we need to call it with StatusOK to fulfill the interface contract. + // https://godoc.org/net/http#ResponseWriter + writer.WriteHeader(http.StatusOK) + } + n, err := writer.rw.Write(data) + if writer.logBody { + writer.captureResponseBody(data) + } + if err != nil { + writer.writeError = err + } + return n, err +} + +// Hijack hijacks the first response writer that is a Hijacker. +func (writer *nonFlushingBadResponseLoggingWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { + hj, ok := writer.rw.(http.Hijacker) + if ok { + return hj.Hijack() + } + return nil, nil, fmt.Errorf("cannot cast underlying response writer to Hijacker") +} + +func (writer *nonFlushingBadResponseLoggingWriter) StatusCode() int { + return writer.statusCode +} + +func (writer *nonFlushingBadResponseLoggingWriter) WriteError() error { + return writer.writeError +} + +func (writer *flushingBadResponseLoggingWriter) Flush() { + writer.f.Flush() +} + +func (writer *nonFlushingBadResponseLoggingWriter) captureResponseBody(data []byte) { + if len(data) > writer.bodyBytesLeft { + _, _ = writer.buffer.Write(data[:writer.bodyBytesLeft]) + _, _ = io.WriteString(writer.buffer, "...") + writer.bodyBytesLeft = 0 + writer.logBody = false + } else { + _, _ = writer.buffer.Write(data) + writer.bodyBytesLeft -= len(data) + } +} diff --git a/pkg/http/middleware/timeout.go b/pkg/http/middleware/timeout.go new file mode 100644 index 0000000000..50e9d82f22 --- /dev/null +++ b/pkg/http/middleware/timeout.go @@ -0,0 +1,78 @@ +package middleware + +import ( + "context" + "net/http" + "strings" + "time" + + "go.uber.org/zap" +) + +const ( + headerName string = "timeout" +) + +type Timeout struct { + logger *zap.Logger + excluded map[string]struct{} + // The default timeout + defaultTimeout time.Duration + // The max allowed timeout + maxTimeout time.Duration +} + +func NewTimeout(logger *zap.Logger, excluded map[string]struct{}, defaultTimeout time.Duration, maxTimeout time.Duration) *Timeout { + if logger == nil { + panic("cannot build timeout, logger is empty") + } + + if excluded == nil { + excluded = make(map[string]struct{}) + } + + if defaultTimeout.Seconds() == 0 { + defaultTimeout = 60 * time.Second + } + + if maxTimeout == 0 { + maxTimeout = 600 * time.Second + } + + return &Timeout{ + logger: logger.Named(pkgname), + excluded: excluded, + defaultTimeout: defaultTimeout, + maxTimeout: maxTimeout, + } +} + +func (middleware *Timeout) Wrap(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + if _, ok := middleware.excluded[req.URL.Path]; !ok { + actual := middleware.defaultTimeout + incoming := req.Header.Get(headerName) + if incoming != "" { + parsed, err := time.ParseDuration(strings.TrimSpace(incoming) + "s") + if err != nil { + middleware.logger.Warn("cannot parse timeout in header, using default timeout", zap.String("timeout", incoming), zap.Error(err), zap.Any("context", req.Context())) + } else { + if parsed > middleware.maxTimeout { + actual = middleware.maxTimeout + } else { + actual = parsed + } + } + } + + ctx, cancel := context.WithTimeout(req.Context(), actual) + defer cancel() + + req = req.WithContext(ctx) + next.ServeHTTP(rw, req) + return + } + + next.ServeHTTP(rw, req) + }) +} diff --git a/pkg/http/middleware/timeout_test.go b/pkg/http/middleware/timeout_test.go new file mode 100644 index 0000000000..2575bfe7d9 --- /dev/null +++ b/pkg/http/middleware/timeout_test.go @@ -0,0 +1,80 @@ +package middleware + +import ( + "net" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/require" + "go.uber.org/zap" +) + +func TestTimeout(t *testing.T) { + t.Parallel() + + writeTimeout := 6 * time.Second + defaultTimeout := 2 * time.Second + maxTimeout := 4 * time.Second + m := NewTimeout(zap.NewNop(), map[string]struct{}{"/excluded": {}}, defaultTimeout, maxTimeout) + + listener, err := net.Listen("tcp", "localhost:0") + require.NoError(t, err) + + server := &http.Server{ + WriteTimeout: writeTimeout, + Handler: m.Wrap(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, ok := r.Context().Deadline() + if ok { + <-r.Context().Done() + require.Error(t, r.Context().Err()) + } + w.WriteHeader(204) + })), + } + + go func() { + require.NoError(t, server.Serve(listener)) + }() + + testCases := []struct { + name string + wait time.Duration + header string + path string + }{ + { + name: "WaitTillNoTimeoutForExcludedPath", + wait: 1 * time.Nanosecond, + header: "4", + path: "excluded", + }, + { + name: "WaitTillHeaderTimeout", + wait: 3 * time.Second, + header: "3", + path: "header-timeout", + }, + { + name: "WaitTillMaxTimeout", + wait: 4 * time.Second, + header: "5", + path: "max-timeout", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + start := time.Now() + req, err := http.NewRequest("GET", "http://"+listener.Addr().String()+"/"+tc.path, nil) + require.NoError(t, err) + req.Header.Add(headerName, tc.header) + + _, err = http.DefaultClient.Do(req) + require.NoError(t, err) + + // confirm that we waited at least till the "wait" time + require.GreaterOrEqual(t, time.Since(start), tc.wait) + }) + } +} diff --git a/pkg/http/server/config.go b/pkg/http/server/config.go new file mode 100644 index 0000000000..fb8eb5be11 --- /dev/null +++ b/pkg/http/server/config.go @@ -0,0 +1,27 @@ +package server + +import ( + "go.signoz.io/signoz/pkg/confmap" +) + +// Config satisfies the confmap.Config interface +var _ confmap.Config = (*Config)(nil) + +// Config holds the configuration for http. +type Config struct { + //Address specifies the TCP address for the server to listen on, in the form "host:port". + // If empty, ":http" (port 80) is used. The service names are defined in RFC 6335 and assigned by IANA. + // See net.Dial for details of the address format. + Address string `mapstructure:"address"` +} + +func (c *Config) NewWithDefaults() confmap.Config { + return &Config{ + Address: "0.0.0.0:8080", + } + +} + +func (c *Config) Validate() error { + return nil +} diff --git a/pkg/http/server/doc.go b/pkg/http/server/doc.go new file mode 100644 index 0000000000..9bf12b5ae5 --- /dev/null +++ b/pkg/http/server/doc.go @@ -0,0 +1,2 @@ +// package server contains an implementation of the http server. +package server diff --git a/pkg/http/server/server.go b/pkg/http/server/server.go new file mode 100644 index 0000000000..fbeca1c3a9 --- /dev/null +++ b/pkg/http/server/server.go @@ -0,0 +1,79 @@ +package server + +import ( + "context" + "fmt" + "net/http" + "time" + + "go.signoz.io/signoz/pkg/registry" + "go.uber.org/zap" +) + +var _ registry.NamedService = (*Server)(nil) + +type Server struct { + srv *http.Server + logger *zap.Logger + handler http.Handler + cfg Config + name string +} + +func New(logger *zap.Logger, name string, cfg Config, handler http.Handler) (*Server, error) { + if handler == nil { + return nil, fmt.Errorf("cannot build http server, handler is required") + } + + if logger == nil { + return nil, fmt.Errorf("cannot build http server, logger is required") + } + + if name == "" { + return nil, fmt.Errorf("cannot build http server, name is required") + } + + srv := &http.Server{ + Addr: cfg.Address, + Handler: handler, + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + MaxHeaderBytes: 1 << 20, + } + + return &Server{ + srv: srv, + logger: logger.Named("go.signoz.io/pkg/http/server"), + handler: handler, + cfg: cfg, + name: name, + }, nil +} + +func (server *Server) Name() string { + return server.name +} + +func (server *Server) Start(ctx context.Context) error { + server.logger.Info("starting http server", zap.String("address", server.srv.Addr)) + if err := server.srv.ListenAndServe(); err != nil { + if err != http.ErrServerClosed { + server.logger.Error("failed to start server", zap.Error(err), zap.Any("context", ctx)) + return err + } + } + return nil +} + +func (server *Server) Stop(ctx context.Context) error { + ctx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + + if err := server.srv.Shutdown(ctx); err != nil { + server.logger.Error("failed to stop server", zap.Error(err), zap.Any("context", ctx)) + return err + } + + server.logger.Info("server stopped gracefully", zap.Any("context", ctx)) + return nil +} diff --git a/pkg/query-service/app/server.go b/pkg/query-service/app/server.go index 4fb4d9ad22..77caa9170b 100644 --- a/pkg/query-service/app/server.go +++ b/pkg/query-service/app/server.go @@ -322,6 +322,7 @@ func (s *Server) createPublicServer(api *APIHandler) (*http.Server, error) { }, nil } +// TODO(remove): Implemented at pkg/http/middleware/logging.go // loggingMiddleware is used for logging public api calls func loggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -392,6 +393,7 @@ func LogCommentEnricher(next http.Handler) http.Handler { }) } +// TODO(remove): Implemented at pkg/http/middleware/logging.go // loggingMiddlewarePrivate is used for logging private api calls // from internal services like alert manager func loggingMiddlewarePrivate(next http.Handler) http.Handler { @@ -404,27 +406,32 @@ func loggingMiddlewarePrivate(next http.Handler) http.Handler { }) } +// 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) @@ -538,6 +545,7 @@ func (s *Server) analyticsMiddleware(next http.Handler) http.Handler { }) } +// TODO(remove): Implemented at pkg/http/middleware/timeout.go func getRouteContextTimeout(overrideTimeout string) time.Duration { var timeout time.Duration var err error @@ -554,6 +562,7 @@ func getRouteContextTimeout(overrideTimeout string) time.Duration { return constants.ContextTimeout } +// TODO(remove): Implemented at pkg/http/middleware/timeout.go func setTimeoutMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() diff --git a/pkg/query-service/app/server_test.go b/pkg/query-service/app/server_test.go index afdc06fa33..49fc6191fb 100644 --- a/pkg/query-service/app/server_test.go +++ b/pkg/query-service/app/server_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/assert" ) +// TODO(remove): Implemented at pkg/http/middleware/timeout_test.go func TestGetRouteContextTimeout(t *testing.T) { var testGetRouteContextTimeoutData = []struct { Name string diff --git a/pkg/registry/doc.go b/pkg/registry/doc.go new file mode 100644 index 0000000000..ff2debbefe --- /dev/null +++ b/pkg/registry/doc.go @@ -0,0 +1,3 @@ +// package registry contains a simple implementation of https://github.com/google/guava/wiki/ServiceExplained +// Here the the "ServiceManager" is called the "Registry" +package registry diff --git a/pkg/registry/registry.go b/pkg/registry/registry.go new file mode 100644 index 0000000000..850a4389a9 --- /dev/null +++ b/pkg/registry/registry.go @@ -0,0 +1,84 @@ +package registry + +import ( + "context" + "errors" + "fmt" + "os" + "os/signal" + "syscall" + + "go.uber.org/zap" +) + +type Registry struct { + services []NamedService + logger *zap.Logger + startCh chan error + stopCh chan error +} + +// New creates a new registry of services. It needs at least one service in the input. +func New(logger *zap.Logger, services ...NamedService) (*Registry, error) { + if logger == nil { + return nil, fmt.Errorf("cannot build registry, logger is required") + } + + if len(services) == 0 { + return nil, fmt.Errorf("cannot build registry, at least one service is required") + } + + return &Registry{ + logger: logger.Named("go.signoz.io/pkg/registry"), + services: services, + startCh: make(chan error, 1), + stopCh: make(chan error, len(services)), + }, nil +} + +func (r *Registry) Start(ctx context.Context) error { + for _, s := range r.services { + go func(s Service) { + err := s.Start(ctx) + r.startCh <- err + }(s) + } + + return nil +} + +func (r *Registry) Wait(ctx context.Context) error { + interrupt := make(chan os.Signal, 1) + signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM) + + select { + case <-ctx.Done(): + r.logger.Info("caught context error, exiting", zap.Any("context", ctx)) + case s := <-interrupt: + r.logger.Info("caught interrupt signal, exiting", zap.Any("context", ctx), zap.Any("signal", s)) + case err := <-r.startCh: + r.logger.Info("caught service error, exiting", zap.Any("context", ctx), zap.Error(err)) + return err + } + + return nil +} + +func (r *Registry) Stop(ctx context.Context) error { + for _, s := range r.services { + go func(s Service) { + err := s.Stop(ctx) + r.stopCh <- err + }(s) + } + + errs := make([]error, len(r.services)) + for i := 0; i < len(r.services); i++ { + err := <-r.stopCh + if err != nil { + errs = append(errs, err) + } + } + + return errors.Join(errs...) +} diff --git a/pkg/registry/registry_test.go b/pkg/registry/registry_test.go new file mode 100644 index 0000000000..12ae1d8862 --- /dev/null +++ b/pkg/registry/registry_test.go @@ -0,0 +1,56 @@ +package registry + +import ( + "context" + "sync" + "testing" + + "github.com/stretchr/testify/require" + "go.uber.org/zap" +) + +func TestRegistryWith2HttpServers(t *testing.T) { + http1, err := newHttpService("http1") + require.NoError(t, err) + + http2, err := newHttpService("http2") + require.NoError(t, err) + + registry, err := New(zap.NewNop(), http1, http2) + require.NoError(t, err) + + ctx, cancel := context.WithCancel(context.Background()) + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + require.NoError(t, registry.Start(ctx)) + require.NoError(t, registry.Wait(ctx)) + require.NoError(t, registry.Stop(ctx)) + }() + cancel() + + wg.Wait() +} + +func TestRegistryWith2HttpServersWithoutWait(t *testing.T) { + http1, err := newHttpService("http1") + require.NoError(t, err) + + http2, err := newHttpService("http2") + require.NoError(t, err) + + registry, err := New(zap.NewNop(), http1, http2) + require.NoError(t, err) + + ctx := context.Background() + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + require.NoError(t, registry.Start(ctx)) + require.NoError(t, registry.Stop(ctx)) + }() + + wg.Wait() +} diff --git a/pkg/registry/service.go b/pkg/registry/service.go new file mode 100644 index 0000000000..38df4f0a4f --- /dev/null +++ b/pkg/registry/service.go @@ -0,0 +1,16 @@ +package registry + +import "context" + +type Service interface { + // Starts a service. The service should return an error if it cannot be started. + Start(context.Context) error + // Stops a service. + Stop(context.Context) error +} + +type NamedService interface { + // Identifier of a service. It should be unique across all services. + Name() string + Service +} diff --git a/pkg/registry/service_test.go b/pkg/registry/service_test.go new file mode 100644 index 0000000000..dc0621e962 --- /dev/null +++ b/pkg/registry/service_test.go @@ -0,0 +1,49 @@ +package registry + +import ( + "context" + "net" + "net/http" +) + +var _ NamedService = (*httpService)(nil) + +type httpService struct { + Listener net.Listener + Server *http.Server + name string +} + +func newHttpService(name string) (*httpService, error) { + return &httpService{ + name: name, + Server: &http.Server{}, + }, nil +} + +func (service *httpService) Name() string { + return service.name +} + +func (service *httpService) Start(ctx context.Context) error { + listener, err := net.Listen("tcp", "localhost:0") + if err != nil { + return err + } + service.Listener = listener + + if err := service.Server.Serve(service.Listener); err != nil { + if err != http.ErrServerClosed { + return err + } + } + return nil +} + +func (service *httpService) Stop(ctx context.Context) error { + if err := service.Server.Shutdown(ctx); err != nil { + return err + } + + return nil +} From c322fc72d9c71c08db93d7a96047da7ff07c954b Mon Sep 17 00:00:00 2001 From: Vibhu Pandey Date: Thu, 22 Aug 2024 15:19:32 +0530 Subject: [PATCH 3/7] feat(errors): add errors package (#5741) ### Summary Add errors package #### Related Issues / PR's https://github.com/SigNoz/signoz/pull/5710 --- pkg/errors/doc.go | 3 + pkg/errors/errors.go | 118 ++++++++++++++++++++++++++++++++++++++ pkg/errors/errors_test.go | 53 +++++++++++++++++ pkg/errors/type.go | 14 +++++ pkg/errors/type_test.go | 18 ++++++ 5 files changed, 206 insertions(+) create mode 100644 pkg/errors/doc.go create mode 100644 pkg/errors/errors.go create mode 100644 pkg/errors/errors_test.go create mode 100644 pkg/errors/type.go create mode 100644 pkg/errors/type_test.go diff --git a/pkg/errors/doc.go b/pkg/errors/doc.go new file mode 100644 index 0000000000..f9cd863c72 --- /dev/null +++ b/pkg/errors/doc.go @@ -0,0 +1,3 @@ +// package error contains error related utilities. Use this package when +// a well-defined error has to be shown. +package errors diff --git a/pkg/errors/errors.go b/pkg/errors/errors.go new file mode 100644 index 0000000000..7bd3e1b97e --- /dev/null +++ b/pkg/errors/errors.go @@ -0,0 +1,118 @@ +package errors + +import ( + "fmt" +) + +// base is the fundamental struct that implements the error interface. +// The order of the struct is 'TCMEUA'. +type base struct { + // t denotes the custom type of the error. + t typ + // c denotes the short code for the error message. + c string + // m contains error message passed through errors.New. + m string + // e is the actual error being wrapped. + e error + // u denotes the url for the documentation (if present) for the error. + u string + // a denotes any additional error messages (if present). + a []string +} + +// base implements Error interface. +func (b *base) Error() string { + if b.e != nil { + return b.e.Error() + } + + return fmt.Sprintf("%s(%s): %s", b.t.s, b.c, b.m) +} + +// New returns a base error. It requires type, code and message as input. +func New(t typ, code string, message string) *base { + return &base{ + t: t, + c: code, + m: message, + e: nil, + u: "", + a: []string{}, + } +} + +// Newf returns a new base by formatting the error message with the supplied format specifier. +func Newf(t typ, code string, format string, args ...interface{}) *base { + return &base{ + t: t, + c: code, + m: fmt.Sprintf(format, args...), + e: nil, + } +} + +// Wrapf returns a new error by formatting the error message with the supplied format specifier +// and wrapping another error with base. +func Wrapf(cause error, t typ, code string, format string, args ...interface{}) *base { + return &base{ + t: t, + c: code, + m: fmt.Sprintf(format, args...), + e: cause, + } +} + +// WithUrl adds a url to the base error and returns a new base error. +func (b *base) WithUrl(u string) *base { + return &base{ + t: b.t, + c: b.c, + m: b.m, + e: b.e, + u: u, + a: b.a, + } +} + +// WithUrl adds additional messages to the base error and returns a new base error. +func (b *base) WithAdditional(a ...string) *base { + return &base{ + t: b.t, + c: b.c, + m: b.m, + e: b.e, + u: b.u, + a: a, + } +} + +// Unwrapb is a combination of built-in errors.As and type casting. +// It finds the first error in cause that matches base, +// and if one is found, returns the individual fields of base. +// Otherwise, it returns TypeInternal, the original error string +// and the error itself. +// +//lint:ignore ST1008 we want to return arguments in the 'TCMEUA' order of the struct +func Unwrapb(cause error) (typ, string, string, error, string, []string) { + base, ok := cause.(*base) + if ok { + return base.t, base.c, base.m, base.e, base.u, base.a + } + + return TypeInternal, "", cause.Error(), cause, "", []string{} +} + +// Ast checks if the provided error matches the specified custom error type. +func Ast(cause error, typ typ) bool { + t, _, _, _, _, _ := Unwrapb(cause) + + return t == typ +} + +// Ast checks if the provided error matches the specified custom error code. +func Asc(cause error, code string) bool { + _, c, _, _, _, _ := Unwrapb(cause) + + return c == code +} diff --git a/pkg/errors/errors_test.go b/pkg/errors/errors_test.go new file mode 100644 index 0000000000..4b04e876af --- /dev/null +++ b/pkg/errors/errors_test.go @@ -0,0 +1,53 @@ +package errors + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNew(t *testing.T) { + typ := typ{"test-error"} + err := New(typ, "code", "test error info") + assert.NotNil(t, err) +} + +func TestNewf(t *testing.T) { + typ := typ{"test-error"} + err := Newf(typ, "test-code", "test error info with %s", "string") + assert.NotNil(t, err) + assert.Equal(t, "test-error(test-code): test error info with string", err.Error()) +} + +func TestWrapf(t *testing.T) { + typ := typ{"test-error"} + err := Wrapf(errors.New("original error"), typ, "test-code", "info for err %d", 2) + assert.NotNil(t, err) +} + +func TestError(t *testing.T) { + typ := typ{"test-error"} + err1 := New(typ, "test-code", "info for err1") + assert.Equal(t, "test-error(test-code): info for err1", err1.Error()) + + err2 := Wrapf(err1, typ, "test-code", "info for err2") + assert.Equal(t, "test-error(test-code): info for err1", err2.Error()) +} + +func TestUnwrapb(t *testing.T) { + typ := typ{"test-error"} + oerr := errors.New("original error") + berr := Wrapf(oerr, typ, "test-code", "this is a base err").WithUrl("https://docs").WithAdditional("additional err") + + atyp, acode, amessage, aerr, au, aa := Unwrapb(berr) + assert.Equal(t, typ, atyp) + assert.Equal(t, "test-code", acode) + assert.Equal(t, "this is a base err", amessage) + assert.Equal(t, oerr, aerr) + assert.Equal(t, "https://docs", au) + assert.Equal(t, []string{"additional err"}, aa) + + atyp, _, _, _, _, _ = Unwrapb(oerr) + assert.Equal(t, TypeInternal, atyp) +} diff --git a/pkg/errors/type.go b/pkg/errors/type.go new file mode 100644 index 0000000000..6800a8bc71 --- /dev/null +++ b/pkg/errors/type.go @@ -0,0 +1,14 @@ +package errors + +var ( + TypeInvalidInput typ = typ{"invalid-input"} + TypeInternal = typ{"internal"} + TypeUnsupported = typ{"unsupported"} + TypeNotFound = typ{"not-found"} + TypeMethodNotAllowed = typ{"method-not-allowed"} + TypeAlreadyExists = typ{"already-exists"} + TypeUnauthenticated = typ{"unauthenticated"} +) + +// Defines custom error types +type typ struct{ s string } diff --git a/pkg/errors/type_test.go b/pkg/errors/type_test.go new file mode 100644 index 0000000000..7b7d3a26b3 --- /dev/null +++ b/pkg/errors/type_test.go @@ -0,0 +1,18 @@ +package errors + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestString(t *testing.T) { + typ := typ{"test-error"} + assert.Equal(t, typ.s, "test-error") +} + +func TestEquals(t *testing.T) { + typ1 := typ{"test-error"} + typ2 := typ{"test-error"} + assert.True(t, typ1 == typ2) +} From bfeceb0ed298c4c977707a847aba342e1e97bd80 Mon Sep 17 00:00:00 2001 From: Vibhu Pandey Date: Thu, 22 Aug 2024 20:56:15 +0530 Subject: [PATCH 4/7] feat(web): add web package (#5743) ### Summary Add a web package for serving frontend #### Related Issues / PR's https://github.com/SigNoz/signoz/pull/5710 --- pkg/config/config.go | 4 + pkg/config/config_test.go | 9 +- pkg/http/middleware/cache.go | 28 ++++++ pkg/http/middleware/cache_test.go | 56 +++++++++++ pkg/web/config.go | 29 ++++++ pkg/web/testdata/index.html | 1 + pkg/web/web.go | 94 ++++++++++++++++++ pkg/web/web_test.go | 159 ++++++++++++++++++++++++++++++ 8 files changed, 379 insertions(+), 1 deletion(-) create mode 100644 pkg/http/middleware/cache.go create mode 100644 pkg/http/middleware/cache_test.go create mode 100644 pkg/web/config.go create mode 100644 pkg/web/testdata/index.html create mode 100644 pkg/web/web.go create mode 100644 pkg/web/web_test.go diff --git a/pkg/config/config.go b/pkg/config/config.go index 621cc073c6..02cc7c37c2 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -4,11 +4,13 @@ import ( "context" "go.signoz.io/signoz/pkg/instrumentation" + "go.signoz.io/signoz/pkg/web" ) // Config defines the entire configuration of signoz. type Config struct { Instrumentation instrumentation.Config `mapstructure:"instrumentation"` + Web web.Config `mapstructure:"web"` } func New(ctx context.Context, settings ProviderSettings) (*Config, error) { @@ -24,6 +26,8 @@ func byName(name string) (any, bool) { switch name { case "instrumentation": return &instrumentation.Config{}, true + case "web": + return &web.Config{}, true default: return nil, false } diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 04ede418f0..b3e3007bb4 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -10,12 +10,15 @@ import ( contribsdkconfig "go.opentelemetry.io/contrib/config" "go.signoz.io/signoz/pkg/confmap/provider/signozenvprovider" "go.signoz.io/signoz/pkg/instrumentation" + "go.signoz.io/signoz/pkg/web" ) func TestNewWithSignozEnvProvider(t *testing.T) { t.Setenv("SIGNOZ__INSTRUMENTATION__LOGS__ENABLED", "true") t.Setenv("SIGNOZ__INSTRUMENTATION__LOGS__PROCESSORS__BATCH__EXPORTER__OTLP__ENDPOINT", "0.0.0.0:4317") t.Setenv("SIGNOZ__INSTRUMENTATION__LOGS__PROCESSORS__BATCH__EXPORT_TIMEOUT", "10") + t.Setenv("SIGNOZ__WEB__PREFIX", "/web") + t.Setenv("SIGNOZ__WEB__DIRECTORY", "/build") config, err := New(context.Background(), ProviderSettings{ ResolverSettings: confmap.ResolverSettings{ @@ -34,7 +37,7 @@ func TestNewWithSignozEnvProvider(t *testing.T) { Enabled: true, LoggerProvider: contribsdkconfig.LoggerProvider{ Processors: []contribsdkconfig.LogRecordProcessor{ - contribsdkconfig.LogRecordProcessor{ + { Batch: &contribsdkconfig.BatchLogRecordProcessor{ ExportTimeout: &i, Exporter: contribsdkconfig.LogRecordExporter{ @@ -48,6 +51,10 @@ func TestNewWithSignozEnvProvider(t *testing.T) { }, }, }, + Web: web.Config{ + Prefix: "/web", + Directory: "/build", + }, } assert.Equal(t, expected, config) diff --git a/pkg/http/middleware/cache.go b/pkg/http/middleware/cache.go new file mode 100644 index 0000000000..ff66288354 --- /dev/null +++ b/pkg/http/middleware/cache.go @@ -0,0 +1,28 @@ +package middleware + +import ( + "net/http" + "strconv" + "time" +) + +type Cache struct { + maxAge time.Duration +} + +func NewCache(maxAge time.Duration) *Cache { + if maxAge == 0 { + maxAge = 7 * 24 * time.Hour + } + + return &Cache{ + maxAge: maxAge, + } +} + +func (middleware *Cache) Wrap(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.Header().Set("Cache-Control", "max-age="+strconv.Itoa(int(middleware.maxAge.Seconds()))) + next.ServeHTTP(rw, req) + }) +} diff --git a/pkg/http/middleware/cache_test.go b/pkg/http/middleware/cache_test.go new file mode 100644 index 0000000000..bef0e3b36b --- /dev/null +++ b/pkg/http/middleware/cache_test.go @@ -0,0 +1,56 @@ +package middleware + +import ( + "net" + "net/http" + "strconv" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestCache(t *testing.T) { + t.Parallel() + + age := 20 * 24 * time.Hour + m := NewCache(age) + + listener, err := net.Listen("tcp", "localhost:0") + require.NoError(t, err) + + server := &http.Server{ + Handler: m.Wrap(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(204) + })), + } + + go func() { + require.NoError(t, server.Serve(listener)) + }() + + testCases := []struct { + name string + age time.Duration + }{ + { + name: "Success", + age: age, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + req, err := http.NewRequest("GET", "http://"+listener.Addr().String(), nil) + require.NoError(t, err) + + res, err := http.DefaultClient.Do(req) + require.NoError(t, err) + + actual := res.Header.Get("Cache-control") + require.NoError(t, err) + + require.Equal(t, "max-age="+strconv.Itoa(int(age.Seconds())), string(actual)) + }) + } +} diff --git a/pkg/web/config.go b/pkg/web/config.go new file mode 100644 index 0000000000..a0e0c531de --- /dev/null +++ b/pkg/web/config.go @@ -0,0 +1,29 @@ +package web + +import ( + "go.signoz.io/signoz/pkg/confmap" +) + +// Config satisfies the confmap.Config interface +var _ confmap.Config = (*Config)(nil) + +// Config holds the configuration for web. +type Config struct { + // The prefix to serve the files from + Prefix string `mapstructure:"prefix"` + // The directory containing the static build files. The root of this directory should + // have an index.html file. + Directory string `mapstructure:"directory"` +} + +func (c *Config) NewWithDefaults() confmap.Config { + return &Config{ + Prefix: "/", + Directory: "/etc/signoz/web", + } + +} + +func (c *Config) Validate() error { + return nil +} diff --git a/pkg/web/testdata/index.html b/pkg/web/testdata/index.html new file mode 100644 index 0000000000..49c8c8383a --- /dev/null +++ b/pkg/web/testdata/index.html @@ -0,0 +1 @@ +

Welcome to test data!!!

\ No newline at end of file diff --git a/pkg/web/web.go b/pkg/web/web.go new file mode 100644 index 0000000000..2e29b000f9 --- /dev/null +++ b/pkg/web/web.go @@ -0,0 +1,94 @@ +package web + +import ( + "fmt" + "net/http" + "os" + "path/filepath" + "time" + + "github.com/gorilla/mux" + "go.signoz.io/signoz/pkg/http/middleware" + "go.uber.org/zap" +) + +var _ http.Handler = (*Web)(nil) + +const ( + indexFileName string = "index.html" +) + +type Web struct { + logger *zap.Logger + cfg Config +} + +func New(logger *zap.Logger, cfg Config) (*Web, error) { + if logger == nil { + return nil, fmt.Errorf("cannot build web, logger is required") + } + + fi, err := os.Stat(cfg.Directory) + if err != nil { + return nil, fmt.Errorf("cannot access web directory: %w", err) + } + + ok := fi.IsDir() + if !ok { + return nil, fmt.Errorf("web directory is not a directory") + } + + fi, err = os.Stat(filepath.Join(cfg.Directory, indexFileName)) + if err != nil { + return nil, fmt.Errorf("cannot access %q in web directory: %w", indexFileName, err) + } + + if os.IsNotExist(err) || fi.IsDir() { + return nil, fmt.Errorf("%q does not exist", indexFileName) + } + + return &Web{ + logger: logger.Named("go.signoz.io/pkg/web"), + cfg: cfg, + }, nil +} + +func (web *Web) AddToRouter(router *mux.Router) error { + cache := middleware.NewCache(7 * 24 * time.Hour) + err := router.PathPrefix(web.cfg.Prefix). + Handler( + http.StripPrefix( + web.cfg.Prefix, + cache.Wrap(http.HandlerFunc(web.ServeHTTP)), + ), + ).GetError() + if err != nil { + return fmt.Errorf("unable to add web to router: %w", err) + } + + return nil +} + +func (web *Web) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + // Join internally call path.Clean to prevent directory traversal + path := filepath.Join(web.cfg.Directory, req.URL.Path) + + // check whether a file exists or is a directory at the given path + fi, err := os.Stat(path) + if os.IsNotExist(err) || fi.IsDir() { + // file does not exist or path is a directory, serve index.html + http.ServeFile(rw, req, filepath.Join(web.cfg.Directory, indexFileName)) + return + } + + if err != nil { + // if we got an error (that wasn't that the file doesn't exist) stating the + // file, return a 500 internal server error and stop + // TODO: Put down a crash html page here + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + // otherwise, use http.FileServer to serve the static file + http.FileServer(http.Dir(web.cfg.Directory)).ServeHTTP(rw, req) +} diff --git a/pkg/web/web_test.go b/pkg/web/web_test.go new file mode 100644 index 0000000000..d5111cf747 --- /dev/null +++ b/pkg/web/web_test.go @@ -0,0 +1,159 @@ +package web + +import ( + "io" + "net" + "net/http" + "os" + "path/filepath" + "testing" + + "github.com/gorilla/mux" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/zap" +) + +func TestServeHttpWithoutPrefix(t *testing.T) { + t.Parallel() + fi, err := os.Open(filepath.Join("testdata", indexFileName)) + require.NoError(t, err) + + expected, err := io.ReadAll(fi) + require.NoError(t, err) + + web, err := New(zap.NewNop(), Config{Prefix: "/", Directory: filepath.Join("testdata")}) + require.NoError(t, err) + + router := mux.NewRouter() + err = web.AddToRouter(router) + require.NoError(t, err) + + listener, err := net.Listen("tcp", "localhost:0") + require.NoError(t, err) + + server := &http.Server{ + Handler: router, + } + + go func() { + _ = server.Serve(listener) + }() + defer func() { + _ = server.Close() + }() + + testCases := []struct { + name string + path string + }{ + { + name: "Root", + path: "/", + }, + { + name: "Index", + path: "/" + indexFileName, + }, + { + name: "DoesNotExist", + path: "/does-not-exist", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + res, err := http.DefaultClient.Get("http://" + listener.Addr().String() + tc.path) + require.NoError(t, err) + + defer func() { + _ = res.Body.Close() + }() + + actual, err := io.ReadAll(res.Body) + require.NoError(t, err) + + assert.Equal(t, expected, actual) + }) + } + +} + +func TestServeHttpWithPrefix(t *testing.T) { + t.Parallel() + fi, err := os.Open(filepath.Join("testdata", indexFileName)) + require.NoError(t, err) + + expected, err := io.ReadAll(fi) + require.NoError(t, err) + + web, err := New(zap.NewNop(), Config{Prefix: "/web", Directory: filepath.Join("testdata")}) + require.NoError(t, err) + + router := mux.NewRouter() + err = web.AddToRouter(router) + require.NoError(t, err) + + listener, err := net.Listen("tcp", "localhost:0") + require.NoError(t, err) + + server := &http.Server{ + Handler: router, + } + + go func() { + _ = server.Serve(listener) + }() + defer func() { + _ = server.Close() + }() + + testCases := []struct { + name string + path string + found bool + }{ + { + name: "Root", + path: "/web", + found: true, + }, + { + name: "Index", + path: "/web/" + indexFileName, + found: true, + }, + { + name: "FileDoesNotExist", + path: "/web/does-not-exist", + found: true, + }, + { + name: "DoesNotExist", + path: "/does-not-exist", + found: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + res, err := http.DefaultClient.Get("http://" + listener.Addr().String() + tc.path) + require.NoError(t, err) + + defer func() { + _ = res.Body.Close() + }() + + if tc.found { + actual, err := io.ReadAll(res.Body) + require.NoError(t, err) + + assert.Equal(t, expected, actual) + } else { + assert.Equal(t, http.StatusNotFound, res.StatusCode) + } + + }) + } + +} From 96b81817e03fe8fb3c1abac6f32e82f7e1d355ba Mon Sep 17 00:00:00 2001 From: Vikrant Gupta Date: Thu, 22 Aug 2024 23:56:51 +0530 Subject: [PATCH 5/7] feat: add support for changing the font size in logs (#5739) * feat: add support for changing the font size in logs * fix: build issues and logs context * chore: fix build issues * feat: scale all the spaces * chore: handle light mode designs * feat: set small as the default --- .../components/Logs/AddToQueryHOC.styles.scss | 13 + .../src/components/Logs/AddToQueryHOC.tsx | 6 +- .../Logs/ListLogView/ListLogView.styles.scss | 69 ++++ .../src/components/Logs/ListLogView/index.tsx | 43 ++- .../src/components/Logs/ListLogView/styles.ts | 33 +- .../LogStateIndicator.styles.scss | 15 +- .../LogStateIndicator.test.tsx | 15 +- .../LogStateIndicator/LogStateIndicator.tsx | 5 +- .../src/components/Logs/RawLogView/index.tsx | 3 + .../src/components/Logs/RawLogView/styles.ts | 16 +- .../src/components/Logs/RawLogView/types.ts | 3 + .../src/components/Logs/TableView/styles.ts | 9 + .../src/components/Logs/TableView/types.ts | 2 + .../Logs/TableView/useTableView.styles.scss | 33 ++ .../Logs/TableView/useTableView.tsx | 12 +- .../LogsFormatOptionsMenu.styles.scss | 153 +++++++- .../LogsFormatOptionsMenu.tsx | 336 +++++++++++------- .../container/LiveLogs/LiveLogsList/index.tsx | 4 + .../ContextView/ContextLogRenderer.tsx | 41 ++- .../ContextView/useContextLogData.ts | 23 +- .../container/LogDetailedView/LogContext.tsx | 2 + .../container/LogDetailedView/TableView.tsx | 3 +- .../src/container/LogsContextList/configs.ts | 1 + .../src/container/LogsContextList/index.tsx | 2 + .../container/LogsExplorerContext/index.tsx | 2 + .../InfinityTableView/TableRow.tsx | 4 + .../InfinityTableView/config.ts | 15 + .../InfinityTableView/index.tsx | 15 +- .../InfinityTableView/styles.ts | 23 +- .../src/container/LogsExplorerList/index.tsx | 21 +- frontend/src/container/LogsTable/index.tsx | 4 + .../src/container/OptionsMenu/constants.ts | 3 +- frontend/src/container/OptionsMenu/types.ts | 12 + .../container/OptionsMenu/useOptionsMenu.ts | 34 +- 34 files changed, 797 insertions(+), 178 deletions(-) diff --git a/frontend/src/components/Logs/AddToQueryHOC.styles.scss b/frontend/src/components/Logs/AddToQueryHOC.styles.scss index 42baabd02a..b65c3cb17e 100644 --- a/frontend/src/components/Logs/AddToQueryHOC.styles.scss +++ b/frontend/src/components/Logs/AddToQueryHOC.styles.scss @@ -1,3 +1,16 @@ .addToQueryContainer { cursor: pointer; + display: flex; + align-items: center; + &.small { + height: 16px; + } + + &.medium { + height: 20px; + } + + &.large { + height: 24px; + } } diff --git a/frontend/src/components/Logs/AddToQueryHOC.tsx b/frontend/src/components/Logs/AddToQueryHOC.tsx index 8391a23b81..df222b7552 100644 --- a/frontend/src/components/Logs/AddToQueryHOC.tsx +++ b/frontend/src/components/Logs/AddToQueryHOC.tsx @@ -1,13 +1,16 @@ import './AddToQueryHOC.styles.scss'; import { Popover } from 'antd'; +import cx from 'classnames'; import { OPERATORS } from 'constants/queryBuilder'; +import { FontSize } from 'container/OptionsMenu/types'; import { memo, MouseEvent, ReactNode, useMemo } from 'react'; function AddToQueryHOC({ fieldKey, fieldValue, onAddToQuery, + fontSize, children, }: AddToQueryHOCProps): JSX.Element { const handleQueryAdd = (event: MouseEvent): void => { @@ -21,7 +24,7 @@ function AddToQueryHOC({ return ( // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions -
+
{children} @@ -33,6 +36,7 @@ export interface AddToQueryHOCProps { fieldKey: string; fieldValue: string; onAddToQuery: (fieldKey: string, fieldValue: string, operator: string) => void; + fontSize: FontSize; children: ReactNode; } diff --git a/frontend/src/components/Logs/ListLogView/ListLogView.styles.scss b/frontend/src/components/Logs/ListLogView/ListLogView.styles.scss index 3caf6a3282..21dcf171ce 100644 --- a/frontend/src/components/Logs/ListLogView/ListLogView.styles.scss +++ b/frontend/src/components/Logs/ListLogView/ListLogView.styles.scss @@ -6,6 +6,21 @@ font-weight: 400; line-height: 18px; /* 128.571% */ letter-spacing: -0.07px; + + &.small { + font-size: 11px; + line-height: 16px; + } + + &.medium { + font-size: 13px; + line-height: 20px; + } + + &.large { + font-size: 14px; + line-height: 24px; + } } .log-value { color: var(--text-vanilla-400, #c0c1c3); @@ -14,6 +29,21 @@ font-weight: 400; line-height: 18px; /* 128.571% */ letter-spacing: -0.07px; + + &.small { + font-size: 11px; + line-height: 16px; + } + + &.medium { + font-size: 13px; + line-height: 20px; + } + + &.large { + font-size: 14px; + line-height: 24px; + } } .log-line { display: flex; @@ -40,6 +70,20 @@ font-weight: 400; line-height: 18px; /* 128.571% */ letter-spacing: -0.07px; + &.small { + font-size: 11px; + line-height: 16px; + } + + &.medium { + font-size: 13px; + line-height: 20px; + } + + &.large { + font-size: 14px; + line-height: 24px; + } } .selected-log-value { @@ -52,12 +96,37 @@ line-height: 18px; letter-spacing: -0.07px; font-size: 14px; + &.small { + font-size: 11px; + line-height: 16px; + } + + &.medium { + font-size: 13px; + line-height: 20px; + } + + &.large { + font-size: 14px; + line-height: 24px; + } } .selected-log-kv { min-height: 24px; display: flex; align-items: center; + &.small { + min-height: 16px; + } + + &.medium { + min-height: 20px; + } + + &.large { + min-height: 24px; + } } } diff --git a/frontend/src/components/Logs/ListLogView/index.tsx b/frontend/src/components/Logs/ListLogView/index.tsx index fa8a2fb608..d1461b0caa 100644 --- a/frontend/src/components/Logs/ListLogView/index.tsx +++ b/frontend/src/components/Logs/ListLogView/index.tsx @@ -3,8 +3,10 @@ import './ListLogView.styles.scss'; import { blue } from '@ant-design/colors'; import Convert from 'ansi-to-html'; import { Typography } from 'antd'; +import cx from 'classnames'; import LogDetail from 'components/LogDetail'; import { VIEW_TYPES } from 'components/LogDetail/constants'; +import { FontSize } from 'container/OptionsMenu/types'; import dayjs from 'dayjs'; import dompurify from 'dompurify'; import { useActiveLog } from 'hooks/logs/useActiveLog'; @@ -39,6 +41,7 @@ interface LogFieldProps { fieldKey: string; fieldValue: string; linesPerRow?: number; + fontSize: FontSize; } type LogSelectedFieldProps = Omit & @@ -48,6 +51,7 @@ function LogGeneralField({ fieldKey, fieldValue, linesPerRow = 1, + fontSize, }: LogFieldProps): JSX.Element { const html = useMemo( () => ({ @@ -62,12 +66,12 @@ function LogGeneralField({ return ( - + {`${fieldKey} : `} 1 ? linesPerRow : undefined} /> @@ -78,6 +82,7 @@ function LogSelectedField({ fieldKey = '', fieldValue = '', onAddToQuery, + fontSize, }: LogSelectedFieldProps): JSX.Element { return (
@@ -85,16 +90,22 @@ function LogSelectedField({ fieldKey={fieldKey} fieldValue={fieldValue} onAddToQuery={onAddToQuery} + fontSize={fontSize} > - + {fieldKey} - - {': '} - {fieldValue || "''"} + + {': '} + + {fieldValue || "''"} +
); @@ -107,6 +118,7 @@ type ListLogViewProps = { onAddToQuery: AddToQueryHOCProps['onAddToQuery']; activeLog?: ILog | null; linesPerRow: number; + fontSize: FontSize; }; function ListLogView({ @@ -116,6 +128,7 @@ function ListLogView({ onAddToQuery, activeLog, linesPerRow, + fontSize, }: ListLogViewProps): JSX.Element { const flattenLogData = useMemo(() => FlatLogData(logData), [logData]); @@ -185,6 +198,7 @@ function ListLogView({ onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} onClick={handleDetailedView} + fontSize={fontSize} >
- + {flattenLogData.stream && ( - + )} - + {updatedSelecedFields.map((field) => isValidLogField(flattenLogData[field.name] as never) ? ( @@ -212,6 +236,7 @@ function ListLogView({ fieldKey={field.name} fieldValue={flattenLogData[field.name] as never} onAddToQuery={onAddToQuery} + fontSize={fontSize} /> ) : null, )} diff --git a/frontend/src/components/Logs/ListLogView/styles.ts b/frontend/src/components/Logs/ListLogView/styles.ts index 52cc2b20d4..d2a6342c77 100644 --- a/frontend/src/components/Logs/ListLogView/styles.ts +++ b/frontend/src/components/Logs/ListLogView/styles.ts @@ -1,21 +1,46 @@ +/* eslint-disable no-nested-ternary */ import { Color } from '@signozhq/design-tokens'; import { Card, Typography } from 'antd'; +import { FontSize } from 'container/OptionsMenu/types'; import styled from 'styled-components'; interface LogTextProps { linesPerRow?: number; } +interface LogContainerProps { + fontSize: FontSize; +} + export const Container = styled(Card)<{ $isActiveLog: boolean; $isDarkMode: boolean; + fontSize: FontSize; }>` width: 100% !important; margin-bottom: 0.3rem; + + ${({ fontSize }): string => + fontSize === FontSize.SMALL + ? `margin-bottom:0.1rem;` + : fontSize === FontSize.MEDIUM + ? `margin-bottom: 0.2rem;` + : fontSize === FontSize.LARGE + ? `margin-bottom:0.3rem;` + : ``} cursor: pointer; .ant-card-body { padding: 0.3rem 0.6rem; + ${({ fontSize }): string => + fontSize === FontSize.SMALL + ? `padding:0.1rem 0.6rem;` + : fontSize === FontSize.MEDIUM + ? `padding: 0.2rem 0.6rem;` + : fontSize === FontSize.LARGE + ? `padding:0.3rem 0.6rem;` + : ``} + ${({ $isActiveLog, $isDarkMode }): string => $isActiveLog ? `background-color: ${ @@ -38,11 +63,17 @@ export const TextContainer = styled.div` width: 100%; `; -export const LogContainer = styled.div` +export const LogContainer = styled.div` margin-left: 0.5rem; display: flex; flex-direction: column; gap: 6px; + ${({ fontSize }): string => + fontSize === FontSize.SMALL + ? `gap: 2px;` + : fontSize === FontSize.MEDIUM + ? ` gap:4px;` + : `gap:6px;`} `; export const LogText = styled.div` diff --git a/frontend/src/components/Logs/LogStateIndicator/LogStateIndicator.styles.scss b/frontend/src/components/Logs/LogStateIndicator/LogStateIndicator.styles.scss index a00c7f6761..61870abc71 100644 --- a/frontend/src/components/Logs/LogStateIndicator/LogStateIndicator.styles.scss +++ b/frontend/src/components/Logs/LogStateIndicator/LogStateIndicator.styles.scss @@ -9,11 +9,24 @@ border-radius: 50px; background-color: transparent; + &.small { + min-height: 16px; + } + + &.medium { + min-height: 20px; + } + + &.large { + min-height: 24px; + } + &.INFO { background-color: var(--bg-slate-400); } - &.WARNING, &.WARN { + &.WARNING, + &.WARN { background-color: var(--bg-amber-500); } diff --git a/frontend/src/components/Logs/LogStateIndicator/LogStateIndicator.test.tsx b/frontend/src/components/Logs/LogStateIndicator/LogStateIndicator.test.tsx index d924c27426..06cc9d3ec4 100644 --- a/frontend/src/components/Logs/LogStateIndicator/LogStateIndicator.test.tsx +++ b/frontend/src/components/Logs/LogStateIndicator/LogStateIndicator.test.tsx @@ -1,10 +1,13 @@ import { render } from '@testing-library/react'; +import { FontSize } from 'container/OptionsMenu/types'; import LogStateIndicator from './LogStateIndicator'; describe('LogStateIndicator', () => { it('renders correctly with default props', () => { - const { container } = render(); + const { container } = render( + , + ); const indicator = container.firstChild as HTMLElement; expect(indicator.classList.contains('log-state-indicator')).toBe(true); expect(indicator.classList.contains('isActive')).toBe(false); @@ -15,28 +18,30 @@ describe('LogStateIndicator', () => { }); it('renders correctly when isActive is true', () => { - const { container } = render(); + const { container } = render( + , + ); const indicator = container.firstChild as HTMLElement; expect(indicator.classList.contains('isActive')).toBe(true); }); it('renders correctly with different types', () => { const { container: containerInfo } = render( - , + , ); expect(containerInfo.querySelector('.line')?.classList.contains('INFO')).toBe( true, ); const { container: containerWarning } = render( - , + , ); expect( containerWarning.querySelector('.line')?.classList.contains('WARNING'), ).toBe(true); const { container: containerError } = render( - , + , ); expect( containerError.querySelector('.line')?.classList.contains('ERROR'), diff --git a/frontend/src/components/Logs/LogStateIndicator/LogStateIndicator.tsx b/frontend/src/components/Logs/LogStateIndicator/LogStateIndicator.tsx index ebad7bd116..b9afa5b7a2 100644 --- a/frontend/src/components/Logs/LogStateIndicator/LogStateIndicator.tsx +++ b/frontend/src/components/Logs/LogStateIndicator/LogStateIndicator.tsx @@ -1,6 +1,7 @@ import './LogStateIndicator.styles.scss'; import cx from 'classnames'; +import { FontSize } from 'container/OptionsMenu/types'; export const SEVERITY_TEXT_TYPE = { TRACE: 'TRACE', @@ -44,13 +45,15 @@ export const LogType = { function LogStateIndicator({ type, isActive, + fontSize, }: { type: string; + fontSize: FontSize; isActive?: boolean; }): JSX.Element { return (
-
+
); } diff --git a/frontend/src/components/Logs/RawLogView/index.tsx b/frontend/src/components/Logs/RawLogView/index.tsx index d1ae19fe99..b4b3eb7783 100644 --- a/frontend/src/components/Logs/RawLogView/index.tsx +++ b/frontend/src/components/Logs/RawLogView/index.tsx @@ -39,6 +39,7 @@ function RawLogView({ linesPerRow, isTextOverflowEllipsisDisabled, selectedFields = [], + fontSize, }: RawLogViewProps): JSX.Element { const { isHighlighted, isLogsExplorerPage, onLogCopy } = useCopyLogLink( data.id, @@ -168,6 +169,7 @@ function RawLogView({ activeContextLog?.id === data.id || isActiveLog } + fontSize={fontSize} /> diff --git a/frontend/src/components/Logs/RawLogView/styles.ts b/frontend/src/components/Logs/RawLogView/styles.ts index d86de435c2..35b853d929 100644 --- a/frontend/src/components/Logs/RawLogView/styles.ts +++ b/frontend/src/components/Logs/RawLogView/styles.ts @@ -1,6 +1,8 @@ +/* eslint-disable no-nested-ternary */ import { blue } from '@ant-design/colors'; import { Color } from '@signozhq/design-tokens'; import { Col, Row, Space } from 'antd'; +import { FontSize } from 'container/OptionsMenu/types'; import styled from 'styled-components'; import { getActiveLogBackground, getDefaultLogBackground } from 'utils/logs'; @@ -48,10 +50,15 @@ export const ExpandIconWrapper = styled(Col)` export const RawLogContent = styled.div` margin-bottom: 0; + display: flex !important; + align-items: center; font-family: 'SF Mono', monospace; font-family: 'Geist Mono'; font-size: 13px; font-weight: 400; + line-height: 24px; + letter-spacing: -0.07px; + padding: 4px; text-align: left; color: ${({ $isDarkMode }): string => $isDarkMode ? Color.BG_VANILLA_400 : Color.BG_INK_400}; @@ -66,9 +73,12 @@ export const RawLogContent = styled.div` line-clamp: ${linesPerRow}; -webkit-box-orient: vertical;`}; - line-height: 24px; - letter-spacing: -0.07px; - padding: 4px; + ${({ fontSize }): string => + fontSize === FontSize.SMALL + ? `font-size:11px; line-height:16px; padding:1px;` + : fontSize === FontSize.MEDIUM + ? `font-size:13px; line-height:20px; padding:1px;` + : `font-size:14px; line-height:24px; padding:2px;`} cursor: ${({ $isActiveLog, $isReadOnly }): string => $isActiveLog || $isReadOnly ? 'initial' : 'pointer'}; diff --git a/frontend/src/components/Logs/RawLogView/types.ts b/frontend/src/components/Logs/RawLogView/types.ts index a9c85c2ad6..ed73725dcc 100644 --- a/frontend/src/components/Logs/RawLogView/types.ts +++ b/frontend/src/components/Logs/RawLogView/types.ts @@ -1,3 +1,4 @@ +import { FontSize } from 'container/OptionsMenu/types'; import { IField } from 'types/api/logs/fields'; import { ILog } from 'types/api/logs/log'; @@ -7,11 +8,13 @@ export interface RawLogViewProps { isTextOverflowEllipsisDisabled?: boolean; data: ILog; linesPerRow: number; + fontSize: FontSize; selectedFields?: IField[]; } export interface RawLogContentProps { linesPerRow: number; + fontSize: FontSize; $isReadOnly?: boolean; $isActiveLog?: boolean; $isDarkMode?: boolean; diff --git a/frontend/src/components/Logs/TableView/styles.ts b/frontend/src/components/Logs/TableView/styles.ts index 9213021971..a79db04a76 100644 --- a/frontend/src/components/Logs/TableView/styles.ts +++ b/frontend/src/components/Logs/TableView/styles.ts @@ -1,7 +1,10 @@ +/* eslint-disable no-nested-ternary */ +import { FontSize } from 'container/OptionsMenu/types'; import styled from 'styled-components'; interface TableBodyContentProps { linesPerRow: number; + fontSize: FontSize; isDarkMode?: boolean; } @@ -20,4 +23,10 @@ export const TableBodyContent = styled.div` -webkit-line-clamp: ${(props): number => props.linesPerRow}; line-clamp: ${(props): number => props.linesPerRow}; -webkit-box-orient: vertical; + ${({ fontSize }): string => + fontSize === FontSize.SMALL + ? `font-size:11px; line-height:16px;` + : fontSize === FontSize.MEDIUM + ? `font-size:13px; line-height:20px;` + : `font-size:14px; line-height:24px;`} `; diff --git a/frontend/src/components/Logs/TableView/types.ts b/frontend/src/components/Logs/TableView/types.ts index 36a796ac0f..b2d3670dd8 100644 --- a/frontend/src/components/Logs/TableView/types.ts +++ b/frontend/src/components/Logs/TableView/types.ts @@ -1,4 +1,5 @@ import { ColumnsType, ColumnType } from 'antd/es/table'; +import { FontSize } from 'container/OptionsMenu/types'; import { IField } from 'types/api/logs/fields'; import { ILog } from 'types/api/logs/log'; @@ -10,6 +11,7 @@ export type LogsTableViewProps = { logs: ILog[]; fields: IField[]; linesPerRow: number; + fontSize: FontSize; onClickExpand?: (log: ILog) => void; }; diff --git a/frontend/src/components/Logs/TableView/useTableView.styles.scss b/frontend/src/components/Logs/TableView/useTableView.styles.scss index 3723ecc705..9592d0ae12 100644 --- a/frontend/src/components/Logs/TableView/useTableView.styles.scss +++ b/frontend/src/components/Logs/TableView/useTableView.styles.scss @@ -5,6 +5,21 @@ font-weight: 400; line-height: 18px; /* 128.571% */ letter-spacing: -0.07px; + + &.small { + font-size: 11px; + line-height: 16px; + } + + &.medium { + font-size: 13px; + line-height: 20px; + } + + &.large { + font-size: 14px; + line-height: 24px; + } } .table-timestamp { @@ -25,3 +40,21 @@ color: var(--bg-slate-400); } } + +.paragraph { + padding: 0px !important; + &.small { + font-size: 11px !important; + line-height: 16px !important; + } + + &.medium { + font-size: 13px !important; + line-height: 20px !important; + } + + &.large { + font-size: 14px !important; + line-height: 24px !important; + } +} diff --git a/frontend/src/components/Logs/TableView/useTableView.tsx b/frontend/src/components/Logs/TableView/useTableView.tsx index fd37132110..3a3ad54e3b 100644 --- a/frontend/src/components/Logs/TableView/useTableView.tsx +++ b/frontend/src/components/Logs/TableView/useTableView.tsx @@ -3,6 +3,7 @@ import './useTableView.styles.scss'; import Convert from 'ansi-to-html'; import { Typography } from 'antd'; import { ColumnsType } from 'antd/es/table'; +import cx from 'classnames'; import dayjs from 'dayjs'; import dompurify from 'dompurify'; import { useIsDarkMode } from 'hooks/useDarkMode'; @@ -31,6 +32,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => { logs, fields, linesPerRow, + fontSize, appendTo = 'center', activeContextLog, activeLog, @@ -57,7 +59,10 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => { : getDefaultCellStyle(isDarkMode), }, children: ( - + {field} ), @@ -87,8 +92,9 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => { isActive={ activeLog?.id === item.id || activeContextLog?.id === item.id } + fontSize={fontSize} /> - + {date}
@@ -114,6 +120,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => { }), ), }} + fontSize={fontSize} linesPerRow={linesPerRow} isDarkMode={isDarkMode} /> @@ -130,6 +137,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => { linesPerRow, activeLog?.id, activeContextLog?.id, + fontSize, ]); return { columns, dataSource: flattenLogData }; diff --git a/frontend/src/components/LogsFormatOptionsMenu/LogsFormatOptionsMenu.styles.scss b/frontend/src/components/LogsFormatOptionsMenu/LogsFormatOptionsMenu.styles.scss index af325a2d25..070d440781 100644 --- a/frontend/src/components/LogsFormatOptionsMenu/LogsFormatOptionsMenu.styles.scss +++ b/frontend/src/components/LogsFormatOptionsMenu/LogsFormatOptionsMenu.styles.scss @@ -17,17 +17,126 @@ box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2); backdrop-filter: blur(20px); + .font-size-dropdown { + display: flex; + flex-direction: column; + + .back-btn { + display: flex; + align-items: center; + gap: 6px; + padding: 12px; + border: none !important; + box-shadow: none !important; + + .icon { + flex-shrink: 0; + } + .text { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 13px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: 0.14px; + } + } + + .back-btn:hover { + background-color: unset !important; + } + + .content { + display: flex; + flex-direction: column; + .option-btn { + display: flex; + align-items: center; + padding: 12px; + border: none !important; + box-shadow: none !important; + justify-content: space-between; + + .icon { + flex-shrink: 0; + } + .text { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 13px; + font-style: normal; + font-weight: 400; + line-height: normal; /* 142.857% */ + letter-spacing: 0.14px; + text-transform: capitalize; + } + + .text:hover { + color: var(--bg-vanilla-300); + } + } + + .option-btn:hover { + background-color: unset !important; + } + } + } + + .font-size-container { + padding: 12px; + display: flex; + flex-direction: column; + gap: 12px; + + .title { + color: var(--bg-slate-50); + font-family: Inter; + font-size: 11px; + font-style: normal; + font-weight: 500; + line-height: 18px; /* 163.636% */ + letter-spacing: 0.88px; + text-transform: uppercase; + } + + .value { + display: flex; + height: 20px; + padding: 4px 0px; + justify-content: space-between; + align-items: center; + border: none !important; + .font-value { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 13px; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: 0.14px; + text-transform: capitalize; + } + .icon { + } + } + + .value:hover { + background-color: unset !important; + } + } + .menu-container { padding: 12px; .title { font-family: Inter; font-size: 11px; - font-weight: 600; + font-weight: 500; line-height: 18px; letter-spacing: 0.08em; text-align: left; - color: #52575c; + color: var(--bg-slate-50); } .menu-items { @@ -65,11 +174,11 @@ padding: 12px; .title { - color: #52575c; + color: var(--bg-slate-50); font-family: Inter; font-size: 11px; font-style: normal; - font-weight: 600; + font-weight: 500; line-height: 18px; /* 163.636% */ letter-spacing: 0.88px; text-transform: uppercase; @@ -149,11 +258,11 @@ } .title { - color: #52575c; + color: var(--bg-slate-50); font-family: Inter; font-size: 11px; font-style: normal; - font-weight: 600; + font-weight: 500; line-height: 18px; /* 163.636% */ letter-spacing: 0.88px; text-transform: uppercase; @@ -299,6 +408,38 @@ box-shadow: 4px 10px 16px 2px rgba(255, 255, 255, 0.2); + .font-size-dropdown { + .back-btn { + .text { + color: var(--bg-ink-400); + } + } + + .content { + .option-btn { + .text { + color: var(--bg-ink-400); + } + + .text:hover { + color: var(--bg-ink-300); + } + } + } + } + + .font-size-container { + .title { + color: var(--bg-ink-100); + } + + .value { + .font-value { + color: var(--bg-ink-400); + } + } + } + .horizontal-line { background: var(--bg-vanilla-300); } diff --git a/frontend/src/components/LogsFormatOptionsMenu/LogsFormatOptionsMenu.tsx b/frontend/src/components/LogsFormatOptionsMenu/LogsFormatOptionsMenu.tsx index 3a42e9a0b0..527c77c6af 100644 --- a/frontend/src/components/LogsFormatOptionsMenu/LogsFormatOptionsMenu.tsx +++ b/frontend/src/components/LogsFormatOptionsMenu/LogsFormatOptionsMenu.tsx @@ -3,12 +3,12 @@ /* eslint-disable jsx-a11y/click-events-have-key-events */ import './LogsFormatOptionsMenu.styles.scss'; -import { Divider, Input, InputNumber, Tooltip } from 'antd'; +import { Button, Divider, Input, InputNumber, Tooltip, Typography } from 'antd'; import cx from 'classnames'; import { LogViewMode } from 'container/LogsTable'; -import { OptionsMenuConfig } from 'container/OptionsMenu/types'; +import { FontSize, OptionsMenuConfig } from 'container/OptionsMenu/types'; import useDebouncedFn from 'hooks/useDebouncedFunction'; -import { Check, Minus, Plus, X } from 'lucide-react'; +import { Check, ChevronLeft, ChevronRight, Minus, Plus, X } from 'lucide-react'; import { useCallback, useEffect, useState } from 'react'; interface LogsFormatOptionsMenuProps { @@ -24,10 +24,16 @@ export default function LogsFormatOptionsMenu({ selectedOptionFormat, config, }: LogsFormatOptionsMenuProps): JSX.Element { - const { maxLines, format, addColumn } = config; + const { maxLines, format, addColumn, fontSize } = config; const [selectedItem, setSelectedItem] = useState(selectedOptionFormat); const maxLinesNumber = (maxLines?.value as number) || 1; const [maxLinesPerRow, setMaxLinesPerRow] = useState(maxLinesNumber); + const [fontSizeValue, setFontSizeValue] = useState( + fontSize?.value || FontSize.SMALL, + ); + const [isFontSizeOptionsOpen, setIsFontSizeOptionsOpen] = useState( + false, + ); const [addNewColumn, setAddNewColumn] = useState(false); @@ -88,6 +94,12 @@ export default function LogsFormatOptionsMenu({ } }, [maxLinesPerRow]); + useEffect(() => { + if (fontSizeValue && config && config.fontSize?.onChange) { + config.fontSize.onChange(fontSizeValue); + } + }, [fontSizeValue]); + return (
-
-
{title}
- -
- {items.map( - (item: any): JSX.Element => ( -
handleMenuItemClick(item.key)} - > -
- {item.label} - - {selectedItem === item.key && } -
-
- ), - )} + {isFontSizeOptionsOpen ? ( +
+ +
+
+ + + +
-
- - {selectedItem && ( + ) : ( <> - <> -
-
-
max lines per row
-
- - - -
-
- - -
- {!addNewColumn &&
} - - {addNewColumn && ( -
-
- {' '} - columns - {' '} -
+
+
Font Size
+ +
+
+
+
{title}
- -
- )} +
+ {items.map( + (item: any): JSX.Element => ( +
handleMenuItemClick(item.key)} + > +
+ {item.label} -
- {!addNewColumn && ( -
- columns - {' '} -
+ {selectedItem === item.key && } +
+
+ ), )} +
+
-
- {addColumn?.value?.map(({ key, id }) => ( -
-
- - {key} - -
- addColumn.onRemove(id as string)} + {selectedItem && ( + <> + <> +
+
+
max lines per row
+
+ + +
- ))} -
+
+ - {addColumn?.isFetching && ( -
Loading ...
- )} +
+ {!addNewColumn &&
} + + {addNewColumn && ( +
+
+ {' '} + columns + {' '} +
- {addNewColumn && - addColumn && - addColumn.value.length > 0 && - addColumn.options && - addColumn?.options?.length > 0 && ( - + +
)} - {addNewColumn && ( -
- {addColumn?.options?.map(({ label, value }) => ( -
{ - eve.stopPropagation(); - - if (addColumn && addColumn?.onSelect) { - addColumn?.onSelect(value, { label, disabled: false }); - } - }} - > -
- - {label} - +
+ {!addNewColumn && ( +
+ columns + {' '} +
+ )} + +
+ {addColumn?.value?.map(({ key, id }) => ( +
+
+ + {key} + +
+ addColumn.onRemove(id as string)} + />
+ ))} +
+ + {addColumn?.isFetching && ( +
Loading ...
+ )} + + {addNewColumn && + addColumn && + addColumn.value.length > 0 && + addColumn.options && + addColumn?.options?.length > 0 && ( + + )} + + {addNewColumn && ( +
+ {addColumn?.options?.map(({ label, value }) => ( +
{ + eve.stopPropagation(); + + if (addColumn && addColumn?.onSelect) { + addColumn?.onSelect(value, { label, disabled: false }); + } + }} + > +
+ + {label} + +
+
+ ))}
- ))} + )}
- )} -
-
+
+ + )} )}
diff --git a/frontend/src/container/LiveLogs/LiveLogsList/index.tsx b/frontend/src/container/LiveLogs/LiveLogsList/index.tsx index f872b55deb..50beda2953 100644 --- a/frontend/src/container/LiveLogs/LiveLogsList/index.tsx +++ b/frontend/src/container/LiveLogs/LiveLogsList/index.tsx @@ -63,6 +63,7 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element { data={log} linesPerRow={options.maxLines} selectedFields={selectedFields} + fontSize={options.fontSize} /> ); } @@ -75,12 +76,14 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element { linesPerRow={options.maxLines} onAddToQuery={onAddToQuery} onSetActiveLog={onSetActiveLog} + fontSize={options.fontSize} /> ); }, [ onAddToQuery, onSetActiveLog, + options.fontSize, options.format, options.maxLines, selectedFields, @@ -123,6 +126,7 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element { logs, fields: selectedFields, linesPerRow: options.maxLines, + fontSize: options.fontSize, appendTo: 'end', activeLogIndex, }} diff --git a/frontend/src/container/LogDetailedView/ContextView/ContextLogRenderer.tsx b/frontend/src/container/LogDetailedView/ContextView/ContextLogRenderer.tsx index d5c9f68547..39b55d21a0 100644 --- a/frontend/src/container/LogDetailedView/ContextView/ContextLogRenderer.tsx +++ b/frontend/src/container/LogDetailedView/ContextView/ContextLogRenderer.tsx @@ -3,12 +3,17 @@ import './ContextLogRenderer.styles.scss'; import { Skeleton } from 'antd'; import RawLogView from 'components/Logs/RawLogView'; import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar'; +import { LOCALSTORAGE } from 'constants/localStorage'; import ShowButton from 'container/LogsContextList/ShowButton'; +import { useOptionsMenu } from 'container/OptionsMenu'; +import { FontSize } from 'container/OptionsMenu/types'; import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config'; -import { useCallback, useEffect, useState } from 'react'; +import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { Virtuoso } from 'react-virtuoso'; import { ILog } from 'types/api/logs/log'; import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData'; +import { DataSource, StringOperators } from 'types/common/queryBuilder'; import { useContextLogData } from './useContextLogData'; @@ -22,6 +27,20 @@ function ContextLogRenderer({ const [afterLogPage, setAfterLogPage] = useState(1); const [logs, setLogs] = useState([log]); + const { initialDataSource, stagedQuery } = useQueryBuilder(); + + const listQuery = useMemo(() => { + if (!stagedQuery || stagedQuery.builder.queryData.length < 1) return null; + + return stagedQuery.builder.queryData.find((item) => !item.disabled) || null; + }, [stagedQuery]); + + const { options } = useOptionsMenu({ + storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS, + dataSource: initialDataSource || DataSource.METRICS, + aggregateOperator: listQuery?.aggregateOperator || StringOperators.NOOP, + }); + const { logs: previousLogs, isFetching: isPreviousLogsFetching, @@ -34,6 +53,7 @@ function ContextLogRenderer({ order: ORDERBY_FILTERS.ASC, page: prevLogPage, setPage: setPrevLogPage, + fontSize: options.fontSize, }); const { @@ -48,6 +68,7 @@ function ContextLogRenderer({ order: ORDERBY_FILTERS.DESC, page: afterLogPage, setPage: setAfterLogPage, + fontSize: options.fontSize, }); useEffect(() => { @@ -65,6 +86,19 @@ function ContextLogRenderer({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [filters]); + const lengthMultipier = useMemo(() => { + switch (options.fontSize) { + case FontSize.SMALL: + return 24; + case FontSize.MEDIUM: + return 28; + case FontSize.LARGE: + return 32; + default: + return 32; + } + }, [options.fontSize]); + const getItemContent = useCallback( (_: number, logTorender: ILog): JSX.Element => ( ), - [log.id], + [log.id, options.fontSize], ); return ( @@ -101,7 +136,7 @@ function ContextLogRenderer({ initialTopMostItemIndex={0} data={logs} itemContent={getItemContent} - style={{ height: `calc(${logs.length} * 32px)` }} + style={{ height: `calc(${logs.length} * ${lengthMultipier}px)` }} /> {isAfterLogsFetching && ( diff --git a/frontend/src/container/LogDetailedView/ContextView/useContextLogData.ts b/frontend/src/container/LogDetailedView/ContextView/useContextLogData.ts index 91c7fdf3f8..3d07ea0af9 100644 --- a/frontend/src/container/LogDetailedView/ContextView/useContextLogData.ts +++ b/frontend/src/container/LogDetailedView/ContextView/useContextLogData.ts @@ -4,9 +4,11 @@ import { PANEL_TYPES } from 'constants/queryBuilder'; import { getOrderByTimestamp, INITIAL_PAGE_SIZE, + INITIAL_PAGE_SIZE_SMALL_FONT, LOGS_MORE_PAGE_SIZE, } from 'container/LogsContextList/configs'; import { getRequestData } from 'container/LogsContextList/utils'; +import { FontSize } from 'container/OptionsMenu/types'; import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config'; import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange'; import { @@ -30,6 +32,7 @@ export const useContextLogData = ({ filters, page, setPage, + fontSize, }: { log: ILog; query: Query; @@ -38,6 +41,7 @@ export const useContextLogData = ({ filters: TagFilter | null; page: number; setPage: Dispatch>; + fontSize?: FontSize; }): { logs: ILog[]; handleShowNextLines: () => void; @@ -54,9 +58,14 @@ export const useContextLogData = ({ const logsMorePageSize = useMemo(() => (page - 1) * LOGS_MORE_PAGE_SIZE, [ page, ]); + + const initialPageSize = + fontSize && fontSize === FontSize.SMALL + ? INITIAL_PAGE_SIZE_SMALL_FONT + : INITIAL_PAGE_SIZE; const pageSize = useMemo( - () => (page <= 1 ? INITIAL_PAGE_SIZE : logsMorePageSize + INITIAL_PAGE_SIZE), - [page, logsMorePageSize], + () => (page <= 1 ? initialPageSize : logsMorePageSize + initialPageSize), + [page, initialPageSize, logsMorePageSize], ); const isDisabledFetch = useMemo(() => logs.length < pageSize, [ logs.length, @@ -77,8 +86,16 @@ export const useContextLogData = ({ log: lastLog, orderByTimestamp, page, + pageSize: initialPageSize, }), - [currentStagedQueryData, query, lastLog, orderByTimestamp, page], + [ + currentStagedQueryData, + query, + lastLog, + orderByTimestamp, + page, + initialPageSize, + ], ); const [requestData, setRequestData] = useState( diff --git a/frontend/src/container/LogDetailedView/LogContext.tsx b/frontend/src/container/LogDetailedView/LogContext.tsx index 90d4a6e5bb..6997c65171 100644 --- a/frontend/src/container/LogDetailedView/LogContext.tsx +++ b/frontend/src/container/LogDetailedView/LogContext.tsx @@ -2,6 +2,7 @@ import './LogContext.styles.scss'; import RawLogView from 'components/Logs/RawLogView'; import LogsContextList from 'container/LogsContextList'; +import { FontSize } from 'container/OptionsMenu/types'; import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config'; import { ILog } from 'types/api/logs/log'; import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData'; @@ -37,6 +38,7 @@ function LogContext({ isTextOverflowEllipsisDisabled={false} data={log} linesPerRow={1} + fontSize={FontSize.SMALL} /> {renderedField} diff --git a/frontend/src/container/LogsContextList/configs.ts b/frontend/src/container/LogsContextList/configs.ts index baa3b39420..9b70dfc5be 100644 --- a/frontend/src/container/LogsContextList/configs.ts +++ b/frontend/src/container/LogsContextList/configs.ts @@ -1,6 +1,7 @@ import { OrderByPayload } from 'types/api/queryBuilder/queryBuilderData'; export const INITIAL_PAGE_SIZE = 10; +export const INITIAL_PAGE_SIZE_SMALL_FONT = 12; export const LOGS_MORE_PAGE_SIZE = 10; export const getOrderByTimestamp = (order: string): OrderByPayload => ({ diff --git a/frontend/src/container/LogsContextList/index.tsx b/frontend/src/container/LogsContextList/index.tsx index 270291a33e..d215386c03 100644 --- a/frontend/src/container/LogsContextList/index.tsx +++ b/frontend/src/container/LogsContextList/index.tsx @@ -5,6 +5,7 @@ import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar'; import Spinner from 'components/Spinner'; import { DEFAULT_ENTITY_VERSION } from 'constants/app'; import { PANEL_TYPES } from 'constants/queryBuilder'; +import { FontSize } from 'container/OptionsMenu/types'; import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config'; import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange'; import { useIsDarkMode } from 'hooks/useDarkMode'; @@ -167,6 +168,7 @@ function LogsContextList({ key={log.id} data={log} linesPerRow={1} + fontSize={FontSize.SMALL} /> ), [], diff --git a/frontend/src/container/LogsExplorerContext/index.tsx b/frontend/src/container/LogsExplorerContext/index.tsx index d62cdb274b..32075097e5 100644 --- a/frontend/src/container/LogsExplorerContext/index.tsx +++ b/frontend/src/container/LogsExplorerContext/index.tsx @@ -2,6 +2,7 @@ import { EditFilled } from '@ant-design/icons'; import { Modal, Typography } from 'antd'; import RawLogView from 'components/Logs/RawLogView'; import LogsContextList from 'container/LogsContextList'; +import { FontSize } from 'container/OptionsMenu/types'; import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config'; import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch'; import { useIsDarkMode } from 'hooks/useDarkMode'; @@ -99,6 +100,7 @@ function LogsExplorerContext({ isTextOverflowEllipsisDisabled data={log} linesPerRow={1} + fontSize={FontSize.SMALL} /> void; logs: ILog[]; hasActions: boolean; + fontSize: FontSize; } export default function TableRow({ @@ -33,6 +35,7 @@ export default function TableRow({ handleSetActiveContextLog, logs, hasActions, + fontSize, }: TableRowProps): JSX.Element { const isDarkMode = useIsDarkMode(); @@ -78,6 +81,7 @@ export default function TableRow({ $isDragColumn={false} $isDarkMode={isDarkMode} key={column.key} + fontSize={fontSize} > {cloneElement(children, props)} diff --git a/frontend/src/container/LogsExplorerList/InfinityTableView/config.ts b/frontend/src/container/LogsExplorerList/InfinityTableView/config.ts index ec16ba1024..c235cbd5e7 100644 --- a/frontend/src/container/LogsExplorerList/InfinityTableView/config.ts +++ b/frontend/src/container/LogsExplorerList/InfinityTableView/config.ts @@ -1,3 +1,5 @@ +/* eslint-disable no-nested-ternary */ +import { FontSize } from 'container/OptionsMenu/types'; import { CSSProperties } from 'react'; export const infinityDefaultStyles: CSSProperties = { @@ -5,3 +7,16 @@ export const infinityDefaultStyles: CSSProperties = { overflowX: 'scroll', marginTop: '15px', }; + +export function getInfinityDefaultStyles(fontSize: FontSize): CSSProperties { + return { + width: '100%', + overflowX: 'scroll', + marginTop: + fontSize === FontSize.SMALL + ? '10px' + : fontSize === FontSize.MEDIUM + ? '12px' + : '15px', + }; +} diff --git a/frontend/src/container/LogsExplorerList/InfinityTableView/index.tsx b/frontend/src/container/LogsExplorerList/InfinityTableView/index.tsx index 2875575162..c678da49db 100644 --- a/frontend/src/container/LogsExplorerList/InfinityTableView/index.tsx +++ b/frontend/src/container/LogsExplorerList/InfinityTableView/index.tsx @@ -15,7 +15,7 @@ import { } from 'react-virtuoso'; import { ILog } from 'types/api/logs/log'; -import { infinityDefaultStyles } from './config'; +import { getInfinityDefaultStyles } from './config'; import { LogsCustomTable } from './LogsCustomTable'; import { TableHeaderCellStyled, TableRowStyled } from './styles'; import TableRow from './TableRow'; @@ -95,9 +95,15 @@ const InfinityTable = forwardRef( handleSetActiveContextLog={handleSetActiveContextLog} logs={tableViewProps.logs} hasActions + fontSize={tableViewProps.fontSize} /> ), - [handleSetActiveContextLog, tableColumns, tableViewProps.logs], + [ + handleSetActiveContextLog, + tableColumns, + tableViewProps.fontSize, + tableViewProps.logs, + ], ); const tableHeader = useCallback( @@ -112,6 +118,7 @@ const InfinityTable = forwardRef( $isDarkMode={isDarkMode} $isDragColumn={isDragColumn} key={column.key} + fontSize={tableViewProps?.fontSize} // eslint-disable-next-line react/jsx-props-no-spreading {...(isDragColumn && { className: 'dragHandler' })} > @@ -121,7 +128,7 @@ const InfinityTable = forwardRef( })} ), - [tableColumns, isDarkMode], + [tableColumns, isDarkMode, tableViewProps?.fontSize], ); const handleClickExpand = (index: number): void => { @@ -137,7 +144,7 @@ const InfinityTable = forwardRef( initialTopMostItemIndex={ tableViewProps.activeLogIndex !== -1 ? tableViewProps.activeLogIndex : 0 } - style={infinityDefaultStyles} + style={getInfinityDefaultStyles(tableViewProps.fontSize)} data={dataSource} components={{ // eslint-disable-next-line react/jsx-props-no-spreading diff --git a/frontend/src/container/LogsExplorerList/InfinityTableView/styles.ts b/frontend/src/container/LogsExplorerList/InfinityTableView/styles.ts index e2719dbc3f..89c9592dd4 100644 --- a/frontend/src/container/LogsExplorerList/InfinityTableView/styles.ts +++ b/frontend/src/container/LogsExplorerList/InfinityTableView/styles.ts @@ -1,5 +1,7 @@ +/* eslint-disable no-nested-ternary */ import { Color } from '@signozhq/design-tokens'; import { themeColors } from 'constants/theme'; +import { FontSize } from 'container/OptionsMenu/types'; import styled from 'styled-components'; import { getActiveLogBackground } from 'utils/logs'; @@ -7,6 +9,7 @@ interface TableHeaderCellStyledProps { $isDragColumn: boolean; $isDarkMode: boolean; $isTimestamp?: boolean; + fontSize?: FontSize; } export const TableStyled = styled.table` @@ -15,6 +18,14 @@ export const TableStyled = styled.table` export const TableCellStyled = styled.td` padding: 0.5rem; + ${({ fontSize }): string => + fontSize === FontSize.SMALL + ? `padding:0.3rem;` + : fontSize === FontSize.MEDIUM + ? `padding:0.4rem;` + : fontSize === FontSize.LARGE + ? `padding:0.5rem;` + : ``} background-color: ${(props): string => props.$isDarkMode ? 'inherit' : themeColors.whiteCream}; @@ -33,7 +44,7 @@ export const TableRowStyled = styled.tr<{ ? `background-color: ${ $isDarkMode ? Color.BG_SLATE_500 : Color.BG_VANILLA_300 } !important` - : ''} + : ''}; } cursor: pointer; @@ -66,9 +77,17 @@ export const TableHeaderCellStyled = styled.th` line-height: 18px; letter-spacing: -0.07px; background: ${(props): string => (props.$isDarkMode ? '#0b0c0d' : '#fdfdfd')}; - ${({ $isTimestamp }): string => ($isTimestamp ? 'padding-left: 24px;' : '')} ${({ $isDragColumn }): string => ($isDragColumn ? 'cursor: col-resize;' : '')} + ${({ fontSize }): string => + fontSize === FontSize.SMALL + ? `font-size:11px; line-height:16px; padding: 0.1rem;` + : fontSize === FontSize.MEDIUM + ? `font-size:13px; line-height:20px; padding:0.3rem;` + : fontSize === FontSize.LARGE + ? `font-size:14px; line-height:24px; padding: 0.5rem;` + : ``}; + ${({ $isTimestamp }): string => ($isTimestamp ? 'padding-left: 24px;' : '')} color: ${(props): string => props.$isDarkMode ? 'var(--bg-vanilla-100, #fff)' : themeColors.bckgGrey}; `; diff --git a/frontend/src/container/LogsExplorerList/index.tsx b/frontend/src/container/LogsExplorerList/index.tsx index 760f3fea30..18f6ba6d92 100644 --- a/frontend/src/container/LogsExplorerList/index.tsx +++ b/frontend/src/container/LogsExplorerList/index.tsx @@ -14,6 +14,7 @@ import EmptyLogsSearch from 'container/EmptyLogsSearch/EmptyLogsSearch'; import LogsError from 'container/LogsError/LogsError'; import { LogsLoading } from 'container/LogsLoading/LogsLoading'; import { useOptionsMenu } from 'container/OptionsMenu'; +import { FontSize } from 'container/OptionsMenu/types'; import { useActiveLog } from 'hooks/logs/useActiveLog'; import { useCopyLogLink } from 'hooks/logs/useCopyLogLink'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; @@ -79,6 +80,7 @@ function LogsExplorerList({ data={log} linesPerRow={options.maxLines} selectedFields={selectedFields} + fontSize={options.fontSize} /> ); } @@ -91,6 +93,7 @@ function LogsExplorerList({ onAddToQuery={onAddToQuery} onSetActiveLog={onSetActiveLog} activeLog={activeLog} + fontSize={options.fontSize} linesPerRow={options.maxLines} /> ); @@ -99,6 +102,7 @@ function LogsExplorerList({ activeLog, onAddToQuery, onSetActiveLog, + options.fontSize, options.format, options.maxLines, selectedFields, @@ -121,6 +125,7 @@ function LogsExplorerList({ logs, fields: selectedFields, linesPerRow: options.maxLines, + fontSize: options.fontSize, appendTo: 'end', activeLogIndex, }} @@ -129,9 +134,22 @@ function LogsExplorerList({ ); } + function getMarginTop(): string { + switch (options.fontSize) { + case FontSize.SMALL: + return '10px'; + case FontSize.MEDIUM: + return '12px'; + case FontSize.LARGE: + return '15px'; + default: + return '15px'; + } + } + return ( @@ -151,6 +169,7 @@ function LogsExplorerList({ isLoading, options.format, options.maxLines, + options.fontSize, activeLogIndex, logs, onEndReached, diff --git a/frontend/src/container/LogsTable/index.tsx b/frontend/src/container/LogsTable/index.tsx index 6b20986d1f..b7b9de84b9 100644 --- a/frontend/src/container/LogsTable/index.tsx +++ b/frontend/src/container/LogsTable/index.tsx @@ -10,6 +10,7 @@ import LogsTableView from 'components/Logs/TableView'; import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar'; import Spinner from 'components/Spinner'; import { CARD_BODY_STYLE } from 'constants/card'; +import { FontSize } from 'container/OptionsMenu/types'; import { useActiveLog } from 'hooks/logs/useActiveLog'; import { memo, useCallback, useMemo } from 'react'; import { useSelector } from 'react-redux'; @@ -66,6 +67,7 @@ function LogsTable(props: LogsTableProps): JSX.Element { data={log} linesPerRow={linesPerRow} selectedFields={selected} + fontSize={FontSize.SMALL} /> ); } @@ -78,6 +80,7 @@ function LogsTable(props: LogsTableProps): JSX.Element { linesPerRow={linesPerRow} onAddToQuery={onAddToQuery} onSetActiveLog={onSetActiveLog} + fontSize={FontSize.SMALL} /> ); }, @@ -92,6 +95,7 @@ function LogsTable(props: LogsTableProps): JSX.Element { logs={logs} fields={selected} linesPerRow={linesPerRow} + fontSize={FontSize.SMALL} /> ); } diff --git a/frontend/src/container/OptionsMenu/constants.ts b/frontend/src/container/OptionsMenu/constants.ts index 7a454de8ca..7b591cd4c5 100644 --- a/frontend/src/container/OptionsMenu/constants.ts +++ b/frontend/src/container/OptionsMenu/constants.ts @@ -1,6 +1,6 @@ import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; -import { OptionsQuery } from './types'; +import { FontSize, OptionsQuery } from './types'; export const URL_OPTIONS = 'options'; @@ -8,6 +8,7 @@ export const defaultOptionsQuery: OptionsQuery = { selectColumns: [], maxLines: 2, format: 'list', + fontSize: FontSize.SMALL, }; export const defaultTraceSelectedColumns = [ diff --git a/frontend/src/container/OptionsMenu/types.ts b/frontend/src/container/OptionsMenu/types.ts index 57b81364d6..2c57d66b28 100644 --- a/frontend/src/container/OptionsMenu/types.ts +++ b/frontend/src/container/OptionsMenu/types.ts @@ -2,10 +2,21 @@ import { InputNumberProps, RadioProps, SelectProps } from 'antd'; import { LogViewMode } from 'container/LogsTable'; import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; +export enum FontSize { + SMALL = 'small', + MEDIUM = 'medium', + LARGE = 'large', +} + +interface FontSizeProps { + value: FontSize; + onChange: (val: FontSize) => void; +} export interface OptionsQuery { selectColumns: BaseAutocompleteData[]; maxLines: number; format: LogViewMode; + fontSize: FontSize; } export interface InitialOptions @@ -18,6 +29,7 @@ export type OptionsMenuConfig = { onChange: (value: LogViewMode) => void; }; maxLines?: Pick; + fontSize?: FontSizeProps; addColumn?: Pick< SelectProps, 'options' | 'onSelect' | 'onFocus' | 'onSearch' | 'onBlur' diff --git a/frontend/src/container/OptionsMenu/useOptionsMenu.ts b/frontend/src/container/OptionsMenu/useOptionsMenu.ts index 97fbbbb006..6ed445a773 100644 --- a/frontend/src/container/OptionsMenu/useOptionsMenu.ts +++ b/frontend/src/container/OptionsMenu/useOptionsMenu.ts @@ -21,7 +21,12 @@ import { defaultTraceSelectedColumns, URL_OPTIONS, } from './constants'; -import { InitialOptions, OptionsMenuConfig, OptionsQuery } from './types'; +import { + FontSize, + InitialOptions, + OptionsMenuConfig, + OptionsQuery, +} from './types'; import { getOptionsFromKeys } from './utils'; interface UseOptionsMenuProps { @@ -248,6 +253,17 @@ const useOptionsMenu = ({ }, [handleRedirectWithOptionsData, optionsQueryData], ); + const handleFontSizeChange = useCallback( + (value: FontSize) => { + const optionsData: OptionsQuery = { + ...optionsQueryData, + fontSize: value, + }; + + handleRedirectWithOptionsData(optionsData); + }, + [handleRedirectWithOptionsData, optionsQueryData], + ); const handleSearchAttribute = useCallback((value: string) => { setSearchText(value); @@ -282,18 +298,24 @@ const useOptionsMenu = ({ value: optionsQueryData.maxLines || defaultOptionsQuery.maxLines, onChange: handleMaxLinesChange, }, + fontSize: { + value: optionsQueryData?.fontSize || defaultOptionsQuery.fontSize, + onChange: handleFontSizeChange, + }, }), [ - optionsFromAttributeKeys, - optionsQueryData?.maxLines, - optionsQueryData?.format, - optionsQueryData?.selectColumns, isSearchedAttributesFetching, - handleSearchAttribute, + optionsQueryData?.selectColumns, + optionsQueryData.format, + optionsQueryData.maxLines, + optionsQueryData?.fontSize, + optionsFromAttributeKeys, handleSelectColumns, handleRemoveSelectedColumn, + handleSearchAttribute, handleFormatChange, handleMaxLinesChange, + handleFontSizeChange, ], ); From ab1caf13fc201bf99e0317c2d5f445f485882b10 Mon Sep 17 00:00:00 2001 From: Vikrant Gupta Date: Thu, 22 Aug 2024 23:59:22 +0530 Subject: [PATCH 6/7] feat: add support for group by attribute in log details (#5753) * feat: add support for group by attribute in log details * feat: auto shift to qb from search on adding groupBY * feat: update icon and styles --- frontend/public/Icons/groupBy.svg | 1 + .../src/assets/CustomIcons/GroupByIcon.tsx | 27 +++ .../LogDetail/LogDetail.interfaces.ts | 6 + frontend/src/components/LogDetail/index.tsx | 2 + .../src/components/Logs/ListLogView/index.tsx | 2 + .../src/components/Logs/RawLogView/index.tsx | 2 + .../container/LiveLogs/LiveLogsList/index.tsx | 2 + .../container/LogDetailedView/Overview.tsx | 9 + .../LogDetailedView/TableView.styles.scss | 8 +- .../container/LogDetailedView/TableView.tsx | 106 +++--------- .../TableView/TableViewActions.styles.scss | 61 +++++++ .../TableView/TableViewActions.tsx | 156 ++++++++++++++++++ .../InfinityTableView/index.tsx | 3 + .../src/container/LogsExplorerList/index.tsx | 2 + .../src/container/LogsExplorerViews/index.tsx | 5 +- .../LogsPanelTable/LogsPanelComponent.tsx | 2 + frontend/src/container/LogsTable/index.tsx | 2 + .../Preview/components/LogsList/index.tsx | 2 + frontend/src/hooks/logs/types.ts | 5 + frontend/src/hooks/logs/useActiveLog.ts | 49 ++++++ frontend/src/pages/LogsExplorer/index.tsx | 19 ++- 21 files changed, 379 insertions(+), 92 deletions(-) create mode 100644 frontend/public/Icons/groupBy.svg create mode 100644 frontend/src/assets/CustomIcons/GroupByIcon.tsx create mode 100644 frontend/src/container/LogDetailedView/TableView/TableViewActions.styles.scss create mode 100644 frontend/src/container/LogDetailedView/TableView/TableViewActions.tsx diff --git a/frontend/public/Icons/groupBy.svg b/frontend/public/Icons/groupBy.svg new file mode 100644 index 0000000000..e668ef176a --- /dev/null +++ b/frontend/public/Icons/groupBy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/CustomIcons/GroupByIcon.tsx b/frontend/src/assets/CustomIcons/GroupByIcon.tsx new file mode 100644 index 0000000000..4cfceef3c8 --- /dev/null +++ b/frontend/src/assets/CustomIcons/GroupByIcon.tsx @@ -0,0 +1,27 @@ +import { Color } from '@signozhq/design-tokens'; +import { useIsDarkMode } from 'hooks/useDarkMode'; + +function GroupByIcon(): JSX.Element { + const isDarkMode = useIsDarkMode(); + return ( + + + + + + + + + + + + ); +} + +export default GroupByIcon; diff --git a/frontend/src/components/LogDetail/LogDetail.interfaces.ts b/frontend/src/components/LogDetail/LogDetail.interfaces.ts index 2a2dd56855..2c56d58fd1 100644 --- a/frontend/src/components/LogDetail/LogDetail.interfaces.ts +++ b/frontend/src/components/LogDetail/LogDetail.interfaces.ts @@ -3,12 +3,18 @@ import { AddToQueryHOCProps } from 'components/Logs/AddToQueryHOC'; import { ActionItemProps } from 'container/LogDetailedView/ActionItem'; import { IField } from 'types/api/logs/fields'; import { ILog } from 'types/api/logs/log'; +import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { VIEWS } from './constants'; export type LogDetailProps = { log: ILog | null; selectedTab: VIEWS; + onGroupByAttribute?: ( + fieldKey: string, + isJSON?: boolean, + dataType?: DataTypes, + ) => Promise; isListViewPanel?: boolean; listViewPanelSelectedFields?: IField[] | null; } & Pick & diff --git a/frontend/src/components/LogDetail/index.tsx b/frontend/src/components/LogDetail/index.tsx index 650d474736..5421672529 100644 --- a/frontend/src/components/LogDetail/index.tsx +++ b/frontend/src/components/LogDetail/index.tsx @@ -37,6 +37,7 @@ function LogDetail({ log, onClose, onAddToQuery, + onGroupByAttribute, onClickActionItem, selectedTab, isListViewPanel = false, @@ -209,6 +210,7 @@ function LogDetail({ logData={log} onAddToQuery={onAddToQuery} onClickActionItem={onClickActionItem} + onGroupByAttribute={onGroupByAttribute} isListViewPanel={isListViewPanel} selectedOptions={options} listViewPanelSelectedFields={listViewPanelSelectedFields} diff --git a/frontend/src/components/Logs/ListLogView/index.tsx b/frontend/src/components/Logs/ListLogView/index.tsx index d1461b0caa..34b3fddd19 100644 --- a/frontend/src/components/Logs/ListLogView/index.tsx +++ b/frontend/src/components/Logs/ListLogView/index.tsx @@ -141,6 +141,7 @@ function ListLogView({ onAddToQuery: handleAddToQuery, onSetActiveLog: handleSetActiveContextLog, onClearActiveLog: handleClearActiveContextLog, + onGroupByAttribute, } = useActiveLog(); const isDarkMode = useIsDarkMode(); @@ -257,6 +258,7 @@ function ListLogView({ onAddToQuery={handleAddToQuery} selectedTab={VIEW_TYPES.CONTEXT} onClose={handlerClearActiveContextLog} + onGroupByAttribute={onGroupByAttribute} /> )} diff --git a/frontend/src/components/Logs/RawLogView/index.tsx b/frontend/src/components/Logs/RawLogView/index.tsx index b4b3eb7783..935f423393 100644 --- a/frontend/src/components/Logs/RawLogView/index.tsx +++ b/frontend/src/components/Logs/RawLogView/index.tsx @@ -55,6 +55,7 @@ function RawLogView({ onSetActiveLog, onClearActiveLog, onAddToQuery, + onGroupByAttribute, } = useActiveLog(); const [hasActionButtons, setHasActionButtons] = useState(false); @@ -202,6 +203,7 @@ function RawLogView({ onClose={handleCloseLogDetail} onAddToQuery={onAddToQuery} onClickActionItem={onAddToQuery} + onGroupByAttribute={onGroupByAttribute} /> )} diff --git a/frontend/src/container/LiveLogs/LiveLogsList/index.tsx b/frontend/src/container/LiveLogs/LiveLogsList/index.tsx index 50beda2953..0be9334849 100644 --- a/frontend/src/container/LiveLogs/LiveLogsList/index.tsx +++ b/frontend/src/container/LiveLogs/LiveLogsList/index.tsx @@ -38,6 +38,7 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element { activeLog, onClearActiveLog, onAddToQuery, + onGroupByAttribute, onSetActiveLog, } = useActiveLog(); @@ -151,6 +152,7 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element { log={activeLog} onClose={onClearActiveLog} onAddToQuery={onAddToQuery} + onGroupByAttribute={onGroupByAttribute} onClickActionItem={onAddToQuery} /> diff --git a/frontend/src/container/LogDetailedView/Overview.tsx b/frontend/src/container/LogDetailedView/Overview.tsx index 9054217c33..1abfa5a526 100644 --- a/frontend/src/container/LogDetailedView/Overview.tsx +++ b/frontend/src/container/LogDetailedView/Overview.tsx @@ -18,6 +18,7 @@ import { ChevronDown, ChevronRight, Search } from 'lucide-react'; import { ReactNode, useState } from 'react'; import { IField } from 'types/api/logs/fields'; import { ILog } from 'types/api/logs/log'; +import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { ActionItemProps } from './ActionItem'; import TableView from './TableView'; @@ -27,6 +28,11 @@ interface OverviewProps { isListViewPanel?: boolean; selectedOptions: OptionsQuery; listViewPanelSelectedFields?: IField[] | null; + onGroupByAttribute?: ( + fieldKey: string, + isJSON?: boolean, + dataType?: DataTypes, + ) => Promise; } type Props = OverviewProps & @@ -39,6 +45,7 @@ function Overview({ onClickActionItem, isListViewPanel = false, selectedOptions, + onGroupByAttribute, listViewPanelSelectedFields, }: Props): JSX.Element { const [isWrapWord, setIsWrapWord] = useState(true); @@ -204,6 +211,7 @@ function Overview({ logData={logData} onAddToQuery={onAddToQuery} fieldSearchInput={fieldSearchInput} + onGroupByAttribute={onGroupByAttribute} onClickActionItem={onClickActionItem} isListViewPanel={isListViewPanel} selectedOptions={selectedOptions} @@ -222,6 +230,7 @@ function Overview({ Overview.defaultProps = { isListViewPanel: false, listViewPanelSelectedFields: null, + onGroupByAttribute: undefined, }; export default Overview; diff --git a/frontend/src/container/LogDetailedView/TableView.styles.scss b/frontend/src/container/LogDetailedView/TableView.styles.scss index 2f092dc04d..322a5cd638 100644 --- a/frontend/src/container/LogDetailedView/TableView.styles.scss +++ b/frontend/src/container/LogDetailedView/TableView.styles.scss @@ -11,7 +11,7 @@ top: 50%; right: 16px; transform: translateY(-50%); - gap: 8px; + gap: 4px; } } } @@ -76,8 +76,10 @@ box-shadow: none; border-radius: 2px; background: var(--bg-slate-400); - - height: 24px; + padding: 2px 3px; + gap: 3px; + height: 18px; + width: 20px; } } } diff --git a/frontend/src/container/LogDetailedView/TableView.tsx b/frontend/src/container/LogDetailedView/TableView.tsx index 0508701af5..591109ac3c 100644 --- a/frontend/src/container/LogDetailedView/TableView.tsx +++ b/frontend/src/container/LogDetailedView/TableView.tsx @@ -4,13 +4,12 @@ import './TableView.styles.scss'; import { LinkOutlined } from '@ant-design/icons'; import { Color } from '@signozhq/design-tokens'; -import { Button, Space, Spin, Tooltip, Tree, Typography } from 'antd'; +import { Button, Space, Tooltip, Typography } from 'antd'; import { ColumnsType } from 'antd/es/table'; import cx from 'classnames'; import AddToQueryHOC, { AddToQueryHOCProps, } from 'components/Logs/AddToQueryHOC'; -import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC'; import { ResizeTable } from 'components/ResizeTable'; import { OPERATORS } from 'constants/queryBuilder'; import ROUTES from 'constants/routes'; @@ -19,8 +18,7 @@ import { useIsDarkMode } from 'hooks/useDarkMode'; import history from 'lib/history'; import { fieldSearchFilter } from 'lib/logs/fieldSearch'; import { removeJSONStringifyQuotes } from 'lib/removeJSONStringifyQuotes'; -import { isEmpty } from 'lodash-es'; -import { ArrowDownToDot, ArrowUpFromDot, Pin } from 'lucide-react'; +import { Pin } from 'lucide-react'; import { useEffect, useMemo, useState } from 'react'; import { useDispatch } from 'react-redux'; import { generatePath } from 'react-router-dom'; @@ -29,17 +27,12 @@ import AppActions from 'types/actions'; import { SET_DETAILED_LOG_DATA } from 'types/actions/logs'; import { IField } from 'types/api/logs/fields'; import { ILog } from 'types/api/logs/log'; +import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { ActionItemProps } from './ActionItem'; import FieldRenderer from './FieldRenderer'; -import { - filterKeyForField, - findKeyPath, - flattenObject, - jsonToDataNodes, - recursiveParseJSON, - removeEscapeCharacters, -} from './utils'; +import { TableViewActions } from './TableView/TableViewActions'; +import { filterKeyForField, findKeyPath, flattenObject } from './utils'; // Fields which should be restricted from adding it to query const RESTRICTED_FIELDS = ['timestamp']; @@ -50,6 +43,11 @@ interface TableViewProps { selectedOptions: OptionsQuery; isListViewPanel?: boolean; listViewPanelSelectedFields?: IField[] | null; + onGroupByAttribute?: ( + fieldKey: string, + isJSON?: boolean, + dataType?: DataTypes, + ) => Promise; } type Props = TableViewProps & @@ -63,6 +61,7 @@ function TableView({ onClickActionItem, isListViewPanel = false, selectedOptions, + onGroupByAttribute, listViewPanelSelectedFields, }: Props): JSX.Element | null { const dispatch = useDispatch>(); @@ -271,75 +270,17 @@ function TableView({ width: 70, ellipsis: false, className: 'value-field-container attribute-value', - render: (fieldData: Record, record): JSX.Element => { - const textToCopy = fieldData.value.slice(1, -1); - - if (record.field === 'body') { - const parsedBody = recursiveParseJSON(fieldData.value); - if (!isEmpty(parsedBody)) { - return ( - - ); - } - } - - const fieldFilterKey = filterKeyForField(fieldData.field); - - return ( -
- - - {removeEscapeCharacters(fieldData.value)} - - - - {!isListViewPanel && ( - - -
- ); - }, + render: (fieldData: Record, record): JSX.Element => ( + + ), }, ]; function sortPinnedAttributes( @@ -380,9 +321,10 @@ function TableView({ TableView.defaultProps = { isListViewPanel: false, listViewPanelSelectedFields: null, + onGroupByAttribute: undefined, }; -interface DataType { +export interface DataType { key: string; field: string; value: string; diff --git a/frontend/src/container/LogDetailedView/TableView/TableViewActions.styles.scss b/frontend/src/container/LogDetailedView/TableView/TableViewActions.styles.scss new file mode 100644 index 0000000000..f5a45ef416 --- /dev/null +++ b/frontend/src/container/LogDetailedView/TableView/TableViewActions.styles.scss @@ -0,0 +1,61 @@ +.open-popover { + &.value-field { + .action-btn { + display: flex !important; + position: absolute !important; + top: 50% !important; + right: 16px !important; + transform: translateY(-50%) !important; + gap: 4px !important; + } + } +} + +.table-view-actions-content { + .ant-popover-inner { + border-radius: 4px; + border: 1px solid var(--bg-slate-400); + background: linear-gradient( + 139deg, + rgba(18, 19, 23, 0.8) 0%, + rgba(18, 19, 23, 0.9) 98.68% + ); + box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2); + backdrop-filter: blur(20px); + padding: 0px; + .group-by-clause { + display: flex; + align-items: center; + gap: 4px; + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: 0.14px; + padding: 12px 18px 12px 14px; + + .ant-btn-icon { + margin-inline-end: 0px; + } + } + + .group-by-clause:hover { + background-color: unset !important; + } + } +} + +.lightMode { + .table-view-actions-content { + .ant-popover-inner { + border: 1px solid var(--bg-vanilla-400); + background: var(--bg-vanilla-100) !important; + + .group-by-clause { + color: var(--bg-ink-400); + } + } + } +} diff --git a/frontend/src/container/LogDetailedView/TableView/TableViewActions.tsx b/frontend/src/container/LogDetailedView/TableView/TableViewActions.tsx new file mode 100644 index 0000000000..b239e64d3a --- /dev/null +++ b/frontend/src/container/LogDetailedView/TableView/TableViewActions.tsx @@ -0,0 +1,156 @@ +import './TableViewActions.styles.scss'; + +import { Color } from '@signozhq/design-tokens'; +import { Button, Popover, Spin, Tooltip, Tree } from 'antd'; +import GroupByIcon from 'assets/CustomIcons/GroupByIcon'; +import cx from 'classnames'; +import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC'; +import { OPERATORS } from 'constants/queryBuilder'; +import ROUTES from 'constants/routes'; +import { isEmpty } from 'lodash-es'; +import { ArrowDownToDot, ArrowUpFromDot, Ellipsis } from 'lucide-react'; +import { useMemo, useState } from 'react'; +import { useLocation } from 'react-router-dom'; +import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; + +import { DataType } from '../TableView'; +import { + filterKeyForField, + jsonToDataNodes, + recursiveParseJSON, + removeEscapeCharacters, +} from '../utils'; + +interface ITableViewActionsProps { + fieldData: Record; + record: DataType; + isListViewPanel: boolean; + isfilterInLoading: boolean; + isfilterOutLoading: boolean; + onGroupByAttribute?: ( + fieldKey: string, + isJSON?: boolean, + dataType?: DataTypes, + ) => Promise; + onClickHandler: ( + operator: string, + fieldKey: string, + fieldValue: string, + ) => () => void; +} + +export function TableViewActions( + props: ITableViewActionsProps, +): React.ReactElement { + const { + fieldData, + record, + isListViewPanel, + isfilterInLoading, + isfilterOutLoading, + onClickHandler, + onGroupByAttribute, + } = props; + + const { pathname } = useLocation(); + + // there is no option for where clause in old logs explorer and live logs page + const isOldLogsExplorerOrLiveLogsPage = useMemo( + () => pathname === ROUTES.OLD_LOGS_EXPLORER || pathname === ROUTES.LIVE_LOGS, + [pathname], + ); + + const [isOpen, setIsOpen] = useState(false); + const textToCopy = fieldData.value.slice(1, -1); + + if (record.field === 'body') { + const parsedBody = recursiveParseJSON(fieldData.value); + if (!isEmpty(parsedBody)) { + return ( + + ); + } + } + + const fieldFilterKey = filterKeyForField(fieldData.field); + + return ( +
+ + + {removeEscapeCharacters(fieldData.value)} + + + + {!isListViewPanel && ( + + + +
+ } + rootClassName="table-view-actions-content" + trigger="hover" + placement="bottomLeft" + > +
+ ); +} + +TableViewActions.defaultProps = { + onGroupByAttribute: undefined, +}; diff --git a/frontend/src/container/LogsExplorerList/InfinityTableView/index.tsx b/frontend/src/container/LogsExplorerList/InfinityTableView/index.tsx index c678da49db..fe2d2ba1d4 100644 --- a/frontend/src/container/LogsExplorerList/InfinityTableView/index.tsx +++ b/frontend/src/container/LogsExplorerList/InfinityTableView/index.tsx @@ -59,6 +59,7 @@ const InfinityTable = forwardRef( onSetActiveLog, onClearActiveLog, onAddToQuery, + onGroupByAttribute, } = useActiveLog(); const { dataSource, columns } = useTableView({ @@ -172,6 +173,7 @@ const InfinityTable = forwardRef( onClose={handleClearActiveContextLog} onAddToQuery={handleAddToQuery} selectedTab={VIEW_TYPES.CONTEXT} + onGroupByAttribute={onGroupByAttribute} /> )} ( onClose={onClearActiveLog} onAddToQuery={onAddToQuery} onClickActionItem={onAddToQuery} + onGroupByAttribute={onGroupByAttribute} /> ); diff --git a/frontend/src/container/LogsExplorerList/index.tsx b/frontend/src/container/LogsExplorerList/index.tsx index 18f6ba6d92..42cffb92cd 100644 --- a/frontend/src/container/LogsExplorerList/index.tsx +++ b/frontend/src/container/LogsExplorerList/index.tsx @@ -51,6 +51,7 @@ function LogsExplorerList({ activeLog, onClearActiveLog, onAddToQuery, + onGroupByAttribute, onSetActiveLog, } = useActiveLog(); @@ -208,6 +209,7 @@ function LogsExplorerList({ log={activeLog} onClose={onClearActiveLog} onAddToQuery={onAddToQuery} + onGroupByAttribute={onGroupByAttribute} onClickActionItem={onAddToQuery} /> diff --git a/frontend/src/container/LogsExplorerViews/index.tsx b/frontend/src/container/LogsExplorerViews/index.tsx index bc7002e7dc..bfe203d352 100644 --- a/frontend/src/container/LogsExplorerViews/index.tsx +++ b/frontend/src/container/LogsExplorerViews/index.tsx @@ -263,10 +263,7 @@ function LogsExplorerViews({ }, undefined, listQueryKeyRef, - { - ...(!isEmpty(queryId) && - selectedPanelType !== PANEL_TYPES.LIST && { 'X-SIGNOZ-QUERY-ID': queryId }), - }, + {}, ); const getRequestData = useCallback( diff --git a/frontend/src/container/LogsPanelTable/LogsPanelComponent.tsx b/frontend/src/container/LogsPanelTable/LogsPanelComponent.tsx index 3835386cd3..a7598b5c76 100644 --- a/frontend/src/container/LogsPanelTable/LogsPanelComponent.tsx +++ b/frontend/src/container/LogsPanelTable/LogsPanelComponent.tsx @@ -108,6 +108,7 @@ function LogsPanelComponent({ onSetActiveLog, onClearActiveLog, onAddToQuery, + onGroupByAttribute, } = useActiveLog(); const handleRow = useCallback( @@ -244,6 +245,7 @@ function LogsPanelComponent({ onClose={onClearActiveLog} onAddToQuery={onAddToQuery} onClickActionItem={onAddToQuery} + onGroupByAttribute={onGroupByAttribute} isListViewPanel listViewPanelSelectedFields={widget?.selectedLogFields} /> diff --git a/frontend/src/container/LogsTable/index.tsx b/frontend/src/container/LogsTable/index.tsx index b7b9de84b9..5717136304 100644 --- a/frontend/src/container/LogsTable/index.tsx +++ b/frontend/src/container/LogsTable/index.tsx @@ -36,6 +36,7 @@ function LogsTable(props: LogsTableProps): JSX.Element { activeLog, onClearActiveLog, onAddToQuery, + onGroupByAttribute, onSetActiveLog, } = useActiveLog(); @@ -130,6 +131,7 @@ function LogsTable(props: LogsTableProps): JSX.Element { selectedTab={VIEW_TYPES.OVERVIEW} log={activeLog} onClose={onClearActiveLog} + onGroupByAttribute={onGroupByAttribute} onAddToQuery={onAddToQuery} onClickActionItem={onAddToQuery} /> diff --git a/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/LogsList/index.tsx b/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/LogsList/index.tsx index f7d3af3a88..5bbe6de737 100644 --- a/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/LogsList/index.tsx +++ b/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/LogsList/index.tsx @@ -13,6 +13,7 @@ function LogsList({ logs }: LogsListProps): JSX.Element { onSetActiveLog, onClearActiveLog, onAddToQuery, + onGroupByAttribute, } = useActiveLog(); const makeLogDetailsHandler = (log: ILog) => (): void => onSetActiveLog(log); @@ -42,6 +43,7 @@ function LogsList({ logs }: LogsListProps): JSX.Element { onClose={onClearActiveLog} onAddToQuery={onAddToQuery} onClickActionItem={onAddToQuery} + onGroupByAttribute={onGroupByAttribute} />
); diff --git a/frontend/src/hooks/logs/types.ts b/frontend/src/hooks/logs/types.ts index 3dbcbb61f1..508c3254ee 100644 --- a/frontend/src/hooks/logs/types.ts +++ b/frontend/src/hooks/logs/types.ts @@ -28,4 +28,9 @@ export type UseActiveLog = { isJSON?: boolean, dataType?: DataTypes, ) => void; + onGroupByAttribute: ( + fieldKey: string, + isJSON?: boolean, + dataType?: DataTypes, + ) => Promise; }; diff --git a/frontend/src/hooks/logs/useActiveLog.ts b/frontend/src/hooks/logs/useActiveLog.ts index a56c13c72e..0a968c4650 100644 --- a/frontend/src/hooks/logs/useActiveLog.ts +++ b/frontend/src/hooks/logs/useActiveLog.ts @@ -128,6 +128,54 @@ export const useActiveLog = (): UseActiveLog => { [currentQuery, notifications, queryClient, redirectWithQueryBuilderData], ); + const onGroupByAttribute = useCallback( + async ( + fieldKey: string, + isJSON?: boolean, + dataType?: DataTypes, + ): Promise => { + try { + const keysAutocompleteResponse = await queryClient.fetchQuery( + [QueryBuilderKeys.GET_AGGREGATE_KEYS, fieldKey], + // eslint-disable-next-line sonarjs/no-identical-functions + async () => + getAggregateKeys({ + searchText: fieldKey, + aggregateOperator: currentQuery.builder.queryData[0].aggregateOperator, + dataSource: currentQuery.builder.queryData[0].dataSource, + aggregateAttribute: + currentQuery.builder.queryData[0].aggregateAttribute.key, + }), + ); + + const keysAutocomplete: BaseAutocompleteData[] = + keysAutocompleteResponse.payload?.attributeKeys || []; + + const existAutocompleteKey = chooseAutocompleteFromCustomValue( + keysAutocomplete, + fieldKey, + isJSON, + dataType, + ); + + const nextQuery: Query = { + ...currentQuery, + builder: { + ...currentQuery.builder, + queryData: currentQuery.builder.queryData.map((item) => ({ + ...item, + groupBy: [...item.groupBy, existAutocompleteKey], + })), + }, + }; + + redirectWithQueryBuilderData(nextQuery); + } catch { + notifications.error({ message: SOMETHING_WENT_WRONG }); + } + }, + [currentQuery, notifications, queryClient, redirectWithQueryBuilderData], + ); const onAddToQueryLogs = useCallback( (fieldKey: string, fieldValue: string, operator: string) => { const updatedQueryString = getGeneratedFilterQueryString( @@ -147,5 +195,6 @@ export const useActiveLog = (): UseActiveLog => { onSetActiveLog, onClearActiveLog, onAddToQuery: isLogsPage ? onAddToQueryLogs : onAddToQueryExplorer, + onGroupByAttribute, }; }; diff --git a/frontend/src/pages/LogsExplorer/index.tsx b/frontend/src/pages/LogsExplorer/index.tsx index 505851da10..0c7e33c52a 100644 --- a/frontend/src/pages/LogsExplorer/index.tsx +++ b/frontend/src/pages/LogsExplorer/index.tsx @@ -42,7 +42,13 @@ function LogsExplorer(): JSX.Element { if (currentQuery.builder.queryData.length > 1) { handleChangeSelectedView(SELECTED_VIEWS.QUERY_BUILDER); } - }, [currentQuery.builder.queryData.length]); + if ( + currentQuery.builder.queryData.length === 1 && + currentQuery.builder.queryData[0].groupBy.length > 0 + ) { + handleChangeSelectedView(SELECTED_VIEWS.QUERY_BUILDER); + } + }, [currentQuery.builder.queryData, currentQuery.builder.queryData.length]); const isMultipleQueries = useMemo( () => @@ -51,12 +57,19 @@ function LogsExplorer(): JSX.Element { [currentQuery], ); + const isGroupByPresent = useMemo( + () => + currentQuery.builder.queryData.length === 1 && + currentQuery.builder.queryData[0].groupBy.length > 0, + [currentQuery.builder.queryData], + ); + const toolbarViews = useMemo( () => ({ search: { name: 'search', label: 'Search', - disabled: isMultipleQueries, + disabled: isMultipleQueries || isGroupByPresent, show: true, }, queryBuilder: { @@ -72,7 +85,7 @@ function LogsExplorer(): JSX.Element { show: false, }, }), - [isMultipleQueries], + [isGroupByPresent, isMultipleQueries], ); return ( From 758b10f1bfb6b9b5bce98cecb357b472796b2bab Mon Sep 17 00:00:00 2001 From: Vikrant Gupta Date: Fri, 23 Aug 2024 00:54:30 +0530 Subject: [PATCH 7/7] fix: raw view css condense fix for line clamp (#5755) --- .../src/components/Logs/RawLogView/index.tsx | 1 + .../src/components/Logs/RawLogView/styles.ts | 16 +++++++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/Logs/RawLogView/index.tsx b/frontend/src/components/Logs/RawLogView/index.tsx index 935f423393..292d7e029a 100644 --- a/frontend/src/components/Logs/RawLogView/index.tsx +++ b/frontend/src/components/Logs/RawLogView/index.tsx @@ -162,6 +162,7 @@ function RawLogView({ $isActiveLog={isActiveLog} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} + fontSize={fontSize} > ` position: relative; width: 100%; @@ -24,6 +25,13 @@ export const RawLogViewContainer = styled(Row)<{ .log-state-indicator { margin: 4px 0; + + ${({ fontSize }): string => + fontSize === FontSize.SMALL + ? `margin: 1px 0;` + : fontSize === FontSize.MEDIUM + ? `margin: 1px 0;` + : `margin: 2px 0;`} } ${({ $isActiveLog }): string => getActiveLogBackground($isActiveLog)} @@ -50,13 +58,8 @@ export const ExpandIconWrapper = styled(Col)` export const RawLogContent = styled.div` margin-bottom: 0; - display: flex !important; - align-items: center; font-family: 'SF Mono', monospace; font-family: 'Geist Mono'; - font-size: 13px; - font-weight: 400; - line-height: 24px; letter-spacing: -0.07px; padding: 4px; text-align: left; @@ -73,6 +76,9 @@ export const RawLogContent = styled.div` line-clamp: ${linesPerRow}; -webkit-box-orient: vertical;`}; + font-size: 13px; + font-weight: 400; + line-height: 24px; ${({ fontSize }): string => fontSize === FontSize.SMALL ? `font-size:11px; line-height:16px; padding:1px;`