mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 01:28:59 +08:00
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
This commit is contained in:
parent
c322fc72d9
commit
bfeceb0ed2
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
28
pkg/http/middleware/cache.go
Normal file
28
pkg/http/middleware/cache.go
Normal file
@ -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)
|
||||
})
|
||||
}
|
56
pkg/http/middleware/cache_test.go
Normal file
56
pkg/http/middleware/cache_test.go
Normal file
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
29
pkg/web/config.go
Normal file
29
pkg/web/config.go
Normal file
@ -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
|
||||
}
|
1
pkg/web/testdata/index.html
vendored
Normal file
1
pkg/web/testdata/index.html
vendored
Normal file
@ -0,0 +1 @@
|
||||
<h1>Welcome to test data!!!</h1>
|
94
pkg/web/web.go
Normal file
94
pkg/web/web.go
Normal file
@ -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)
|
||||
}
|
159
pkg/web/web_test.go
Normal file
159
pkg/web/web_test.go
Normal file
@ -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)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user