Plugins WIP

This commit is contained in:
Mike Gibson
2025-07-31 08:05:37 +00:00
parent b6081fecd6
commit d5aaaeaacb
276 changed files with 9517 additions and 4077 deletions
+1 -49
View File
@@ -3,61 +3,13 @@ package httpserver
type Config struct {
LocalAddress string
GinMode string
Cors CorsConfig
Options []string
}
type CorsConfig struct {
// AllowedOrigins is a list of origins a cross-domain request can be executed from.
// If the special "*" value is present in the list, all origins will be allowed.
// An origin may contain a wildcard (*) to replace 0 or more characters
// (i.e.: http://*.domain.com). Usage of wildcards implies a small performance penalty.
// Only one wildcard can be used per origin.
// Default value is ["*"]
AllowedOrigins []string
// AllowedMethods is a list of methods the client is allowed to use with
// cross-domain requests. Default value is simple methods (HEAD, GET and POST).
AllowedMethods []string
// AllowedHeaders is list of non simple headers the client is allowed to use with
// cross-domain requests.
// If the special "*" value is present in the list, all headers will be allowed.
// Default value is [].
AllowedHeaders []string
// ExposedHeaders indicates which headers are safe to expose to the API of a CORS
// API specification
ExposedHeaders []string
// MaxAge indicates how long (in seconds) the results of a preflight request
// can be cached. Default value is 0, which stands for no
// Access-Control-Max-Age header to be sent back, resulting in browsers
// using their default value (5s by spec). If you need to force a 0 max-age,
// set `MaxAge` to a negative value (ie: -1).
MaxAge int
// AllowCredentials indicates whether the request can include user credentials like
// cookies, HTTP authentication or client side SSL certificates.
AllowCredentials bool
// AllowPrivateNetwork indicates whether to accept cross-origin requests over a
// private network.
AllowPrivateNetwork bool
// OptionsPassthrough instructs preflight to let other potential next handlers to
// process the OPTIONS method. Turn this on if your application handles OPTIONS.
OptionsPassthrough bool
// Provides a status code to use for successful OPTIONS requests.
// Default value is http.StatusNoContent (204).
OptionsSuccessStatus int
// Debugging flag adds additional output to debug server side CORS issues
Debug bool
}
func NewDefaultConfig() Config {
return Config{
LocalAddress: ":3333",
GinMode: "release",
// todo review
Cors: CorsConfig{
AllowedOrigins: []string{"*"},
AllowedHeaders: []string{"*"},
Debug: true,
},
Options: []string{"*"},
Options: []string{"*"},
}
}
-54
View File
@@ -1,54 +0,0 @@
package cors
import (
"time"
"github.com/bitmagnet-io/bitmagnet/internal/httpserver"
"github.com/gin-gonic/gin"
"github.com/rs/cors"
gincors "github.com/rs/cors/wrapper/gin"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
func New(
config httpserver.CorsConfig,
logger *zap.SugaredLogger,
) httpserver.Option {
return corsOption{gincors.New(cors.Options{
AllowedOrigins: config.AllowedOrigins,
AllowedMethods: config.AllowedMethods,
AllowedHeaders: config.AllowedHeaders,
ExposedHeaders: config.ExposedHeaders,
MaxAge: config.MaxAge,
AllowCredentials: config.AllowCredentials,
AllowPrivateNetwork: config.AllowPrivateNetwork,
OptionsPassthrough: config.OptionsPassthrough,
OptionsSuccessStatus: config.OptionsSuccessStatus,
Debug: config.Debug,
// we don't need every request logged so apply sampling
Logger: corsLogger{logger.WithOptions(zap.WrapCore(func(core zapcore.Core) zapcore.Core {
return zapcore.NewSamplerWithOptions(core, time.Hour, 10, 0)
})).Named("cors")},
})}
}
type corsOption struct {
handlerFunc gin.HandlerFunc
}
func (corsOption) Key() string {
return "cors"
}
func (c corsOption) Apply(g *gin.Engine) {
g.Use(c.handlerFunc)
}
type corsLogger struct {
logger *zap.SugaredLogger
}
func (c corsLogger) Printf(format string, v ...interface{}) {
c.logger.Debugf(format, v...)
}
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2017 gin-contrib
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-4
View File
@@ -1,4 +0,0 @@
// Copied from https://github.com/gin-contrib/zap
// In order to customize log level
package ginzap
-138
View File
@@ -1,138 +0,0 @@
package ginzap
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"go.uber.org/zap/zaptest/observer"
)
func init() {
gin.SetMode(gin.TestMode)
}
func buildDummyLogger() (*zap.Logger, *observer.ObservedLogs) {
core, obs := observer.New(zap.DebugLevel)
logger := zap.New(core)
return logger, obs
}
func timestampLocationCheck(timestampStr string, location *time.Location) error {
timestamp, err := time.Parse(time.RFC3339, timestampStr)
if err != nil {
return err
}
if timestamp.Location() != location {
return fmt.Errorf("timestamp should be utc but %v", timestamp.Location())
}
return nil
}
const testPath = "/test"
func TestGinzap(t *testing.T) {
t.Parallel()
r := gin.New()
utcLogger, utcLoggerObserved := buildDummyLogger()
r.Use(Ginzap(utcLogger, time.RFC3339, true))
localLogger, localLoggerObserved := buildDummyLogger()
r.Use(Ginzap(localLogger, time.RFC3339, false))
r.GET(testPath, func(c *gin.Context) {
c.JSON(204, nil)
})
res1 := httptest.NewRecorder()
ctx := t.Context()
req1, _ := http.NewRequestWithContext(ctx, http.MethodGet, testPath, nil)
r.ServeHTTP(res1, req1)
if len(utcLoggerObserved.All()) != 1 {
t.Fatalf("Log should be 1 line but there're %d", len(utcLoggerObserved.All()))
}
logLine := utcLoggerObserved.All()[0]
pathStr := logLine.Context[2].String
if pathStr != testPath {
t.Fatalf("logged path should be /test but %s", pathStr)
}
err := timestampLocationCheck(logLine.Context[7].String, time.UTC)
if err != nil {
t.Fatal(err)
}
if len(localLoggerObserved.All()) != 1 {
t.Fatalf("Log should be 1 line but there're %d", len(utcLoggerObserved.All()))
}
logLine = localLoggerObserved.All()[0]
pathStr = logLine.Context[2].String
if pathStr != testPath {
t.Fatalf("logged path should be /test but %s", pathStr)
}
}
func TestGinzapWithConfig(t *testing.T) {
t.Parallel()
r := gin.New()
utcLogger, utcLoggerObserved := buildDummyLogger()
r.Use(WithConfig(utcLogger, &Config{
TimeFormat: time.RFC3339,
UTC: true,
SkipPaths: []string{"/no_log"},
}))
r.GET(testPath, func(c *gin.Context) {
c.JSON(204, nil)
})
r.GET("/no_log", func(c *gin.Context) {
c.JSON(204, nil)
})
res1 := httptest.NewRecorder()
ctx := t.Context()
req1, _ := http.NewRequestWithContext(ctx, http.MethodGet, testPath, nil)
r.ServeHTTP(res1, req1)
res2 := httptest.NewRecorder()
req2, _ := http.NewRequestWithContext(ctx, http.MethodGet, "/no_log", nil)
r.ServeHTTP(res2, req2)
if res2.Code != 204 {
t.Fatalf("request /no_log is failed (%d)", res2.Code)
}
if len(utcLoggerObserved.All()) != 1 {
t.Fatalf("Log should be 1 line but there're %d", len(utcLoggerObserved.All()))
}
logLine := utcLoggerObserved.All()[0]
pathStr := logLine.Context[2].String
if pathStr != testPath {
t.Fatalf("logged path should be /test but %s", pathStr)
}
err := timestampLocationCheck(logLine.Context[7].String, time.UTC)
if err != nil {
t.Fatal(err)
}
}
-15
View File
@@ -1,15 +0,0 @@
package ginzap
import (
"time"
"github.com/bitmagnet-io/bitmagnet/internal/httpserver"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
func Option(logger *zap.Logger) httpserver.Option {
return httpserver.NewOption("logger", func(engine *gin.Engine) {
engine.Use(Ginzap(logger, time.RFC3339, true))
})
}
-138
View File
@@ -1,138 +0,0 @@
package httpserverfx
import (
"fmt"
"sort"
"time"
"github.com/bitmagnet-io/bitmagnet/internal/blocking"
"github.com/bitmagnet-io/bitmagnet/internal/config/configfx"
"github.com/bitmagnet-io/bitmagnet/internal/database"
"github.com/bitmagnet-io/bitmagnet/internal/health"
"github.com/bitmagnet-io/bitmagnet/internal/httpserver"
"github.com/bitmagnet-io/bitmagnet/internal/httpserver/circuitbreaker"
"github.com/bitmagnet-io/bitmagnet/internal/httpserver/cors"
"github.com/bitmagnet-io/bitmagnet/internal/httpserver/ginzap"
"github.com/bitmagnet-io/bitmagnet/internal/workers/registry"
"github.com/bitmagnet-io/bitmagnet/internal/workers/worker"
"github.com/gin-gonic/gin"
"go.uber.org/fx"
"go.uber.org/zap"
)
func New() fx.Option {
return fx.Module(
httpserver.Namespace,
configfx.NewConfigModule[httpserver.Config](httpserver.Namespace, httpserver.NewDefaultConfig()),
fx.Provide(
circuitbreaker.New,
fx.Annotate(
func(logger *zap.Logger) httpserver.Option {
return httpserver.NewOption("logger", func(engine *gin.Engine) {
engine.Use(ginzap.Ginzap(logger, time.RFC3339, true))
})
},
fx.ResultTags(`group:"http_server_options"`),
),
fx.Annotate(
func() httpserver.Option {
return httpserver.NewOption("recovery", func(engine *gin.Engine) {
engine.Use(gin.Recovery())
})
},
fx.ResultTags(`group:"http_server_options"`),
),
fx.Annotate(
func(
options []httpserver.Option,
config httpserver.Config,
) (gin.OptionFunc, error) {
return resolveOptions(options, config.Options)
},
fx.ParamTags(`group:"http_server_options"`),
),
fx.Annotate(
func(
config httpserver.Config,
handler circuitbreaker.Handler,
) (registry.Option, error) {
return registry.WithWorker(
httpserver.Namespace,
httpserver.New(handler, config.LocalAddress),
worker.WithDependencies(
database.Namespace,
blocking.Namespace,
health.Namespace,
),
), nil
},
fx.ResultTags(`group:"worker_options"`),
),
fx.Annotate(
func(
config httpserver.Config,
logger *zap.SugaredLogger,
) httpserver.Option {
return cors.New(config.Cors, logger)
},
fx.ResultTags(`group:"http_server_options"`),
),
),
fx.Invoke(
func(
config httpserver.Config,
option gin.OptionFunc,
handler circuitbreaker.Handler,
) error {
gin.SetMode(config.GinMode)
return handler.SetOption(option)
},
),
)
}
func resolveOptions(options []httpserver.Option, optionsNames []string) (gin.OptionFunc, error) {
paramMap := make(map[string]struct{})
for _, p := range optionsNames {
paramMap[p] = struct{}{}
}
enabledOptions := make([]httpserver.Option, 0, len(options))
foundMap := make(map[string]struct{}, len(options))
for _, o := range options {
if _, ok := foundMap[o.Key()]; ok {
return nil, fmt.Errorf("duplicate http server option: '%s'", o.Key())
}
foundMap[o.Key()] = struct{}{}
enabled := false
if _, ok := paramMap["*"]; ok {
enabled = true
} else if _, ok := paramMap[o.Key()]; ok {
enabled = true
}
if enabled {
enabledOptions = append(enabledOptions, o)
}
}
for p := range paramMap {
if _, ok := foundMap[p]; !ok && p != "*" {
return nil, fmt.Errorf("unknown http server option: '%s'", p)
}
}
sort.Slice(enabledOptions, func(i, j int) bool {
return enabledOptions[i].Key() < enabledOptions[j].Key()
})
return func(engine *gin.Engine) {
for _, opt := range enabledOptions {
opt.Apply(engine)
}
}, nil
}
-2
View File
@@ -8,8 +8,6 @@ import (
"github.com/bitmagnet-io/bitmagnet/internal/workers/runner"
)
const Namespace = "http_server"
func New(
handler http.Handler,
localAddress string,