diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index b1027e74..04d69c79 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -24,8 +24,7 @@ jobs:
run: go mod download
- name: Run tests
- run: go test -count=1 -coverprofile=coverage.txt ./... &&
- grep -v "^github.com/go-spring/spring-core/log" coverage.txt > coverage.txt.tmp && mv coverage.txt.tmp coverage.txt
+ run: go test -count=1 -coverprofile=coverage.txt ./...
- name: Upload results to Codecov
uses: codecov/codecov-action@v5
diff --git a/.gitignore b/.gitignore
index f60530d8..a9bb854a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -32,7 +32,9 @@ go.work.sum
/conf/remote/
-doc/examples/bookman/conf/
-doc/examples/bookman/log/*.log
+docs/**/bookman/conf/
+docs/**/bookman/log/*.log
-coverage.txt
\ No newline at end of file
+coverage.txt
+
+log/access.log
\ No newline at end of file
diff --git a/README.md b/README.md
index 60e01722..23f43907 100644
--- a/README.md
+++ b/README.md
@@ -4,10 +4,13 @@
-
+
+
+
+
-[中文](README_CN.md)
+[English](README.md) | [中文](README_CN.md)
**Go-Spring is a high-performance framework for modern Go application development, inspired by the Spring / Spring Boot
ecosystem in the Java community.**
diff --git a/README_CN.md b/README_CN.md
index 8f51b374..3d41ddfa 100644
--- a/README_CN.md
+++ b/README_CN.md
@@ -4,10 +4,13 @@
-
+
+
+
+
-[English](README.md)
+[English](README.md) | [中文](README_CN.md)
**Go-Spring 是一个面向现代 Go 应用开发的高性能框架,灵感源自 Java 社区的 Spring / Spring Boot。**
它的设计理念深度融合 Go 语言的特性,既保留了 Spring 世界中成熟的开发范式,如依赖注入(DI)、自动配置和生命周期管理,
diff --git a/conf/expr_test.go b/conf/expr_test.go
index 5c941a70..c05e215a 100644
--- a/conf/expr_test.go
+++ b/conf/expr_test.go
@@ -55,7 +55,7 @@ func TestExpr(t *testing.T) {
var v struct {
A int `value:"${a}" expr:"checkInt(2$)"`
}
- p := conf.Map(map[string]interface{}{
+ p := conf.Map(map[string]any{
"a": 4,
})
err := p.Bind(&v)
diff --git a/doc/examples/servers/gin/.keep b/doc/examples/servers/gin/.keep
deleted file mode 100644
index e69de29b..00000000
diff --git a/docs/0. overview/overview.md b/docs/0. overview/overview.md
new file mode 100644
index 00000000..5a0e6a0d
--- /dev/null
+++ b/docs/0. overview/overview.md
@@ -0,0 +1 @@
+todo 项目概览、目标、特性,解决了什么问题
\ No newline at end of file
diff --git a/docs/1. getting-started/getting-started.md b/docs/1. getting-started/getting-started.md
new file mode 100644
index 00000000..66818101
--- /dev/null
+++ b/docs/1. getting-started/getting-started.md
@@ -0,0 +1 @@
+todo 快速开始、安装、HelloWorld、项目结构引导
\ No newline at end of file
diff --git a/docs/2. concepts/concepts.md b/docs/2. concepts/concepts.md
new file mode 100644
index 00000000..15bcb69a
--- /dev/null
+++ b/docs/2. concepts/concepts.md
@@ -0,0 +1 @@
+todo 核心架构理念、DI、配置系统、生命周期管理等
\ No newline at end of file
diff --git a/docs/3. guides/guides.md b/docs/3. guides/guides.md
new file mode 100644
index 00000000..2bbc33ce
--- /dev/null
+++ b/docs/3. guides/guides.md
@@ -0,0 +1 @@
+todo 常见操作,如创建服务、注入 Bean、读取配置、测试
\ No newline at end of file
diff --git a/doc/examples/bookman/README.md b/docs/4. examples/bookman/README.md
similarity index 100%
rename from doc/examples/bookman/README.md
rename to docs/4. examples/bookman/README.md
diff --git a/doc/examples/bookman/README_CN.md b/docs/4. examples/bookman/README_CN.md
similarity index 100%
rename from doc/examples/bookman/README_CN.md
rename to docs/4. examples/bookman/README_CN.md
diff --git a/doc/examples/bookman/conf/app-test.properties b/docs/4. examples/bookman/conf/app-test.properties
similarity index 100%
rename from doc/examples/bookman/conf/app-test.properties
rename to docs/4. examples/bookman/conf/app-test.properties
diff --git a/doc/examples/bookman/conf/app.properties b/docs/4. examples/bookman/conf/app.properties
similarity index 100%
rename from doc/examples/bookman/conf/app.properties
rename to docs/4. examples/bookman/conf/app.properties
diff --git a/doc/examples/bookman/go.mod b/docs/4. examples/bookman/go.mod
similarity index 100%
rename from doc/examples/bookman/go.mod
rename to docs/4. examples/bookman/go.mod
diff --git a/doc/examples/bookman/go.sum b/docs/4. examples/bookman/go.sum
similarity index 100%
rename from doc/examples/bookman/go.sum
rename to docs/4. examples/bookman/go.sum
diff --git a/doc/examples/bookman/init.go b/docs/4. examples/bookman/init.go
similarity index 100%
rename from doc/examples/bookman/init.go
rename to docs/4. examples/bookman/init.go
diff --git a/doc/examples/bookman/log/.keep b/docs/4. examples/bookman/log/.keep
similarity index 100%
rename from doc/examples/bookman/log/.keep
rename to docs/4. examples/bookman/log/.keep
diff --git a/doc/examples/bookman/main.go b/docs/4. examples/bookman/main.go
similarity index 100%
rename from doc/examples/bookman/main.go
rename to docs/4. examples/bookman/main.go
diff --git a/doc/examples/bookman/public/index.html b/docs/4. examples/bookman/public/index.html
similarity index 100%
rename from doc/examples/bookman/public/index.html
rename to docs/4. examples/bookman/public/index.html
diff --git a/doc/examples/bookman/src/app/app.go b/docs/4. examples/bookman/src/app/app.go
similarity index 100%
rename from doc/examples/bookman/src/app/app.go
rename to docs/4. examples/bookman/src/app/app.go
diff --git a/doc/examples/bookman/src/app/bootstrap/bootstrap.go b/docs/4. examples/bookman/src/app/bootstrap/bootstrap.go
similarity index 100%
rename from doc/examples/bookman/src/app/bootstrap/bootstrap.go
rename to docs/4. examples/bookman/src/app/bootstrap/bootstrap.go
diff --git a/doc/examples/bookman/src/app/common/handlers/log/log.go b/docs/4. examples/bookman/src/app/common/handlers/log/log.go
similarity index 100%
rename from doc/examples/bookman/src/app/common/handlers/log/log.go
rename to docs/4. examples/bookman/src/app/common/handlers/log/log.go
diff --git a/doc/examples/bookman/src/app/common/httpsvr/httpsvr.go b/docs/4. examples/bookman/src/app/common/httpsvr/httpsvr.go
similarity index 100%
rename from doc/examples/bookman/src/app/common/httpsvr/httpsvr.go
rename to docs/4. examples/bookman/src/app/common/httpsvr/httpsvr.go
diff --git a/doc/examples/bookman/src/app/controller/controller-book.go b/docs/4. examples/bookman/src/app/controller/controller-book.go
similarity index 100%
rename from doc/examples/bookman/src/app/controller/controller-book.go
rename to docs/4. examples/bookman/src/app/controller/controller-book.go
diff --git a/doc/examples/bookman/src/app/controller/controller.go b/docs/4. examples/bookman/src/app/controller/controller.go
similarity index 100%
rename from doc/examples/bookman/src/app/controller/controller.go
rename to docs/4. examples/bookman/src/app/controller/controller.go
diff --git a/doc/examples/bookman/src/biz/biz.go b/docs/4. examples/bookman/src/biz/biz.go
similarity index 100%
rename from doc/examples/bookman/src/biz/biz.go
rename to docs/4. examples/bookman/src/biz/biz.go
diff --git a/doc/examples/bookman/src/biz/job/job.go b/docs/4. examples/bookman/src/biz/job/job.go
similarity index 100%
rename from doc/examples/bookman/src/biz/job/job.go
rename to docs/4. examples/bookman/src/biz/job/job.go
diff --git a/doc/examples/bookman/src/biz/service/book_service/book_service.go b/docs/4. examples/bookman/src/biz/service/book_service/book_service.go
similarity index 100%
rename from doc/examples/bookman/src/biz/service/book_service/book_service.go
rename to docs/4. examples/bookman/src/biz/service/book_service/book_service.go
diff --git a/doc/examples/bookman/src/biz/service/book_service/book_service_test.go b/docs/4. examples/bookman/src/biz/service/book_service/book_service_test.go
similarity index 100%
rename from doc/examples/bookman/src/biz/service/book_service/book_service_test.go
rename to docs/4. examples/bookman/src/biz/service/book_service/book_service_test.go
diff --git a/doc/examples/bookman/src/dao/book_dao/book_dao.go b/docs/4. examples/bookman/src/dao/book_dao/book_dao.go
similarity index 100%
rename from doc/examples/bookman/src/dao/book_dao/book_dao.go
rename to docs/4. examples/bookman/src/dao/book_dao/book_dao.go
diff --git a/doc/examples/bookman/src/dao/book_dao/book_dao_test.go b/docs/4. examples/bookman/src/dao/book_dao/book_dao_test.go
similarity index 100%
rename from doc/examples/bookman/src/dao/book_dao/book_dao_test.go
rename to docs/4. examples/bookman/src/dao/book_dao/book_dao_test.go
diff --git a/doc/examples/bookman/src/idl/http/proto/proto.go b/docs/4. examples/bookman/src/idl/http/proto/proto.go
similarity index 100%
rename from doc/examples/bookman/src/idl/http/proto/proto.go
rename to docs/4. examples/bookman/src/idl/http/proto/proto.go
diff --git a/doc/examples/bookman/src/sdk/book_sdk/book_sdk.go b/docs/4. examples/bookman/src/sdk/book_sdk/book_sdk.go
similarity index 100%
rename from doc/examples/bookman/src/sdk/book_sdk/book_sdk.go
rename to docs/4. examples/bookman/src/sdk/book_sdk/book_sdk.go
diff --git a/docs/4. examples/chatAI/chatAI.html b/docs/4. examples/chatAI/chatAI.html
new file mode 100644
index 00000000..6afd2246
--- /dev/null
+++ b/docs/4. examples/chatAI/chatAI.html
@@ -0,0 +1,181 @@
+
+
+
+
+ Chat AI
+
+
+
+
+
🧠 Chat AI
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/4. examples/chatAI/go.mod b/docs/4. examples/chatAI/go.mod
new file mode 100644
index 00000000..bc244dac
--- /dev/null
+++ b/docs/4. examples/chatAI/go.mod
@@ -0,0 +1,15 @@
+module chatai
+
+go 1.24
+
+require github.com/go-spring/spring-core v0.0.0
+
+require (
+ github.com/expr-lang/expr v1.17.2 // indirect
+ github.com/magiconair/properties v1.8.10 // indirect
+ github.com/pelletier/go-toml v1.9.5 // indirect
+ github.com/spf13/cast v1.7.1 // indirect
+ gopkg.in/yaml.v2 v2.4.0 // indirect
+)
+
+replace github.com/go-spring/spring-core => ../../../
diff --git a/doc/examples/miniapi/go.sum b/docs/4. examples/chatAI/go.sum
similarity index 100%
rename from doc/examples/miniapi/go.sum
rename to docs/4. examples/chatAI/go.sum
diff --git a/docs/4. examples/chatAI/main.go b/docs/4. examples/chatAI/main.go
new file mode 100644
index 00000000..d6853c7e
--- /dev/null
+++ b/docs/4. examples/chatAI/main.go
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2025 The Go-Spring Authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package main
+
+import (
+ "embed"
+ "fmt"
+ "net/http"
+ "time"
+
+ "github.com/go-spring/spring-core/gs"
+ "github.com/go-spring/spring-core/util/sysconf"
+)
+
+//go:embed chatAI.html
+var files embed.FS
+
+func main() {
+ // Disable the write timeout for the HTTP server
+ sysconf.Set("http.server.writeTimeout", "0")
+
+ // Serve static files from the embedded file system under the "/public/" path
+ http.Handle("/public/", http.StripPrefix("/public/", http.FileServer(http.FS(files))))
+
+ // Handle the Server-Sent Events (SSE) endpoint
+ http.HandleFunc("/chat/sse", func(w http.ResponseWriter, r *http.Request) {
+
+ // Set the necessary HTTP headers for SSE
+ w.Header().Set("Content-Type", "text/event-stream")
+ w.Header().Set("Cache-Control", "no-cache")
+ w.Header().Set("Connection", "keep-alive")
+ w.Header().Set("Access-Control-Allow-Origin", "*")
+
+ flusher, ok := w.(http.Flusher)
+ if !ok {
+ http.Error(w, "Streaming unsupported!", http.StatusInternalServerError)
+ return
+ }
+
+ // Send an SSE message every second for 10 seconds
+ for i := 0; i < 10; i++ {
+ select {
+ case <-r.Context().Done():
+ // Exit the loop if the client disconnects
+ return
+ default:
+ // Each SSE message must end with two newlines to be recognized correctly by the client
+ // See more about SSE protocol: https://www.ruanyifeng.com/blog/2017/05/server-sent_events.html
+ fmt.Fprintf(w, "data: Message %d at %s\n\n", i, time.Now().Format("15:04:05"))
+ flusher.Flush()
+ time.Sleep(1 * time.Second)
+ }
+ }
+ })
+
+ gs.Run()
+}
+
+// open http://127.0.0.1:9090/public/chatAI.html in the browser
diff --git a/docs/4. examples/examples.md b/docs/4. examples/examples.md
new file mode 100644
index 00000000..76a69a37
--- /dev/null
+++ b/docs/4. examples/examples.md
@@ -0,0 +1 @@
+todo 完整的 demo 项目或代码片段
\ No newline at end of file
diff --git a/doc/examples/miniapi/go.mod b/docs/4. examples/miniapi/go.mod
similarity index 100%
rename from doc/examples/miniapi/go.mod
rename to docs/4. examples/miniapi/go.mod
diff --git a/doc/examples/noweb/go.sum b/docs/4. examples/miniapi/go.sum
similarity index 100%
rename from doc/examples/noweb/go.sum
rename to docs/4. examples/miniapi/go.sum
diff --git a/doc/examples/miniapi/main.go b/docs/4. examples/miniapi/main.go
similarity index 89%
rename from doc/examples/miniapi/main.go
rename to docs/4. examples/miniapi/main.go
index ef26a211..ca816b33 100644
--- a/doc/examples/miniapi/main.go
+++ b/docs/4. examples/miniapi/main.go
@@ -17,9 +17,11 @@
package main
import (
+ "context"
"net/http"
"github.com/go-spring/spring-core/gs"
+ "github.com/go-spring/spring-core/util/syslog"
)
func main() {
@@ -34,7 +36,10 @@ func main() {
// - Property Binding: Binds external configs (YAML, ENV) into structs.
// - Dependency Injection: Wires beans automatically.
// - Dynamic Refresh: Updates configs at runtime without restart.
- gs.Run()
+ gs.RunWith(func(ctx context.Context) error {
+ syslog.Infof("app started")
+ return nil
+ })
}
//~ curl http://127.0.0.1:9090/echo
diff --git a/doc/examples/noweb/go.mod b/docs/4. examples/noweb/go.mod
similarity index 100%
rename from doc/examples/noweb/go.mod
rename to docs/4. examples/noweb/go.mod
diff --git a/doc/examples/startup/go.sum b/docs/4. examples/noweb/go.sum
similarity index 100%
rename from doc/examples/startup/go.sum
rename to docs/4. examples/noweb/go.sum
diff --git a/doc/examples/noweb/main.go b/docs/4. examples/noweb/main.go
similarity index 78%
rename from doc/examples/noweb/main.go
rename to docs/4. examples/noweb/main.go
index 79984432..20e5f0c9 100644
--- a/doc/examples/noweb/main.go
+++ b/docs/4. examples/noweb/main.go
@@ -17,12 +17,23 @@
package main
import (
+ "time"
+
"github.com/go-spring/spring-core/gs"
+ "github.com/go-spring/spring-core/util/syslog"
)
func main() {
// Disable the built-in HTTP service.
- gs.Web(false).Run()
+ stopApp, err := gs.Web(false).RunAsync()
+ if err != nil {
+ syslog.Errorf("app run failed: %s", err.Error())
+ }
+
+ syslog.Infof("app started")
+ time.Sleep(time.Minute)
+
+ stopApp()
}
// ~ telnet 127.0.0.1 9090
diff --git a/docs/4. examples/servers/gin/go.mod b/docs/4. examples/servers/gin/go.mod
new file mode 100644
index 00000000..e32d56a4
--- /dev/null
+++ b/docs/4. examples/servers/gin/go.mod
@@ -0,0 +1,44 @@
+module ginsvr
+
+go 1.24
+
+require (
+ github.com/gin-gonic/gin v1.10.1
+ github.com/go-spring/spring-core v0.0.0
+)
+
+require (
+ github.com/bytedance/sonic v1.13.2 // indirect
+ github.com/bytedance/sonic/loader v0.2.4 // indirect
+ github.com/cloudwego/base64x v0.1.5 // indirect
+ github.com/expr-lang/expr v1.17.2 // indirect
+ github.com/gabriel-vasile/mimetype v1.4.9 // indirect
+ github.com/gin-contrib/sse v1.1.0 // indirect
+ github.com/go-playground/locales v0.14.1 // indirect
+ github.com/go-playground/universal-translator v0.18.1 // indirect
+ github.com/go-playground/validator/v10 v10.26.0 // indirect
+ github.com/goccy/go-json v0.10.5 // indirect
+ github.com/google/go-cmp v0.6.0 // indirect
+ github.com/json-iterator/go v1.1.12 // indirect
+ github.com/klauspost/cpuid/v2 v2.2.10 // indirect
+ github.com/leodido/go-urn v1.4.0 // indirect
+ github.com/magiconair/properties v1.8.10 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+ github.com/modern-go/reflect2 v1.0.2 // indirect
+ github.com/pelletier/go-toml v1.9.5 // indirect
+ github.com/pelletier/go-toml/v2 v2.2.4 // indirect
+ github.com/spf13/cast v1.7.1 // indirect
+ github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
+ github.com/ugorji/go/codec v1.2.14 // indirect
+ golang.org/x/arch v0.17.0 // indirect
+ golang.org/x/crypto v0.38.0 // indirect
+ golang.org/x/net v0.40.0 // indirect
+ golang.org/x/sys v0.33.0 // indirect
+ golang.org/x/text v0.25.0 // indirect
+ google.golang.org/protobuf v1.36.6 // indirect
+ gopkg.in/yaml.v2 v2.4.0 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
+
+replace github.com/go-spring/spring-core => ../../../../
diff --git a/docs/4. examples/servers/gin/go.sum b/docs/4. examples/servers/gin/go.sum
new file mode 100644
index 00000000..17da0161
--- /dev/null
+++ b/docs/4. examples/servers/gin/go.sum
@@ -0,0 +1,104 @@
+github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
+github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
+github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
+github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
+github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
+github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
+github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
+github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/expr-lang/expr v1.17.2 h1:o0A99O/Px+/DTjEnQiodAgOIK9PPxL8DtXhBRKC+Iso=
+github.com/expr-lang/expr v1.17.2/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
+github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
+github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
+github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
+github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
+github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
+github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
+github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
+github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
+github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
+github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
+github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
+github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
+github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
+github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
+github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
+github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
+github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
+github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
+github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
+github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
+github.com/lvan100/go-assert v0.0.2 h1:K1G++zfdM5h+1Q/hSctEEqqcJIOs327k2kLiO3MmE5E=
+github.com/lvan100/go-assert v0.0.2/go.mod h1:osFFuU9zt4/SdTaJ9uU3y9qabAFDYlaH4Yte/ndDAj4=
+github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
+github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
+github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
+github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
+github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
+github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
+github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
+github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
+github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
+github.com/ugorji/go/codec v1.2.14 h1:yOQvXCBc3Ij46LRkRoh4Yd5qK6LVOgi0bYOXfb7ifjw=
+github.com/ugorji/go/codec v1.2.14/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
+go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
+go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
+golang.org/x/arch v0.17.0 h1:4O3dfLzd+lQewptAHqjewQZQDyEdejz3VwgeYwkZneU=
+golang.org/x/arch v0.17.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
+golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
+golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
+golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
+golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
+golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
+golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
+google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
diff --git a/docs/4. examples/servers/gin/main.go b/docs/4. examples/servers/gin/main.go
new file mode 100644
index 00000000..0b0fe204
--- /dev/null
+++ b/docs/4. examples/servers/gin/main.go
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2025 The Go-Spring Authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package main
+
+import (
+ "fmt"
+ "io"
+ "net/http"
+ "os"
+ "time"
+
+ "github.com/gin-gonic/gin"
+ "github.com/go-spring/spring-core/gs"
+)
+
+func init() {
+ gin.SetMode(gin.ReleaseMode)
+ gs.EnableSimpleHttpServer(false)
+
+ gs.Object(&Controller{})
+ gs.Provide(func(c *Controller) *gin.Engine {
+ e := gin.Default()
+ e.GET("/echo", c.Echo)
+ return e
+ })
+}
+
+type Controller struct{}
+
+func (c *Controller) Echo(ctx *gin.Context) {
+ ctx.String(http.StatusOK, "Hello, gin!")
+}
+
+func main() {
+ _ = os.Unsetenv("_")
+ _ = os.Unsetenv("TERM")
+ _ = os.Unsetenv("TERM_SESSION_ID")
+ go func() {
+ time.Sleep(time.Millisecond * 500)
+ runTest()
+ }()
+ gs.Run()
+}
+
+func runTest() {
+ resp, _ := http.Get("http://localhost:9090/echo")
+ b, _ := io.ReadAll(resp.Body)
+ defer resp.Body.Close()
+ fmt.Println("Response from server:", string(b))
+ gs.ShutDown()
+}
diff --git a/docs/4. examples/servers/gin/server.go b/docs/4. examples/servers/gin/server.go
new file mode 100644
index 00000000..70a49010
--- /dev/null
+++ b/docs/4. examples/servers/gin/server.go
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2025 The Go-Spring Authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package main
+
+import (
+ "context"
+ "net"
+ "net/http"
+ "time"
+
+ "github.com/gin-gonic/gin"
+ "github.com/go-spring/spring-core/gs"
+)
+
+func init() {
+ gs.Provide(
+ NewSimpleGinServer,
+ gs.IndexArg(1, gs.BindArg(gs.SetHttpServerAddr, gs.TagArg("${http.server.addr:=0.0.0.0:9090}"))),
+ gs.IndexArg(1, gs.BindArg(gs.SetHttpServerReadTimeout, gs.TagArg("${http.server.readTimeout:=5s}"))),
+ gs.IndexArg(1, gs.BindArg(gs.SetHttpServerHeaderTimeout, gs.TagArg("${http.server.headerTimeout:=1s}"))),
+ gs.IndexArg(1, gs.BindArg(gs.SetHttpServerWriteTimeout, gs.TagArg("${http.server.writeTimeout:=5s}"))),
+ gs.IndexArg(1, gs.BindArg(gs.SetHttpServerIdleTimeout, gs.TagArg("${http.server.idleTimeout:=60s}"))),
+ ).AsServer()
+}
+
+type SimpleGinServer struct {
+ svr *http.Server
+}
+
+func NewSimpleGinServer(e *gin.Engine, opts ...gs.HttpServerOption) *SimpleGinServer {
+ arg := &gs.HttpServerConfig{
+ Address: "0.0.0.0:9090",
+ ReadTimeout: time.Second * 5,
+ HeaderTimeout: time.Second,
+ WriteTimeout: time.Second * 5,
+ IdleTimeout: time.Second * 60,
+ }
+ for _, opt := range opts {
+ opt(arg)
+ }
+ return &SimpleGinServer{svr: &http.Server{
+ Handler: e,
+ Addr: arg.Address,
+ ReadTimeout: arg.ReadTimeout,
+ ReadHeaderTimeout: arg.HeaderTimeout,
+ WriteTimeout: arg.WriteTimeout,
+ IdleTimeout: arg.IdleTimeout,
+ }}
+}
+
+func (s *SimpleGinServer) ListenAndServe(sig gs.ReadySignal) error {
+ ln, err := net.Listen("tcp", s.svr.Addr)
+ if err != nil {
+ return err
+ }
+ <-sig.TriggerAndWait()
+ return s.svr.Serve(ln)
+}
+
+func (s *SimpleGinServer) Shutdown(ctx context.Context) error {
+ return s.svr.Shutdown(ctx)
+}
diff --git a/doc/examples/servers/grpc/go.mod b/docs/4. examples/servers/grpc/go.mod
similarity index 100%
rename from doc/examples/servers/grpc/go.mod
rename to docs/4. examples/servers/grpc/go.mod
diff --git a/doc/examples/servers/grpc/go.sum b/docs/4. examples/servers/grpc/go.sum
similarity index 100%
rename from doc/examples/servers/grpc/go.sum
rename to docs/4. examples/servers/grpc/go.sum
diff --git a/doc/examples/servers/grpc/idl/echo.proto b/docs/4. examples/servers/grpc/idl/echo.proto
similarity index 100%
rename from doc/examples/servers/grpc/idl/echo.proto
rename to docs/4. examples/servers/grpc/idl/echo.proto
diff --git a/doc/examples/servers/grpc/idl/proto/echo.pb.go b/docs/4. examples/servers/grpc/idl/proto/echo.pb.go
similarity index 100%
rename from doc/examples/servers/grpc/idl/proto/echo.pb.go
rename to docs/4. examples/servers/grpc/idl/proto/echo.pb.go
diff --git a/doc/examples/servers/grpc/idl/proto/echo_grpc.pb.go b/docs/4. examples/servers/grpc/idl/proto/echo_grpc.pb.go
similarity index 100%
rename from doc/examples/servers/grpc/idl/proto/echo_grpc.pb.go
rename to docs/4. examples/servers/grpc/idl/proto/echo_grpc.pb.go
diff --git a/doc/examples/servers/grpc/main.go b/docs/4. examples/servers/grpc/main.go
similarity index 100%
rename from doc/examples/servers/grpc/main.go
rename to docs/4. examples/servers/grpc/main.go
diff --git a/doc/examples/servers/grpc/server.go b/docs/4. examples/servers/grpc/server.go
similarity index 100%
rename from doc/examples/servers/grpc/server.go
rename to docs/4. examples/servers/grpc/server.go
diff --git a/doc/examples/servers/thrift/go.mod b/docs/4. examples/servers/thrift/go.mod
similarity index 100%
rename from doc/examples/servers/thrift/go.mod
rename to docs/4. examples/servers/thrift/go.mod
diff --git a/doc/examples/servers/thrift/go.sum b/docs/4. examples/servers/thrift/go.sum
similarity index 100%
rename from doc/examples/servers/thrift/go.sum
rename to docs/4. examples/servers/thrift/go.sum
diff --git a/doc/examples/servers/thrift/idl/echo.thrift b/docs/4. examples/servers/thrift/idl/echo.thrift
similarity index 100%
rename from doc/examples/servers/thrift/idl/echo.thrift
rename to docs/4. examples/servers/thrift/idl/echo.thrift
diff --git a/doc/examples/servers/thrift/idl/proto/GoUnusedProtection__.go b/docs/4. examples/servers/thrift/idl/proto/GoUnusedProtection__.go
similarity index 100%
rename from doc/examples/servers/thrift/idl/proto/GoUnusedProtection__.go
rename to docs/4. examples/servers/thrift/idl/proto/GoUnusedProtection__.go
diff --git a/doc/examples/servers/thrift/idl/proto/echo-consts.go b/docs/4. examples/servers/thrift/idl/proto/echo-consts.go
similarity index 100%
rename from doc/examples/servers/thrift/idl/proto/echo-consts.go
rename to docs/4. examples/servers/thrift/idl/proto/echo-consts.go
diff --git a/doc/examples/servers/thrift/idl/proto/echo.go b/docs/4. examples/servers/thrift/idl/proto/echo.go
similarity index 100%
rename from doc/examples/servers/thrift/idl/proto/echo.go
rename to docs/4. examples/servers/thrift/idl/proto/echo.go
diff --git a/doc/examples/servers/thrift/main.go b/docs/4. examples/servers/thrift/main.go
similarity index 100%
rename from doc/examples/servers/thrift/main.go
rename to docs/4. examples/servers/thrift/main.go
diff --git a/doc/examples/servers/thrift/server.go b/docs/4. examples/servers/thrift/server.go
similarity index 100%
rename from doc/examples/servers/thrift/server.go
rename to docs/4. examples/servers/thrift/server.go
diff --git a/doc/examples/startup/README.md b/docs/4. examples/startup/README.md
similarity index 100%
rename from doc/examples/startup/README.md
rename to docs/4. examples/startup/README.md
diff --git a/doc/examples/startup/README_CN.md b/docs/4. examples/startup/README_CN.md
similarity index 100%
rename from doc/examples/startup/README_CN.md
rename to docs/4. examples/startup/README_CN.md
diff --git a/doc/examples/startup/go.mod b/docs/4. examples/startup/go.mod
similarity index 100%
rename from doc/examples/startup/go.mod
rename to docs/4. examples/startup/go.mod
diff --git a/docs/4. examples/startup/go.sum b/docs/4. examples/startup/go.sum
new file mode 100644
index 00000000..77e1730d
--- /dev/null
+++ b/docs/4. examples/startup/go.sum
@@ -0,0 +1,26 @@
+github.com/expr-lang/expr v1.17.2 h1:o0A99O/Px+/DTjEnQiodAgOIK9PPxL8DtXhBRKC+Iso=
+github.com/expr-lang/expr v1.17.2/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
+github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
+github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/lvan100/go-assert v0.0.2 h1:K1G++zfdM5h+1Q/hSctEEqqcJIOs327k2kLiO3MmE5E=
+github.com/lvan100/go-assert v0.0.2/go.mod h1:osFFuU9zt4/SdTaJ9uU3y9qabAFDYlaH4Yte/ndDAj4=
+github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
+github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
+github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
+github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
+github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
+github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
+github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
+github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
+go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
+go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
diff --git a/doc/examples/startup/main.go b/docs/4. examples/startup/main.go
similarity index 100%
rename from doc/examples/startup/main.go
rename to docs/4. examples/startup/main.go
diff --git a/docs/5. advanced/advanced.md b/docs/5. advanced/advanced.md
new file mode 100644
index 00000000..af1e9a05
--- /dev/null
+++ b/docs/5. advanced/advanced.md
@@ -0,0 +1 @@
+todo 条件注入、自定义扩展、插件机制、热更新、模块组合
\ No newline at end of file
diff --git a/docs/6. integrations/integrations.md b/docs/6. integrations/integrations.md
new file mode 100644
index 00000000..7dd16c2b
--- /dev/null
+++ b/docs/6. integrations/integrations.md
@@ -0,0 +1 @@
+todo 与数据库、消息队列、缓存、中间件等的结合方式
\ No newline at end of file
diff --git a/docs/7. faq.md b/docs/7. faq.md
new file mode 100644
index 00000000..d21aba8c
--- /dev/null
+++ b/docs/7. faq.md
@@ -0,0 +1 @@
+todo FAQ,错误处理、故障排查、性能调优建议
\ No newline at end of file
diff --git a/docs/8. contributing.md b/docs/8. contributing.md
new file mode 100644
index 00000000..1f02b41c
--- /dev/null
+++ b/docs/8. contributing.md
@@ -0,0 +1 @@
+todo 如何参与开发、测试、PR 流程、代码规范
\ No newline at end of file
diff --git a/docs/9. changelog.md b/docs/9. changelog.md
new file mode 100644
index 00000000..64b54164
--- /dev/null
+++ b/docs/9. changelog.md
@@ -0,0 +1 @@
+todo Changelog、升级指南、版本兼容性
\ No newline at end of file
diff --git a/go.mod b/go.mod
index 16abfab6..e25e3f41 100644
--- a/go.mod
+++ b/go.mod
@@ -4,8 +4,8 @@ go 1.24
require (
github.com/expr-lang/expr v1.17.2
+ github.com/go-spring/log v0.0.2
github.com/lvan100/go-assert v0.0.2
- github.com/lvan100/go-loop v0.0.2
github.com/magiconair/properties v1.8.10
github.com/pelletier/go-toml v1.9.5
github.com/spf13/cast v1.7.1
diff --git a/go.sum b/go.sum
index 8801ffe2..7e856eec 100644
--- a/go.sum
+++ b/go.sum
@@ -2,6 +2,8 @@ github.com/expr-lang/expr v1.17.2 h1:o0A99O/Px+/DTjEnQiodAgOIK9PPxL8DtXhBRKC+Iso
github.com/expr-lang/expr v1.17.2/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
+github.com/go-spring/log v0.0.2 h1:Apt5hjV5kBjm+jxYzYkLBMEBUt9x7xw/7zINljMENFo=
+github.com/go-spring/log v0.0.2/go.mod h1:aKAsCR5Fv4WyFBtNtHNZV4YmitRutJ1Wh9fRI/IiHZM=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -10,8 +12,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lvan100/go-assert v0.0.2 h1:K1G++zfdM5h+1Q/hSctEEqqcJIOs327k2kLiO3MmE5E=
github.com/lvan100/go-assert v0.0.2/go.mod h1:osFFuU9zt4/SdTaJ9uU3y9qabAFDYlaH4Yte/ndDAj4=
-github.com/lvan100/go-loop v0.0.2 h1:FPy0gCO4jjWrNeJazTtIDH1HgKogK4HkTgplXMa0mu4=
-github.com/lvan100/go-loop v0.0.2/go.mod h1:xlhZBgRA1uBEDGsxTgWy3r7Ab04J/gbVYc2wHNKTv6w=
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
diff --git a/gs/app.go b/gs/app.go
new file mode 100644
index 00000000..df63280c
--- /dev/null
+++ b/gs/app.go
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2025 The Go-Spring Authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package gs
+
+import (
+ "context"
+
+ "github.com/go-spring/log"
+ "github.com/go-spring/spring-core/gs/internal/gs_app"
+)
+
+type AppStarter struct{}
+
+// Run runs the app and waits for an interrupt signal to exit.
+func (s *AppStarter) Run() {
+ s.RunWith(nil)
+}
+
+// initApp initializes the app.
+func (s *AppStarter) initApp() error {
+ printBanner()
+ if err := initLog(); err != nil {
+ return err
+ }
+ if err := B.(*gs_app.BootImpl).Run(); err != nil {
+ return err
+ }
+ B = nil
+ return nil
+}
+
+// RunWith runs the app with a given function and waits for an interrupt signal to exit.
+func (s *AppStarter) RunWith(fn func(ctx context.Context) error) {
+ var err error
+ defer func() {
+ if err != nil {
+ log.Errorf(context.Background(), log.TagApp, "app run failed: %v", err)
+ }
+ }()
+ if err = s.initApp(); err != nil {
+ return
+ }
+ err = gs_app.GS.RunWith(fn)
+}
+
+// RunAsync runs the app asynchronously and returns a function to stop the app.
+func (s *AppStarter) RunAsync() (func(), error) {
+ if err := s.initApp(); err != nil {
+ return nil, err
+ }
+ if err := gs_app.GS.Start(); err != nil {
+ return nil, err
+ }
+ return func() { gs_app.GS.Stop() }, nil
+}
diff --git a/gs/banner.go b/gs/banner.go
new file mode 100644
index 00000000..1ab10b9a
--- /dev/null
+++ b/gs/banner.go
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2025 The Go-Spring Authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package gs
+
+import (
+ "fmt"
+ "strings"
+)
+
+var appBanner = `
+ ____ ___ ____ ____ ____ ___ _ _ ____
+ / ___| / _ \ / ___| | _ \ | _ \ |_ _| | \ | | / ___|
+ | | _ | | | | _____ \___ \ | |_) | | |_) | | | | \| | | | _
+ | |_| | | |_| | |_____| ___) | | __/ | _ < | | | |\ | | |_| |
+ \____| \___/ |____/ |_| |_| \_\ |___| |_| \_| \____|
+`
+
+// Banner sets a custom app banner.
+func Banner(banner string) {
+ appBanner = banner
+}
+
+// printBanner prints the app banner.
+func printBanner() {
+ if len(appBanner) == 0 {
+ return
+ }
+
+ var sb strings.Builder
+ if appBanner[0] != '\n' {
+ sb.WriteString("\n")
+ }
+
+ maxLength := 0
+ for s := range strings.SplitSeq(appBanner, "\n") {
+ sb.WriteString("\x1b[36m")
+ sb.WriteString(s)
+ sb.WriteString("\x1b[0m\n")
+ if len(s) > maxLength {
+ maxLength = len(s)
+ }
+ }
+
+ if appBanner[len(appBanner)-1] != '\n' {
+ sb.WriteString("\n")
+ }
+
+ var padding []byte
+ if n := (maxLength - len(Version)) / 2; n > 0 {
+ padding = make([]byte, n)
+ for i := range padding {
+ padding[i] = ' '
+ }
+ }
+ sb.WriteString(string(padding))
+ sb.WriteString(Version)
+ fmt.Println(sb.String())
+}
diff --git a/gs/gs.go b/gs/gs.go
index 8b1997fd..ae33e2ca 100644
--- a/gs/gs.go
+++ b/gs/gs.go
@@ -18,10 +18,9 @@ package gs
import (
"context"
- "fmt"
"reflect"
- "strings"
+ "github.com/go-spring/log"
"github.com/go-spring/spring-core/conf"
"github.com/go-spring/spring-core/gs/internal/gs"
"github.com/go-spring/spring-core/gs/internal/gs_app"
@@ -30,7 +29,6 @@ import (
"github.com/go-spring/spring-core/gs/internal/gs_cond"
"github.com/go-spring/spring-core/gs/internal/gs_conf"
"github.com/go-spring/spring-core/gs/internal/gs_dync"
- "github.com/go-spring/spring-core/util/syslog"
)
const (
@@ -172,6 +170,13 @@ func BeanSelectorFor[T any](name ...string) BeanSelector {
/*********************************** app *************************************/
+// Property sets a system property.
+func Property(key string, val string) {
+ if err := gs_conf.SysConf.Set(key, val); err != nil {
+ log.Errorf(context.Background(), log.TagApp, "failed to set property key=%s, err=%v", key, err)
+ }
+}
+
type (
Runner = gs.Runner
Job = gs.Job
@@ -205,35 +210,27 @@ func FuncJob(fn func(ctx context.Context) error) *RegisteredBean {
return Object(funcJob(fn)).AsJob().Caller(1)
}
-type AppStarter struct{}
-
// Web enables or disables the built-in web server.
func Web(enable bool) *AppStarter {
EnableSimpleHttpServer(enable)
return &AppStarter{}
}
-// Run runs the app and waits for an interrupt signal to exit.
-func (s *AppStarter) Run() {
- var err error
- defer func() {
- if err != nil {
- syslog.Errorf("app run failed: %s", err.Error())
- }
- }()
- printBanner()
- if err = B.(*gs_app.BootImpl).Run(); err != nil {
- return
- }
- B = nil
- err = gs_app.GS.Run()
-}
-
// Run runs the app and waits for an interrupt signal to exit.
func Run() {
new(AppStarter).Run()
}
+// RunWith runs the app with a given function and waits for an interrupt signal to exit.
+func RunWith(fn func(ctx context.Context) error) {
+ new(AppStarter).RunWith(fn)
+}
+
+// RunAsync runs the app asynchronously and returns a function to stop the app.
+func RunAsync() (func(), error) {
+ return new(AppStarter).RunAsync()
+}
+
// Exiting returns a boolean indicating whether the application is exiting.
func Exiting() bool {
return gs_app.GS.Exiting()
@@ -286,50 +283,3 @@ func RefreshProperties() error {
}
return gs_app.GS.C.RefreshProperties(p)
}
-
-/********************************** banner ***********************************/
-
-var appBanner = `
- ____ ___ ____ ____ ____ ___ _ _ ____
- / ___| / _ \ / ___| | _ \ | _ \ |_ _| | \ | | / ___|
- | | _ | | | | _____ \___ \ | |_) | | |_) | | | | \| | | | _
- | |_| | | |_| | |_____| ___) | | __/ | _ < | | | |\ | | |_| |
- \____| \___/ |____/ |_| |_| \_\ |___| |_| \_| \____|
-`
-
-// Banner sets a custom app banner.
-func Banner(banner string) {
- appBanner = banner
-}
-
-// printBanner prints the app banner.
-func printBanner() {
- if len(appBanner) == 0 {
- return
- }
-
- if appBanner[0] != '\n' {
- fmt.Println()
- }
-
- maxLength := 0
- for s := range strings.SplitSeq(appBanner, "\n") {
- fmt.Printf("\x1b[36m%s\x1b[0m\n", s) // CYAN
- if len(s) > maxLength {
- maxLength = len(s)
- }
- }
-
- if appBanner[len(appBanner)-1] != '\n' {
- fmt.Println()
- }
-
- var padding []byte
- if n := (maxLength - len(Version)) / 2; n > 0 {
- padding = make([]byte, n)
- for i := range padding {
- padding[i] = ' '
- }
- }
- fmt.Println(string(padding) + Version + "\n")
-}
diff --git a/gs/http.go b/gs/http.go
index 2c00a22e..d9f085cf 100644
--- a/gs/http.go
+++ b/gs/http.go
@@ -35,7 +35,9 @@ func init() {
NewSimpleHttpServer,
IndexArg(1, BindArg(SetHttpServerAddr, TagArg("${http.server.addr:=0.0.0.0:9090}"))),
IndexArg(1, BindArg(SetHttpServerReadTimeout, TagArg("${http.server.readTimeout:=5s}"))),
+ IndexArg(1, BindArg(SetHttpServerHeaderTimeout, TagArg("${http.server.headerTimeout:=1s}"))),
IndexArg(1, BindArg(SetHttpServerWriteTimeout, TagArg("${http.server.writeTimeout:=5s}"))),
+ IndexArg(1, BindArg(SetHttpServerIdleTimeout, TagArg("${http.server.idleTimeout:=60s}"))),
).Condition(
OnBean[*http.ServeMux](),
OnProperty(EnableServersProp).HavingValue("true").MatchIfMissing(),
@@ -45,9 +47,11 @@ func init() {
// HttpServerConfig holds configuration options for the HTTP server.
type HttpServerConfig struct {
- Address string // The address to bind the server to.
- ReadTimeout time.Duration // The read timeout duration.
- WriteTimeout time.Duration // The write timeout duration.
+ Address string // The address to bind the server to.
+ ReadTimeout time.Duration // The read timeout duration.
+ HeaderTimeout time.Duration // The header timeout duration.
+ WriteTimeout time.Duration // The write timeout duration.
+ IdleTimeout time.Duration // The idle timeout duration.
}
// HttpServerOption is a function type for setting options on HttpServerConfig.
@@ -67,6 +71,13 @@ func SetHttpServerReadTimeout(timeout time.Duration) HttpServerOption {
}
}
+// SetHttpServerHeaderTimeout sets the header timeout for the HTTP server.
+func SetHttpServerHeaderTimeout(timeout time.Duration) HttpServerOption {
+ return func(arg *HttpServerConfig) {
+ arg.HeaderTimeout = timeout
+ }
+}
+
// SetHttpServerWriteTimeout sets the write timeout for the HTTP server.
func SetHttpServerWriteTimeout(timeout time.Duration) HttpServerOption {
return func(arg *HttpServerConfig) {
@@ -74,6 +85,13 @@ func SetHttpServerWriteTimeout(timeout time.Duration) HttpServerOption {
}
}
+// SetHttpServerIdleTimeout sets the idle timeout for the HTTP server.
+func SetHttpServerIdleTimeout(timeout time.Duration) HttpServerOption {
+ return func(arg *HttpServerConfig) {
+ arg.IdleTimeout = timeout
+ }
+}
+
// SimpleHttpServer wraps a [http.Server] instance.
type SimpleHttpServer struct {
svr *http.Server // The HTTP server instance.
@@ -82,9 +100,11 @@ type SimpleHttpServer struct {
// NewSimpleHttpServer creates a new instance of SimpleHttpServer.
func NewSimpleHttpServer(mux *http.ServeMux, opts ...HttpServerOption) *SimpleHttpServer {
arg := &HttpServerConfig{
- Address: "0.0.0.0:9090",
- ReadTimeout: time.Second * 5,
- WriteTimeout: time.Second * 5,
+ Address: "0.0.0.0:9090",
+ ReadTimeout: time.Second * 5,
+ HeaderTimeout: time.Second,
+ WriteTimeout: time.Second * 5,
+ IdleTimeout: time.Second * 60,
}
for _, opt := range opts {
opt(arg)
diff --git a/gs/internal/gs_app/app.go b/gs/internal/gs_app/app.go
index 9ec0b193..ea793b02 100644
--- a/gs/internal/gs_app/app.go
+++ b/gs/internal/gs_app/app.go
@@ -27,12 +27,12 @@ import (
"syscall"
"time"
+ "github.com/go-spring/log"
"github.com/go-spring/spring-core/conf"
"github.com/go-spring/spring-core/gs/internal/gs"
"github.com/go-spring/spring-core/gs/internal/gs_conf"
"github.com/go-spring/spring-core/gs/internal/gs_core"
"github.com/go-spring/spring-core/util/goutil"
- "github.com/go-spring/spring-core/util/syslog"
)
// GS is the global application instance.
@@ -74,18 +74,30 @@ func NewApp() *App {
// (e.g., SIGINT, SIGTERM). Upon receiving a signal, it initiates
// a graceful shutdown.
func (app *App) Run() error {
- app.C.Object(app)
+ return app.RunWith(nil)
+}
+// RunWith starts the application and listens for termination signals
+// (e.g., SIGINT, SIGTERM). Upon receiving a signal, it initiates
+// a graceful shutdown.
+func (app *App) RunWith(fn func(ctx context.Context) error) error {
if err := app.Start(); err != nil {
return err
}
+ // runs the user-defined function
+ if fn != nil {
+ if err := fn(app.ctx); err != nil {
+ return err
+ }
+ }
+
// listens for OS termination signals
go func() {
ch := make(chan os.Signal, 1)
signal.Notify(ch, os.Interrupt, syscall.SIGTERM)
sig := <-ch
- syslog.Infof("Received signal: %v", sig)
+ log.Infof(context.Background(), log.TagApp, "Received signal: %v", sig)
app.ShutDown()
}()
@@ -99,9 +111,10 @@ func (app *App) Run() error {
// loading, IoC container refreshing, dependency injection, and runs
// runners, jobs and servers.
func (app *App) Start() error {
- var p conf.Properties
+ app.C.Object(app)
// loads the layered app properties
+ var p conf.Properties
{
var err error
if p, err = app.P.Refresh(); err != nil {
@@ -132,7 +145,7 @@ func (app *App) Start() error {
}
}()
if err := job.Run(app.ctx); err != nil {
- syslog.Errorf("job run error: %s", err.Error())
+ log.Errorf(context.Background(), log.TagApp, "job run error: %v", err)
app.ShutDown()
}
})
@@ -156,7 +169,7 @@ func (app *App) Start() error {
}()
err := svr.ListenAndServe(sig)
if err != nil && !errors.Is(err, http.ErrServerClosed) {
- syslog.Errorf("server serve error: %s", err.Error())
+ log.Errorf(context.Background(), log.TagApp, "server serve error: %v", err)
sig.Intercept()
app.ShutDown()
}
@@ -166,7 +179,7 @@ func (app *App) Start() error {
if sig.Intercepted() {
return nil
}
- syslog.Infof("ready to serve requests")
+ log.Infof(context.Background(), log.TagApp, "ready to serve requests")
sig.Close()
}
return nil
@@ -183,7 +196,7 @@ func (app *App) Stop() {
for _, svr := range app.Servers {
goutil.GoFunc(func() {
if err := svr.Shutdown(ctx); err != nil {
- syslog.Errorf("shutdown server failed: %s", err.Error())
+ log.Errorf(context.Background(), log.TagApp, "shutdown server failed: %v", err)
}
})
}
@@ -194,9 +207,9 @@ func (app *App) Stop() {
select {
case <-waitChan:
- syslog.Infof("shutdown complete")
+ log.Infof(context.Background(), log.TagApp, "shutdown complete")
case <-ctx.Done():
- syslog.Infof("shutdown timeout")
+ log.Infof(context.Background(), log.TagApp, "shutdown timeout")
}
}
diff --git a/gs/internal/gs_app/app_test.go b/gs/internal/gs_app/app_test.go
index cee35f0f..f6a7bba3 100644
--- a/gs/internal/gs_app/app_test.go
+++ b/gs/internal/gs_app/app_test.go
@@ -20,14 +20,17 @@ import (
"bytes"
"context"
"errors"
- "log/slog"
"net/http"
"os"
+ "runtime/debug"
"testing"
"time"
+ "github.com/go-spring/log"
+ "github.com/go-spring/spring-core/conf"
"github.com/go-spring/spring-core/gs/internal/gs"
- "github.com/go-spring/spring-core/util/sysconf"
+ "github.com/go-spring/spring-core/gs/internal/gs_conf"
+ "github.com/go-spring/spring-core/util/goutil"
"github.com/lvan100/go-assert"
"go.uber.org/mock/gomock"
)
@@ -35,14 +38,17 @@ import (
var logBuf = &bytes.Buffer{}
func init() {
- slog.SetDefault(slog.New(slog.NewTextHandler(logBuf, nil)))
+ goutil.OnPanic = func(ctx context.Context, r any) {
+ log.Panicf(ctx, log.TagDef, "panic: %v\n%s\n", r, debug.Stack())
+ }
}
func clean() {
logBuf.Reset()
+ log.Stdout = logBuf
os.Args = nil
os.Clearenv()
- sysconf.Clear()
+ gs_conf.SysConf = conf.New()
}
func TestApp(t *testing.T) {
@@ -67,7 +73,7 @@ func TestApp(t *testing.T) {
t.Run("config refresh error", func(t *testing.T) {
t.Cleanup(clean)
- sysconf.Set("a", "123")
+ _ = gs_conf.SysConf.Set("a", "123")
_ = os.Setenv("GS_A_B", "456")
app := NewApp()
err := app.Run()
@@ -98,8 +104,8 @@ func TestApp(t *testing.T) {
t.Run("disable jobs & servers", func(t *testing.T) {
t.Cleanup(clean)
- sysconf.Set("spring.app.enable-jobs", "false")
- sysconf.Set("spring.app.enable-servers", "false")
+ _ = gs_conf.SysConf.Set("spring.app.enable-jobs", "false")
+ _ = gs_conf.SysConf.Set("spring.app.enable-servers", "false")
app := NewApp()
go func() {
time.Sleep(50 * time.Millisecond)
@@ -252,7 +258,7 @@ func TestApp(t *testing.T) {
t.Run("shutdown timeout", func(t *testing.T) {
t.Cleanup(clean)
- sysconf.Set("spring.app.shutdown-timeout", "10ms")
+ _ = gs_conf.SysConf.Set("spring.app.shutdown-timeout", "10ms")
ctrl := gomock.NewController(t)
defer ctrl.Finish()
app := NewApp()
diff --git a/gs/internal/gs_app/boot_test.go b/gs/internal/gs_app/boot_test.go
index 97737033..f0a0ff1c 100644
--- a/gs/internal/gs_app/boot_test.go
+++ b/gs/internal/gs_app/boot_test.go
@@ -24,7 +24,7 @@ import (
"testing"
"github.com/go-spring/spring-core/gs/internal/gs_bean"
- "github.com/go-spring/spring-core/util/sysconf"
+ "github.com/go-spring/spring-core/gs/internal/gs_conf"
"github.com/lvan100/go-assert"
)
@@ -32,7 +32,7 @@ func TestBoot(t *testing.T) {
t.Run("flag is false", func(t *testing.T) {
t.Cleanup(clean)
- sysconf.Set("a", "123")
+ _ = gs_conf.SysConf.Set("a", "123")
_ = os.Setenv("GS_A_B", "456")
boot := NewBoot().(*BootImpl)
err := boot.Run()
@@ -41,7 +41,7 @@ func TestBoot(t *testing.T) {
t.Run("config refresh error", func(t *testing.T) {
t.Cleanup(clean)
- sysconf.Set("a", "123")
+ _ = gs_conf.SysConf.Set("a", "123")
_ = os.Setenv("GS_A_B", "456")
boot := NewBoot().(*BootImpl)
boot.Object(bytes.NewBuffer(nil))
diff --git a/gs/internal/gs_conf/conf.go b/gs/internal/gs_conf/conf.go
index f56df962..91865d99 100644
--- a/gs/internal/gs_conf/conf.go
+++ b/gs/internal/gs_conf/conf.go
@@ -49,12 +49,14 @@ import (
"strings"
"github.com/go-spring/spring-core/conf"
- "github.com/go-spring/spring-core/util/sysconf"
)
// osStat only for test.
var osStat = os.Stat
+// SysConf is the builtin configuration.
+var SysConf = conf.New()
+
// PropertyCopier defines the interface for copying properties.
type PropertyCopier interface {
CopyTo(out *conf.MutableProperties) error
@@ -78,6 +80,21 @@ func (c *NamedPropertyCopier) CopyTo(out *conf.MutableProperties) error {
return nil
}
+/******************************** SysConfig **********************************/
+
+type SysConfig struct {
+ Environment *Environment // Environment variables as configuration source.
+ CommandArgs *CommandArgs // Command line arguments as configuration source.
+}
+
+func (c *SysConfig) Refresh() (conf.Properties, error) {
+ return merge(
+ NewNamedPropertyCopier("sys", SysConf),
+ NewNamedPropertyCopier("env", c.Environment),
+ NewNamedPropertyCopier("cmd", c.CommandArgs),
+ )
+}
+
/******************************** AppConfig **********************************/
// AppConfig represents a layered application configuration.
@@ -113,11 +130,7 @@ func merge(sources ...PropertyCopier) (conf.Properties, error) {
// Refresh merges all layers of configurations into a read-only properties.
func (c *AppConfig) Refresh() (conf.Properties, error) {
- p, err := merge(
- NewNamedPropertyCopier("sys", sysconf.Clone()),
- NewNamedPropertyCopier("env", c.Environment),
- NewNamedPropertyCopier("cmd", c.CommandArgs),
- )
+ p, err := new(SysConfig).Refresh()
if err != nil {
return nil, err
}
@@ -133,7 +146,7 @@ func (c *AppConfig) Refresh() (conf.Properties, error) {
}
var sources []PropertyCopier
- sources = append(sources, NewNamedPropertyCopier("sys", sysconf.Clone()))
+ sources = append(sources, NewNamedPropertyCopier("sys", SysConf))
sources = append(sources, localFiles...)
sources = append(sources, remoteFiles...)
sources = append(sources, NewNamedPropertyCopier("remote", c.RemoteProp))
@@ -163,11 +176,7 @@ func NewBootConfig() *BootConfig {
// Refresh merges all layers of configurations into a read-only properties.
func (c *BootConfig) Refresh() (conf.Properties, error) {
- p, err := merge(
- NewNamedPropertyCopier("sys", sysconf.Clone()),
- NewNamedPropertyCopier("env", c.Environment),
- NewNamedPropertyCopier("cmd", c.CommandArgs),
- )
+ p, err := new(SysConfig).Refresh()
if err != nil {
return nil, err
}
@@ -178,7 +187,7 @@ func (c *BootConfig) Refresh() (conf.Properties, error) {
}
var sources []PropertyCopier
- sources = append(sources, NewNamedPropertyCopier("sys", sysconf.Clone()))
+ sources = append(sources, NewNamedPropertyCopier("sys", SysConf))
sources = append(sources, localFiles...)
sources = append(sources, NewNamedPropertyCopier("env", c.Environment))
sources = append(sources, NewNamedPropertyCopier("cmd", c.CommandArgs))
diff --git a/gs/internal/gs_conf/conf_test.go b/gs/internal/gs_conf/conf_test.go
index 1966c10a..2544a6ab 100644
--- a/gs/internal/gs_conf/conf_test.go
+++ b/gs/internal/gs_conf/conf_test.go
@@ -22,14 +22,13 @@ import (
"testing"
"github.com/go-spring/spring-core/conf"
- "github.com/go-spring/spring-core/util/sysconf"
"github.com/lvan100/go-assert"
)
func clean() {
os.Args = nil
os.Clearenv()
- sysconf.Clear()
+ SysConf = conf.New()
}
func TestAppConfig(t *testing.T) {
@@ -74,7 +73,7 @@ func TestAppConfig(t *testing.T) {
t.Run("merge error - 2", func(t *testing.T) {
t.Cleanup(clean)
_ = os.Setenv("GS_SPRING_APP_CONFIG-LOCAL_DIR", "./testdata/conf")
- sysconf.Set("http.server[0].addr", "0.0.0.0:8080")
+ _ = SysConf.Set("http.server[0].addr", "0.0.0.0:8080")
_, err := NewAppConfig().Refresh()
assert.ThatError(t, err).Matches("property conflict at path http.server.addr")
})
@@ -113,7 +112,7 @@ func TestBootConfig(t *testing.T) {
t.Run("merge error - 2", func(t *testing.T) {
t.Cleanup(clean)
_ = os.Setenv("GS_SPRING_APP_CONFIG-LOCAL_DIR", "./testdata/conf")
- sysconf.Set("http.server[0].addr", "0.0.0.0:8080")
+ _ = SysConf.Set("http.server[0].addr", "0.0.0.0:8080")
_, err := NewBootConfig().Refresh()
assert.ThatError(t, err).Matches("property conflict at path http.server.addr")
})
diff --git a/gs/internal/gs_core/injecting/injecting.go b/gs/internal/gs_core/injecting/injecting.go
index 209cbc4b..9e9998be 100644
--- a/gs/internal/gs_core/injecting/injecting.go
+++ b/gs/internal/gs_core/injecting/injecting.go
@@ -19,6 +19,7 @@ package injecting
import (
"bytes"
"container/list"
+ "context"
"errors"
"fmt"
"reflect"
@@ -27,6 +28,7 @@ import (
"strings"
"testing"
+ "github.com/go-spring/log"
"github.com/go-spring/spring-core/conf"
"github.com/go-spring/spring-core/gs/internal/gs"
"github.com/go-spring/spring-core/gs/internal/gs_arg"
@@ -34,7 +36,6 @@ import (
"github.com/go-spring/spring-core/gs/internal/gs_dync"
"github.com/go-spring/spring-core/gs/internal/gs_util"
"github.com/go-spring/spring-core/util"
- "github.com/go-spring/spring-core/util/syslog"
"github.com/spf13/cast"
)
@@ -98,7 +99,7 @@ func (c *Injecting) Refresh(beans []*gs_bean.BeanDefinition) (err error) {
defer func() {
if err != nil || len(stack.beans) > 0 {
err = fmt.Errorf("%s ↩\n%s", err, stack.Path())
- syslog.Errorf("%s", err.Error())
+ log.Errorf(context.Background(), log.TagApp, "%v", err)
}
}()
@@ -271,7 +272,7 @@ func (c *Injector) getBean(t reflect.Type, tag WireTag, stack *Stack) (BeanRunti
}
if !slices.Contains(foundBeans, b) {
foundBeans = append(foundBeans, b)
- syslog.Warnf("you should call Export() on %s", b)
+ log.Warnf(context.Background(), log.TagApp, "you should call Export() on %s", b)
}
}
}
@@ -565,7 +566,7 @@ func (c *Injector) getBeanValue(b BeanRuntime, stack *Stack) (reflect.Value, err
out, err := b.Callable().Call(NewArgContext(c, stack))
if err != nil {
if c.forceAutowireIsNullable {
- syslog.Warnf("autowire error: %v", err)
+ log.Warnf(context.Background(), log.TagApp, "autowire error: %v", err)
return reflect.Value{}, nil
}
return reflect.Value{}, err
@@ -575,7 +576,7 @@ func (c *Injector) getBeanValue(b BeanRuntime, stack *Stack) (reflect.Value, err
if o := out[len(out)-1]; util.IsErrorType(o.Type()) {
if i := o.Interface(); i != nil {
if c.forceAutowireIsNullable {
- syslog.Warnf("autowire error: %v", err)
+ log.Warnf(context.Background(), log.TagApp, "autowire error: %v", err)
return reflect.Value{}, nil
}
return reflect.Value{}, i.(error)
@@ -745,7 +746,7 @@ func NewStack() *Stack {
// pushBean records that bean b is being wired, used for cycle detection.
func (s *Stack) pushBean(b *gs_bean.BeanDefinition) {
- syslog.Debugf("push %s %s", b, b.Status())
+ log.Debugf(context.Background(), log.TagApp, "push %s %s", b, b.Status())
s.beans = append(s.beans, b)
}
@@ -755,7 +756,7 @@ func (s *Stack) popBean() {
b := s.beans[n-1]
s.beans[n-1] = nil
s.beans = s.beans[:n-1]
- syslog.Debugf("pop %s %s", b, b.Status())
+ log.Debugf(context.Background(), log.TagApp, "pop %s %s", b, b.Status())
}
// Path builds a readable representation of the wiring stack path for errors.
@@ -809,7 +810,7 @@ func (s *Stack) getSortedDestroyers() []func() {
fnValue := reflect.ValueOf(fn)
out := fnValue.Call([]reflect.Value{v})
if len(out) > 0 && !out[0].IsNil() {
- syslog.Errorf("%s", out[0].Interface().(error).Error())
+ log.Errorf(context.Background(), log.TagApp, "%v", out[0].Interface())
}
}
}
diff --git a/gs/log.go b/gs/log.go
new file mode 100644
index 00000000..baf2e423
--- /dev/null
+++ b/gs/log.go
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2025 The Go-Spring Authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package gs
+
+import (
+ "path/filepath"
+ "strings"
+
+ "github.com/go-spring/log"
+ "github.com/go-spring/spring-core/gs/internal/gs_conf"
+)
+
+// initLog initializes the log system.
+func initLog() error {
+ p, err := new(gs_conf.SysConfig).Refresh()
+ if err != nil {
+ return err
+ }
+ var c struct {
+ LocalDir string `value:"${spring.app.config-local.dir:=./conf}"`
+ Profiles string `value:"${spring.profiles.active:=}"`
+ }
+ if err = p.Bind(&c); err != nil {
+ return err
+ }
+ logFile := "log.xml"
+ if c.Profiles != "" {
+ profile := strings.Split(c.Profiles, ",")[0]
+ logFile = "log-" + profile + ".xml"
+ }
+ return log.RefreshFile(filepath.Join(c.LocalDir, logFile))
+}
diff --git a/gs/prop.go b/gs/prop.go
index 85d167d7..6a521ede 100644
--- a/gs/prop.go
+++ b/gs/prop.go
@@ -18,8 +18,6 @@ package gs
import (
"strconv"
-
- "github.com/go-spring/spring-core/util/sysconf"
)
const (
@@ -34,35 +32,35 @@ const (
// AllowCircularReferences enables or disables circular references between beans.
func AllowCircularReferences(enable bool) {
- sysconf.Set(AllowCircularReferencesProp, strconv.FormatBool(enable))
+ Property(AllowCircularReferencesProp, strconv.FormatBool(enable))
}
// ForceAutowireIsNullable forces autowire to be nullable.
func ForceAutowireIsNullable(enable bool) {
- sysconf.Set(ForceAutowireIsNullableProp, strconv.FormatBool(enable))
+ Property(ForceAutowireIsNullableProp, strconv.FormatBool(enable))
}
// SetActiveProfiles sets the active profiles for the app.
func SetActiveProfiles(profiles string) {
- sysconf.Set(ActiveProfilesProp, profiles)
+ Property(ActiveProfilesProp, profiles)
}
// EnableJobs enables or disables the app jobs.
func EnableJobs(enable bool) {
- sysconf.Set(EnableJobsProp, strconv.FormatBool(enable))
+ Property(EnableJobsProp, strconv.FormatBool(enable))
}
// EnableServers enables or disables the app servers.
func EnableServers(enable bool) {
- sysconf.Set(EnableServersProp, strconv.FormatBool(enable))
+ Property(EnableServersProp, strconv.FormatBool(enable))
}
// EnableSimpleHttpServer enables or disables the simple HTTP server.
func EnableSimpleHttpServer(enable bool) {
- sysconf.Set(EnableSimpleHttpServerProp, strconv.FormatBool(enable))
+ Property(EnableSimpleHttpServerProp, strconv.FormatBool(enable))
}
// EnableSimplePProfServer enables or disables the simple pprof server.
func EnableSimplePProfServer(enable bool) {
- sysconf.Set(EnableSimplePProfServerProp, strconv.FormatBool(enable))
+ Property(EnableSimplePProfServerProp, strconv.FormatBool(enable))
}
diff --git a/log/field.go b/log/field.go
deleted file mode 100644
index f1257b50..00000000
--- a/log/field.go
+++ /dev/null
@@ -1,422 +0,0 @@
-/*
- * Copyright 2025 The Go-Spring Authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package log
-
-import (
- "fmt"
-)
-
-const MsgKey = "msg"
-
-// Field represents a structured log field with a key and a value.
-type Field struct {
- Key string // The name of the field.
- Val Value // The value of the field.
-}
-
-// Msg creates a string Field with the "msg" key.
-func Msg(msg string) Field {
- return String(MsgKey, msg)
-}
-
-// Msgf formats a message using fmt.Sprintf and creates a string Field with "msg" key.
-func Msgf(format string, args ...any) Field {
- return String(MsgKey, fmt.Sprintf(format, args...))
-}
-
-// Reflect wraps any value into a Field using reflection.
-func Reflect(key string, val interface{}) Field {
- return Field{Key: key, Val: ReflectValue{Val: val}}
-}
-
-// Nil creates a Field with a nil value.
-func Nil(key string) Field {
- return Reflect(key, nil)
-}
-
-// Bool creates a Field for a boolean value.
-func Bool(key string, val bool) Field {
- return Field{Key: key, Val: BoolValue(val)}
-}
-
-// BoolPtr creates a Field from a *bool, or a nil Field if the pointer is nil.
-func BoolPtr(key string, val *bool) Field {
- if val == nil {
- return Nil(key)
- }
- return Bool(key, *val)
-}
-
-// Int creates a Field for an int value.
-func Int(key string, val int) Field {
- return Field{Key: key, Val: Int64Value(val)}
-}
-
-// IntPtr creates a Field from a *int, or returns Nil if pointer is nil.
-func IntPtr(key string, val *int) Field {
- if val == nil {
- return Nil(key)
- }
- return Int(key, *val)
-}
-
-// Int8 creates a Field for an int8 value.
-func Int8(key string, val int8) Field {
- return Field{Key: key, Val: Int64Value(val)}
-}
-
-// Int8Ptr creates a Field from a *int8, or returns Nil if pointer is nil.
-func Int8Ptr(key string, val *int8) Field {
- if val == nil {
- return Nil(key)
- }
- return Int8(key, *val)
-}
-
-// Int16 creates a Field for an int16 value.
-func Int16(key string, val int16) Field {
- return Field{Key: key, Val: Int64Value(val)}
-}
-
-// Int16Ptr creates a Field from a *int16, or returns Nil if pointer is nil.
-func Int16Ptr(key string, val *int16) Field {
- if val == nil {
- return Nil(key)
- }
- return Int16(key, *val)
-}
-
-// Int32 creates a Field for an int32 value.
-func Int32(key string, val int32) Field {
- return Field{Key: key, Val: Int64Value(val)}
-}
-
-// Int32Ptr creates a Field from a *int32, or returns Nil if pointer is nil.
-func Int32Ptr(key string, val *int32) Field {
- if val == nil {
- return Nil(key)
- }
- return Int32(key, *val)
-}
-
-// Int64 creates a Field for an int64 value.
-func Int64(key string, val int64) Field {
- return Field{Key: key, Val: Int64Value(val)}
-}
-
-// Int64Ptr creates a Field from a *int64, or returns Nil if pointer is nil.
-func Int64Ptr(key string, val *int64) Field {
- if val == nil {
- return Nil(key)
- }
- return Int64(key, *val)
-}
-
-// Uint creates a Field for an uint value.
-func Uint(key string, val uint) Field {
- return Field{Key: key, Val: Uint64Value(val)}
-}
-
-// UintPtr creates a Field from a *uint, or returns Nil if pointer is nil.
-func UintPtr(key string, val *uint) Field {
- if val == nil {
- return Nil(key)
- }
- return Uint(key, *val)
-}
-
-// Uint8 creates a Field for an uint8 value.
-func Uint8(key string, val uint8) Field {
- return Field{Key: key, Val: Uint64Value(val)}
-}
-
-// Uint8Ptr creates a Field from a *uint8, or returns Nil if pointer is nil.
-func Uint8Ptr(key string, val *uint8) Field {
- if val == nil {
- return Nil(key)
- }
- return Uint8(key, *val)
-}
-
-// Uint16 creates a Field for an uint16 value.
-func Uint16(key string, val uint16) Field {
- return Field{Key: key, Val: Uint64Value(val)}
-}
-
-// Uint16Ptr creates a Field from a *uint16, or returns Nil if pointer is nil.
-func Uint16Ptr(key string, val *uint16) Field {
- if val == nil {
- return Nil(key)
- }
- return Uint16(key, *val)
-}
-
-// Uint32 creates a Field for an uint32 value.
-func Uint32(key string, val uint32) Field {
- return Field{Key: key, Val: Uint64Value(val)}
-}
-
-// Uint32Ptr creates a Field from a *uint32, or returns Nil if pointer is nil.
-func Uint32Ptr(key string, val *uint32) Field {
- if val == nil {
- return Nil(key)
- }
- return Uint32(key, *val)
-}
-
-// Uint64 creates a Field for an uint64 value.
-func Uint64(key string, val uint64) Field {
- return Field{Key: key, Val: Uint64Value(val)}
-}
-
-// Uint64Ptr creates a Field from a *uint64, or returns Nil if pointer is nil.
-func Uint64Ptr(key string, val *uint64) Field {
- if val == nil {
- return Nil(key)
- }
- return Uint64(key, *val)
-}
-
-// Float32 creates a Field for a float32 value.
-func Float32(key string, val float32) Field {
- return Field{Key: key, Val: Float64Value(val)}
-}
-
-// Float32Ptr creates a Field from a *float32, or returns Nil if pointer is nil.
-func Float32Ptr(key string, val *float32) Field {
- if val == nil {
- return Nil(key)
- }
- return Float32(key, *val)
-}
-
-// Float64 creates a Field for a float64 value.
-func Float64(key string, val float64) Field {
- return Field{Key: key, Val: Float64Value(val)}
-}
-
-// Float64Ptr creates a Field from a *float64, or returns Nil if pointer is nil.
-func Float64Ptr(key string, val *float64) Field {
- if val == nil {
- return Nil(key)
- }
- return Float64(key, *val)
-}
-
-// String creates a Field for a string value.
-func String(key string, val string) Field {
- return Field{Key: key, Val: StringValue(val)}
-}
-
-// StringPtr creates a Field from a *string, or returns Nil if pointer is nil.
-func StringPtr(key string, val *string) Field {
- if val == nil {
- return Nil(key)
- }
- return String(key, *val)
-}
-
-// Array creates a Field containing a variadic slice of Values, wrapped as an array.
-func Array(key string, val ...Value) Field {
- return Field{Key: key, Val: ArrayValue(val)}
-}
-
-// Object creates a Field containing a variadic slice of Fields, treated as a nested object.
-func Object(key string, fields ...Field) Field {
- return Field{Key: key, Val: ObjectValue(fields)}
-}
-
-// Bools creates a Field with a slice of booleans.
-func Bools(key string, val []bool) Field {
- return Field{Key: key, Val: BoolsValue(val)}
-}
-
-// Ints creates a Field with a slice of integers.
-func Ints(key string, val []int) Field {
- return Field{Key: key, Val: IntsValue(val)}
-}
-
-// Int8s creates a Field with a slice of int8 values.
-func Int8s(key string, val []int8) Field {
- return Field{Key: key, Val: Int8sValue(val)}
-}
-
-// Int16s creates a Field with a slice of int16 values.
-func Int16s(key string, val []int16) Field {
- return Field{Key: key, Val: Int16sValue(val)}
-}
-
-// Int32s creates a Field with a slice of int32 values.
-func Int32s(key string, val []int32) Field {
- return Field{Key: key, Val: Int32sValue(val)}
-}
-
-// Int64s creates a Field with a slice of int64 values.
-func Int64s(key string, val []int64) Field {
- return Field{Key: key, Val: Int64sValue(val)}
-}
-
-// Uints creates a Field with a slice of unsigned integers.
-func Uints(key string, val []uint) Field {
- return Field{Key: key, Val: UintsValue(val)}
-}
-
-// Uint8s creates a Field with a slice of uint8 values.
-func Uint8s(key string, val []uint8) Field {
- return Field{Key: key, Val: Uint8sValue(val)}
-}
-
-// Uint16s creates a Field with a slice of uint16 values.
-func Uint16s(key string, val []uint16) Field {
- return Field{Key: key, Val: Uint16sValue(val)}
-}
-
-// Uint32s creates a Field with a slice of uint32 values.
-func Uint32s(key string, val []uint32) Field {
- return Field{Key: key, Val: Uint32sValue(val)}
-}
-
-// Uint64s creates a Field with a slice of uint64 values.
-func Uint64s(key string, val []uint64) Field {
- return Field{Key: key, Val: Uint64sValue(val)}
-}
-
-// Float32s creates a Field with a slice of float32 values.
-func Float32s(key string, val []float32) Field {
- return Field{Key: key, Val: Float32sValue(val)}
-}
-
-// Float64s creates a Field with a slice of float64 values.
-func Float64s(key string, val []float64) Field {
- return Field{Key: key, Val: Float64sValue(val)}
-}
-
-// Strings creates a Field with a slice of strings.
-func Strings(key string, val []string) Field {
- return Field{Key: key, Val: StringsValue(val)}
-}
-
-// Any creates a Field from a value of any type by inspecting its dynamic type.
-// It dispatches to the appropriate typed constructor based on the actual value.
-// If the type is not explicitly handled, it falls back to using Reflect.
-func Any(key string, value interface{}) Field {
- switch val := value.(type) {
- case nil:
- return Nil(key)
-
- case bool:
- return Bool(key, val)
- case *bool:
- return BoolPtr(key, val)
- case []bool:
- return Bools(key, val)
-
- case int:
- return Int(key, val)
- case *int:
- return IntPtr(key, val)
- case []int:
- return Ints(key, val)
-
- case int8:
- return Int8(key, val)
- case *int8:
- return Int8Ptr(key, val)
- case []int8:
- return Int8s(key, val)
-
- case int16:
- return Int16(key, val)
- case *int16:
- return Int16Ptr(key, val)
- case []int16:
- return Int16s(key, val)
-
- case int32:
- return Int32(key, val)
- case *int32:
- return Int32Ptr(key, val)
- case []int32:
- return Int32s(key, val)
-
- case int64:
- return Int64(key, val)
- case *int64:
- return Int64Ptr(key, val)
- case []int64:
- return Int64s(key, val)
-
- case uint:
- return Uint(key, val)
- case *uint:
- return UintPtr(key, val)
- case []uint:
- return Uints(key, val)
-
- case uint8:
- return Uint8(key, val)
- case *uint8:
- return Uint8Ptr(key, val)
- case []uint8:
- return Uint8s(key, val)
-
- case uint16:
- return Uint16(key, val)
- case *uint16:
- return Uint16Ptr(key, val)
- case []uint16:
- return Uint16s(key, val)
-
- case uint32:
- return Uint32(key, val)
- case *uint32:
- return Uint32Ptr(key, val)
- case []uint32:
- return Uint32s(key, val)
-
- case uint64:
- return Uint64(key, val)
- case *uint64:
- return Uint64Ptr(key, val)
- case []uint64:
- return Uint64s(key, val)
-
- case float32:
- return Float32(key, val)
- case *float32:
- return Float32Ptr(key, val)
- case []float32:
- return Float32s(key, val)
-
- case float64:
- return Float64(key, val)
- case *float64:
- return Float64Ptr(key, val)
- case []float64:
- return Float64s(key, val)
-
- case string:
- return String(key, val)
- case *string:
- return StringPtr(key, val)
- case []string:
- return Strings(key, val)
-
- default:
- return Reflect(key, val)
- }
-}
diff --git a/log/field_encoder.go b/log/field_encoder.go
deleted file mode 100644
index ce60d9a3..00000000
--- a/log/field_encoder.go
+++ /dev/null
@@ -1,379 +0,0 @@
-/*
- * Copyright 2025 The Go-Spring Authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package log
-
-import (
- "bytes"
- "encoding/json"
- "strconv"
- "unicode/utf8"
-)
-
-// Encoder is an interface that defines methods for appending structured data elements.
-type Encoder interface {
- AppendEncoderBegin()
- AppendEncoderEnd()
- AppendObjectBegin()
- AppendObjectEnd()
- AppendArrayBegin()
- AppendArrayEnd()
- AppendKey(key string)
- AppendBool(v bool)
- AppendInt64(v int64)
- AppendUint64(v uint64)
- AppendFloat64(v float64)
- AppendString(v string)
- AppendReflect(v interface{})
-}
-
-var (
- _ Encoder = (*JSONEncoder)(nil)
- _ Encoder = (*TextEncoder)(nil)
-)
-
-// jsonToken represents the current state of the encoder while building a JSON structure.
-type jsonToken int
-
-const (
- jsonTokenUnknown jsonToken = iota
- jsonTokenObjectBegin
- jsonTokenObjectEnd
- jsonTokenArrayBegin
- jsonTokenArrayEnd
- jsonTokenKey
- jsonTokenValue
-)
-
-// JSONEncoder is a simple JSON encoder.
-type JSONEncoder struct {
- buf *bytes.Buffer // Buffer to write JSON output.
- last jsonToken // The last token type written.
-}
-
-// NewJSONEncoder creates a new JSONEncoder.
-func NewJSONEncoder(buf *bytes.Buffer) *JSONEncoder {
- return &JSONEncoder{
- buf: buf,
- last: jsonTokenUnknown,
- }
-}
-
-// Reset resets the encoder's state.
-func (enc *JSONEncoder) Reset() {
- enc.last = jsonTokenUnknown
-}
-
-// AppendEncoderBegin writes the start of an encoder section.
-func (enc *JSONEncoder) AppendEncoderBegin() {
- enc.AppendObjectBegin()
-}
-
-// AppendEncoderEnd writes the end of an encoder section.
-func (enc *JSONEncoder) AppendEncoderEnd() {
- enc.AppendObjectEnd()
-}
-
-// AppendObjectBegin writes the beginning of a JSON object.
-func (enc *JSONEncoder) AppendObjectBegin() {
- enc.last = jsonTokenObjectBegin
- enc.buf.WriteByte('{')
-}
-
-// AppendObjectEnd writes the end of a JSON object.
-func (enc *JSONEncoder) AppendObjectEnd() {
- enc.last = jsonTokenObjectEnd
- enc.buf.WriteByte('}')
-}
-
-// AppendArrayBegin writes the beginning of a JSON array.
-func (enc *JSONEncoder) AppendArrayBegin() {
- enc.last = jsonTokenArrayBegin
- enc.buf.WriteByte('[')
-}
-
-// AppendArrayEnd writes the end of a JSON array.
-func (enc *JSONEncoder) AppendArrayEnd() {
- enc.last = jsonTokenArrayEnd
- enc.buf.WriteByte(']')
-}
-
-// appendSeparator writes a comma if the previous token requires separation (e.g., between values).
-func (enc *JSONEncoder) appendSeparator() {
- if enc.last == jsonTokenObjectEnd || enc.last == jsonTokenArrayEnd || enc.last == jsonTokenValue {
- enc.buf.WriteByte(',')
- }
-}
-
-// AppendKey writes a JSON key.
-func (enc *JSONEncoder) AppendKey(key string) {
- enc.appendSeparator()
- enc.last = jsonTokenKey
- enc.buf.WriteByte('"')
- enc.safeAddString(key)
- enc.buf.WriteByte('"')
- enc.buf.WriteByte(':')
-}
-
-// AppendBool writes a boolean value.
-func (enc *JSONEncoder) AppendBool(v bool) {
- enc.appendSeparator()
- enc.last = jsonTokenValue
- enc.buf.WriteString(strconv.FormatBool(v))
-}
-
-// AppendInt64 writes an int64 value.
-func (enc *JSONEncoder) AppendInt64(v int64) {
- enc.appendSeparator()
- enc.last = jsonTokenValue
- enc.buf.WriteString(strconv.FormatInt(v, 10))
-}
-
-// AppendUint64 writes an uint64 value.
-func (enc *JSONEncoder) AppendUint64(u uint64) {
- enc.appendSeparator()
- enc.last = jsonTokenValue
- enc.buf.WriteString(strconv.FormatUint(u, 10))
-}
-
-// AppendFloat64 writes a float64 value.
-func (enc *JSONEncoder) AppendFloat64(v float64) {
- enc.appendSeparator()
- enc.last = jsonTokenValue
- enc.buf.WriteString(strconv.FormatFloat(v, 'f', -1, 64))
-}
-
-// AppendString writes a string value with proper escaping.
-func (enc *JSONEncoder) AppendString(v string) {
- enc.appendSeparator()
- enc.last = jsonTokenValue
- enc.buf.WriteByte('"')
- enc.safeAddString(v)
- enc.buf.WriteByte('"')
-}
-
-// AppendReflect marshals any Go value into JSON and appends it.
-func (enc *JSONEncoder) AppendReflect(v interface{}) {
- enc.appendSeparator()
- enc.last = jsonTokenValue
- b, err := json.Marshal(v)
- if err != nil {
- enc.buf.WriteByte('"')
- enc.safeAddString(err.Error())
- enc.buf.WriteByte('"')
- return
- }
- enc.buf.Write(b)
-}
-
-// safeAddString escapes and writes a string according to JSON rules.
-func (enc *JSONEncoder) safeAddString(s string) {
- for i := 0; i < len(s); {
- // Try to add a single-byte (ASCII) character directly
- if enc.tryAddRuneSelf(s[i]) {
- i++
- continue
- }
- // Decode multi-byte UTF-8 character
- r, size := utf8.DecodeRuneInString(s[i:])
- // Handle invalid UTF-8 encoding
- if enc.tryAddRuneError(r, size) {
- i++
- continue
- }
- // Valid multi-byte rune; add as is
- enc.buf.WriteString(s[i : i+size])
- i += size
- }
-}
-
-// tryAddRuneSelf handles ASCII characters and escapes control/quote characters.
-func (enc *JSONEncoder) tryAddRuneSelf(b byte) bool {
- const _hex = "0123456789abcdef"
- if b >= utf8.RuneSelf {
- return false // not a single-byte rune
- }
- if 0x20 <= b && b != '\\' && b != '"' {
- enc.buf.WriteByte(b)
- return true
- }
- // Handle escaping
- switch b {
- case '\\', '"':
- enc.buf.WriteByte('\\')
- enc.buf.WriteByte(b)
- case '\n':
- enc.buf.WriteByte('\\')
- enc.buf.WriteByte('n')
- case '\r':
- enc.buf.WriteByte('\\')
- enc.buf.WriteByte('r')
- case '\t':
- enc.buf.WriteByte('\\')
- enc.buf.WriteByte('t')
- default:
- // Encode bytes < 0x20, except for the escape sequences above.
- enc.buf.WriteString(`\u00`)
- enc.buf.WriteByte(_hex[b>>4])
- enc.buf.WriteByte(_hex[b&0xF])
- }
- return true
-}
-
-// tryAddRuneError checks and escapes invalid UTF-8 runes.
-func (enc *JSONEncoder) tryAddRuneError(r rune, size int) bool {
- if r == utf8.RuneError && size == 1 {
- enc.buf.WriteString(`\ufffd`)
- return true
- }
- return false
-}
-
-// TextEncoder encodes key-value pairs in a plain text format,
-// optionally using JSON when inside objects/arrays.
-type TextEncoder struct {
- buf *bytes.Buffer // Buffer to write the encoded output
- separator string // Separator used between top-level key-value pairs
- jsonEncoder *JSONEncoder // Embedded JSON encoder for nested objects/arrays
- jsonDepth int8 // Tracks depth of nested JSON structures
- firstField bool // Tracks if the first key-value has been written
-}
-
-// NewTextEncoder creates a new TextEncoder, using the specified separator.
-func NewTextEncoder(buf *bytes.Buffer, separator string) *TextEncoder {
- return &TextEncoder{
- buf: buf,
- separator: separator,
- jsonEncoder: &JSONEncoder{buf: buf},
- }
-}
-
-// AppendEncoderBegin writes the start of an encoder section.
-func (enc *TextEncoder) AppendEncoderBegin() {}
-
-// AppendEncoderEnd writes the end of an encoder section.
-func (enc *TextEncoder) AppendEncoderEnd() {}
-
-// AppendObjectBegin signals the start of a JSON object.
-// Increments the depth and delegates to the JSON encoder.
-func (enc *TextEncoder) AppendObjectBegin() {
- enc.jsonDepth++
- enc.jsonEncoder.AppendObjectBegin()
-}
-
-// AppendObjectEnd signals the end of a JSON object.
-// Decrements the depth and resets the JSON encoder if back to top level.
-func (enc *TextEncoder) AppendObjectEnd() {
- enc.jsonDepth--
- enc.jsonEncoder.AppendObjectEnd()
- if enc.jsonDepth == 0 {
- enc.jsonEncoder.Reset()
- }
-}
-
-// AppendArrayBegin signals the start of a JSON array.
-// Increments the depth and delegates to the JSON encoder.
-func (enc *TextEncoder) AppendArrayBegin() {
- enc.jsonDepth++
- enc.jsonEncoder.AppendArrayBegin()
-}
-
-// AppendArrayEnd signals the end of a JSON array.
-// Decrements the depth and resets the JSON encoder if back to top level.
-func (enc *TextEncoder) AppendArrayEnd() {
- enc.jsonDepth--
- enc.jsonEncoder.AppendArrayEnd()
- if enc.jsonDepth == 0 {
- enc.jsonEncoder.Reset()
- }
-}
-
-// AppendKey appends a key for a key-value pair.
-// If inside a JSON structure, the key is handled by the JSON encoder.
-// Otherwise, it's written directly with proper separator handling.
-func (enc *TextEncoder) AppendKey(key string) {
- if enc.jsonDepth > 0 {
- enc.jsonEncoder.AppendKey(key)
- return
- }
- if enc.firstField {
- enc.buf.WriteString(enc.separator)
- } else {
- enc.firstField = true
- }
- enc.buf.WriteString(key)
- enc.buf.WriteByte('=')
-}
-
-// AppendBool appends a boolean value, using JSON encoder if nested.
-func (enc *TextEncoder) AppendBool(v bool) {
- if enc.jsonDepth > 0 {
- enc.jsonEncoder.AppendBool(v)
- return
- }
- enc.buf.WriteString(strconv.FormatBool(v))
-}
-
-// AppendInt64 appends an int64 value, using JSON encoder if nested.
-func (enc *TextEncoder) AppendInt64(v int64) {
- if enc.jsonDepth > 0 {
- enc.jsonEncoder.AppendInt64(v)
- return
- }
- enc.buf.WriteString(strconv.FormatInt(v, 10))
-}
-
-// AppendUint64 appends a uint64 value, using JSON encoder if nested.
-func (enc *TextEncoder) AppendUint64(v uint64) {
- if enc.jsonDepth > 0 {
- enc.jsonEncoder.AppendUint64(v)
- return
- }
- enc.buf.WriteString(strconv.FormatUint(v, 10))
-}
-
-// AppendFloat64 appends a float64 value, using JSON encoder if nested.
-func (enc *TextEncoder) AppendFloat64(v float64) {
- if enc.jsonDepth > 0 {
- enc.jsonEncoder.AppendFloat64(v)
- return
- }
- enc.buf.WriteString(strconv.FormatFloat(v, 'f', -1, 64))
-}
-
-// AppendString appends a string value, using JSON encoder if nested.
-func (enc *TextEncoder) AppendString(v string) {
- if enc.jsonDepth > 0 {
- enc.jsonEncoder.AppendString(v)
- return
- }
- enc.buf.WriteString(v)
-}
-
-// AppendReflect uses reflection to marshal any value as JSON.
-// If nested, delegates to JSON encoder.
-func (enc *TextEncoder) AppendReflect(v interface{}) {
- if enc.jsonDepth > 0 {
- enc.jsonEncoder.AppendReflect(v)
- return
- }
- b, err := json.Marshal(v)
- if err != nil {
- enc.AppendString(err.Error())
- return
- }
- enc.buf.Write(b)
-}
diff --git a/log/field_test.go b/log/field_test.go
deleted file mode 100644
index a260c6a0..00000000
--- a/log/field_test.go
+++ /dev/null
@@ -1,296 +0,0 @@
-/*
- * Copyright 2025 The Go-Spring Authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package log
-
-import (
- "bytes"
- "testing"
-
- "github.com/lvan100/go-assert"
-)
-
-func ptr[T any](i T) *T {
- return &i
-}
-
-var testFields = []Field{
- Msgf("hello %s", "中国"),
- Msg("hello world\n\\\t\"\r"),
- Any("null", nil),
- Any("bool", false),
- Any("bool_ptr", ptr(false)),
- Any("bool_ptr_nil", (*bool)(nil)),
- Any("bools", []bool{true, true, false}),
- Any("int", int(1)),
- Any("int_ptr", ptr(int(1))),
- Any("int_ptr_nil", (*int)(nil)),
- Any("int_slice", []int{int(1), int(2), int(3)}),
- Any("int8", int8(1)),
- Any("int8_ptr", ptr(int8(1))),
- Any("int8_ptr_nil", (*int8)(nil)),
- Any("int8_slice", []int8{int8(1), int8(2), int8(3)}),
- Any("int16", int16(1)),
- Any("int16_ptr", ptr(int16(1))),
- Any("int16_ptr_nil", (*int16)(nil)),
- Any("int16_slice", []int16{int16(1), int16(2), int16(3)}),
- Any("int32", int32(1)),
- Any("int32_ptr", ptr(int32(1))),
- Any("int32_ptr_nil", (*int32)(nil)),
- Any("int32_slice", []int32{int32(1), int32(2), int32(3)}),
- Any("int64", int64(1)),
- Any("int64_ptr", ptr(int64(1))),
- Any("int64_ptr_nil", (*int64)(nil)),
- Any("int64_slice", []int64{int64(1), int64(2), int64(3)}),
- Any("uint", uint(1)),
- Any("uint_ptr", ptr(uint(1))),
- Any("uint_ptr_nil", (*uint)(nil)),
- Any("uint_slice", []uint{uint(1), uint(2), uint(3)}),
- Any("uint8", uint8(1)),
- Any("uint8_ptr", ptr(uint8(1))),
- Any("uint8_ptr_nil", (*uint8)(nil)),
- Any("uint8_slice", []uint8{uint8(1), uint8(2), uint8(3)}),
- Any("uint16", uint16(1)),
- Any("uint16_ptr", ptr(uint16(1))),
- Any("uint16_ptr_nil", (*uint16)(nil)),
- Any("uint16_slice", []uint16{uint16(1), uint16(2), uint16(3)}),
- Any("uint32", uint32(1)),
- Any("uint32_ptr", ptr(uint32(1))),
- Any("uint32_ptr_nil", (*uint32)(nil)),
- Any("uint32_slice", []uint32{uint32(1), uint32(2), uint32(3)}),
- Any("uint64", uint64(1)),
- Any("uint64_ptr", ptr(uint64(1))),
- Any("uint64_ptr_nil", (*uint64)(nil)),
- Any("uint64_slice", []uint64{uint64(1), uint64(2), uint64(3)}),
- Any("float32", float32(1)),
- Any("float32_ptr", ptr(float32(1))),
- Any("float32_ptr_nil", (*float32)(nil)),
- Any("float32_slice", []float32{float32(1), float32(2), float32(3)}),
- Any("float64", float64(1)),
- Any("float64_ptr", ptr(float64(1))),
- Any("float64_ptr_nil", (*float64)(nil)),
- Any("float64_slice", []float64{float64(1), float64(2), float64(3)}),
- Any("string", "\x80\xC2\xED\xA0\x08"),
- Any("string_ptr", ptr("a")),
- Any("string_ptr_nil", (*string)(nil)),
- Any("string_slice", []string{"a", "b", "c"}),
- Array("array", Int64Value(1), Uint64Value(2), StringValue("a")),
- Object("object", Any("int64", int64(1)), Any("uint64", uint64(1)), Any("string", "a")),
- Any("struct", struct{ Int64 int64 }{10}),
-}
-
-func TestJSONEncoder(t *testing.T) {
-
- t.Run("chan error", func(t *testing.T) {
- buf := bytes.NewBuffer(nil)
- enc := NewJSONEncoder(buf)
- enc.AppendEncoderBegin()
- enc.AppendKey("chan")
- enc.AppendReflect(make(chan error))
- enc.AppendEncoderEnd()
- assert.ThatString(t, buf.String()).Equal(`{"chan":"json: unsupported type: chan error"}`)
- })
-
- t.Run("success", func(t *testing.T) {
- buf := bytes.NewBuffer(nil)
- enc := NewJSONEncoder(buf)
- enc.AppendEncoderBegin()
- WriteFields(enc, testFields)
- enc.AppendEncoderEnd()
- assert.ThatString(t, buf.String()).JsonEqual(`{
- "msg": "hello world\n\\\t\"\r",
- "null": null,
- "bool": false,
- "bool_ptr": false,
- "bool_ptr_nil": null,
- "bools": [
- true,
- true,
- false
- ],
- "int": 1,
- "int_ptr": 1,
- "int_ptr_nil": null,
- "int_slice": [
- 1,
- 2,
- 3
- ],
- "int8": 1,
- "int8_ptr": 1,
- "int8_ptr_nil": null,
- "int8_slice": [
- 1,
- 2,
- 3
- ],
- "int16": 1,
- "int16_ptr": 1,
- "int16_ptr_nil": null,
- "int16_slice": [
- 1,
- 2,
- 3
- ],
- "int32": 1,
- "int32_ptr": 1,
- "int32_ptr_nil": null,
- "int32_slice": [
- 1,
- 2,
- 3
- ],
- "int64": 1,
- "int64_ptr": 1,
- "int64_ptr_nil": null,
- "int64_slice": [
- 1,
- 2,
- 3
- ],
- "uint": 1,
- "uint_ptr": 1,
- "uint_ptr_nil": null,
- "uint_slice": [
- 1,
- 2,
- 3
- ],
- "uint8": 1,
- "uint8_ptr": 1,
- "uint8_ptr_nil": null,
- "uint8_slice": [
- 1,
- 2,
- 3
- ],
- "uint16": 1,
- "uint16_ptr": 1,
- "uint16_ptr_nil": null,
- "uint16_slice": [
- 1,
- 2,
- 3
- ],
- "uint32": 1,
- "uint32_ptr": 1,
- "uint32_ptr_nil": null,
- "uint32_slice": [
- 1,
- 2,
- 3
- ],
- "uint64": 1,
- "uint64_ptr": 1,
- "uint64_ptr_nil": null,
- "uint64_slice": [
- 1,
- 2,
- 3
- ],
- "float32": 1,
- "float32_ptr": 1,
- "float32_ptr_nil": null,
- "float32_slice": [
- 1,
- 2,
- 3
- ],
- "float64": 1,
- "float64_ptr": 1,
- "float64_ptr_nil": null,
- "float64_slice": [
- 1,
- 2,
- 3
- ],
- "string": "\ufffd\ufffd\ufffd\ufffd\u0008",
- "string_ptr": "a",
- "string_ptr_nil": null,
- "string_slice": [
- "a",
- "b",
- "c"
- ],
- "array": [
- 1,
- 2,
- "a"
- ],
- "object": {
- "int64": 1,
- "uint64": 1,
- "string": "a"
- },
- "struct": {
- "Int64": 10
- }
- }`)
- })
-}
-
-func TestTextEncoder(t *testing.T) {
-
- t.Run("chan error", func(t *testing.T) {
- buf := bytes.NewBuffer(nil)
- enc := NewTextEncoder(buf, "||")
- enc.AppendEncoderBegin()
- enc.AppendKey("chan")
- enc.AppendReflect(make(chan error))
- enc.AppendEncoderEnd()
- assert.ThatString(t, buf.String()).Equal("chan=json: unsupported type: chan error")
- })
-
- t.Run("success", func(t *testing.T) {
- buf := bytes.NewBuffer(nil)
- enc := NewTextEncoder(buf, "||")
- enc.AppendEncoderBegin()
- WriteFields(enc, testFields)
- {
- enc.AppendKey("object_2")
- enc.AppendObjectBegin()
- enc.AppendKey("map")
- enc.AppendReflect(map[string]int{"a": 1})
- enc.AppendObjectEnd()
- }
- {
- enc.AppendKey("array_2")
- enc.AppendArrayBegin()
- enc.AppendReflect(map[string]int{"a": 1})
- enc.AppendReflect(map[string]int{"a": 1})
- enc.AppendArrayEnd()
- }
- enc.AppendEncoderEnd()
- const expect = "msg=hello 中国||msg=hello world\n\\\t\"\r||null=null||" +
- `bool=false||bool_ptr=false||bool_ptr_nil=null||bools=[true,true,false]||` +
- `int=1||int_ptr=1||int_ptr_nil=null||int_slice=[1,2,3]||` +
- `int8=1||int8_ptr=1||int8_ptr_nil=null||int8_slice=[1,2,3]||` +
- `int16=1||int16_ptr=1||int16_ptr_nil=null||int16_slice=[1,2,3]||` +
- `int32=1||int32_ptr=1||int32_ptr_nil=null||int32_slice=[1,2,3]||` +
- `int64=1||int64_ptr=1||int64_ptr_nil=null||int64_slice=[1,2,3]||` +
- `uint=1||uint_ptr=1||uint_ptr_nil=null||uint_slice=[1,2,3]||` +
- `uint8=1||uint8_ptr=1||uint8_ptr_nil=null||uint8_slice=[1,2,3]||` +
- `uint16=1||uint16_ptr=1||uint16_ptr_nil=null||uint16_slice=[1,2,3]||` +
- `uint32=1||uint32_ptr=1||uint32_ptr_nil=null||uint32_slice=[1,2,3]||` +
- `uint64=1||uint64_ptr=1||uint64_ptr_nil=null||uint64_slice=[1,2,3]||` +
- `float32=1||float32_ptr=1||float32_ptr_nil=null||float32_slice=[1,2,3]||` +
- `float64=1||float64_ptr=1||float64_ptr_nil=null||float64_slice=[1,2,3]||` +
- `string=` + "\x80\xC2\xED\xA0\x08" + `||string_ptr=a||string_ptr_nil=null||string_slice=["a","b","c"]||` +
- `array=[1,2,"a"]||object={"int64":1,"uint64":1,"string":"a"}||struct={"Int64":10}||` +
- `object_2={"map":{"a":1}}||array_2=[{"a":1},{"a":1}]`
- assert.ThatString(t, buf.String()).Equal(expect)
- })
-}
diff --git a/log/field_value.go b/log/field_value.go
deleted file mode 100644
index 176d5756..00000000
--- a/log/field_value.go
+++ /dev/null
@@ -1,270 +0,0 @@
-/*
- * Copyright 2025 The Go-Spring Authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package log
-
-// Value is an interface for types that can encode themselves using an Encoder.
-type Value interface {
- Encode(enc Encoder)
-}
-
-// BoolValue represents a bool carried by Field.
-type BoolValue bool
-
-// Encode encodes the data represented by v to an Encoder.
-func (v BoolValue) Encode(enc Encoder) {
- enc.AppendBool(bool(v))
-}
-
-// Int64Value represents an int64 carried by Field.
-type Int64Value int64
-
-// Encode encodes the data represented by v to an Encoder.
-func (v Int64Value) Encode(enc Encoder) {
- enc.AppendInt64(int64(v))
-}
-
-// Uint64Value represents an uint64 carried by Field.
-type Uint64Value uint64
-
-// Encode encodes the data represented by v to an Encoder.
-func (v Uint64Value) Encode(enc Encoder) {
- enc.AppendUint64(uint64(v))
-}
-
-// Float64Value represents a float64 carried by Field.
-type Float64Value float64
-
-// Encode encodes the data represented by v to an Encoder.
-func (v Float64Value) Encode(enc Encoder) {
- enc.AppendFloat64(float64(v))
-}
-
-// StringValue represents a string carried by Field.
-type StringValue string
-
-// Encode encodes the data represented by v to an Encoder.
-func (v StringValue) Encode(enc Encoder) {
- enc.AppendString(string(v))
-}
-
-// ReflectValue represents an interface{} carried by Field.
-type ReflectValue struct {
- Val interface{}
-}
-
-// Encode encodes the data represented by v to an Encoder.
-func (v ReflectValue) Encode(enc Encoder) {
- enc.AppendReflect(v.Val)
-}
-
-// BoolsValue represents a slice of bool carried by Field.
-type BoolsValue []bool
-
-// Encode encodes the data represented by v to an Encoder.
-func (v BoolsValue) Encode(enc Encoder) {
- enc.AppendArrayBegin()
- for _, val := range v {
- enc.AppendBool(val)
- }
- enc.AppendArrayEnd()
-}
-
-// IntsValue represents a slice of int carried by Field.
-type IntsValue []int
-
-// Encode encodes the data represented by v to an Encoder.
-func (v IntsValue) Encode(enc Encoder) {
- enc.AppendArrayBegin()
- for _, val := range v {
- enc.AppendInt64(int64(val))
- }
- enc.AppendArrayEnd()
-}
-
-// Int8sValue represents a slice of int8 carried by Field.
-type Int8sValue []int8
-
-// Encode encodes the data represented by v to an Encoder.
-func (v Int8sValue) Encode(enc Encoder) {
- enc.AppendArrayBegin()
- for _, val := range v {
- enc.AppendInt64(int64(val))
- }
- enc.AppendArrayEnd()
-}
-
-// Int16sValue represents a slice of int16 carried by Field.
-type Int16sValue []int16
-
-// Encode encodes the data represented by v to an Encoder.
-func (v Int16sValue) Encode(enc Encoder) {
- enc.AppendArrayBegin()
- for _, val := range v {
- enc.AppendInt64(int64(val))
- }
- enc.AppendArrayEnd()
-}
-
-// Int32sValue represents a slice of int32 carried by Field.
-type Int32sValue []int32
-
-// Encode encodes the data represented by v to an Encoder.
-func (v Int32sValue) Encode(enc Encoder) {
- enc.AppendArrayBegin()
- for _, val := range v {
- enc.AppendInt64(int64(val))
- }
- enc.AppendArrayEnd()
-}
-
-// Int64sValue represents a slice of int64 carried by Field.
-type Int64sValue []int64
-
-// Encode encodes the data represented by v to an Encoder.
-func (v Int64sValue) Encode(enc Encoder) {
- enc.AppendArrayBegin()
- for _, val := range v {
- enc.AppendInt64(val)
- }
- enc.AppendArrayEnd()
-}
-
-// UintsValue represents a slice of uint carried by Field.
-type UintsValue []uint
-
-// Encode encodes the data represented by v to an Encoder.
-func (v UintsValue) Encode(enc Encoder) {
- enc.AppendArrayBegin()
- for _, val := range v {
- enc.AppendUint64(uint64(val))
- }
- enc.AppendArrayEnd()
-}
-
-// Uint8sValue represents a slice of uint8 carried by Field.
-type Uint8sValue []uint8
-
-// Encode encodes the data represented by v to an Encoder.
-func (v Uint8sValue) Encode(enc Encoder) {
- enc.AppendArrayBegin()
- for _, val := range v {
- enc.AppendUint64(uint64(val))
- }
- enc.AppendArrayEnd()
-}
-
-// Uint16sValue represents a slice of uint16 carried by Field.
-type Uint16sValue []uint16
-
-// Encode encodes the data represented by v to an Encoder.
-func (v Uint16sValue) Encode(enc Encoder) {
- enc.AppendArrayBegin()
- for _, val := range v {
- enc.AppendUint64(uint64(val))
- }
- enc.AppendArrayEnd()
-}
-
-// Uint32sValue represents a slice of uint32 carried by Field.
-type Uint32sValue []uint32
-
-// Encode encodes the data represented by v to an Encoder.
-func (v Uint32sValue) Encode(enc Encoder) {
- enc.AppendArrayBegin()
- for _, val := range v {
- enc.AppendUint64(uint64(val))
- }
- enc.AppendArrayEnd()
-}
-
-// Uint64sValue represents a slice of uint64 carried by Field.
-type Uint64sValue []uint64
-
-// Encode encodes the data represented by v to an Encoder.
-func (v Uint64sValue) Encode(enc Encoder) {
- enc.AppendArrayBegin()
- for _, val := range v {
- enc.AppendUint64(val)
- }
- enc.AppendArrayEnd()
-}
-
-// Float32sValue represents a slice of float32 carried by Field.
-type Float32sValue []float32
-
-// Encode encodes the data represented by v to an Encoder.
-func (v Float32sValue) Encode(enc Encoder) {
- enc.AppendArrayBegin()
- for _, val := range v {
- enc.AppendFloat64(float64(val))
- }
- enc.AppendArrayEnd()
-}
-
-// Float64sValue represents a slice of float64 carried by Field.
-type Float64sValue []float64
-
-// Encode encodes the data represented by v to an Encoder.
-func (v Float64sValue) Encode(enc Encoder) {
- enc.AppendArrayBegin()
- for _, val := range v {
- enc.AppendFloat64(val)
- }
- enc.AppendArrayEnd()
-}
-
-// StringsValue represents a slice of string carried by Field.
-type StringsValue []string
-
-// Encode encodes the data represented by v to an Encoder.
-func (v StringsValue) Encode(enc Encoder) {
- enc.AppendArrayBegin()
- for _, val := range v {
- enc.AppendString(val)
- }
- enc.AppendArrayEnd()
-}
-
-// ArrayValue represents a slice of Value carried by Field.
-type ArrayValue []Value
-
-// Encode encodes the data represented by v to an Encoder.
-func (v ArrayValue) Encode(enc Encoder) {
- enc.AppendArrayBegin()
- for _, val := range v {
- val.Encode(enc)
- }
- enc.AppendArrayEnd()
-}
-
-// ObjectValue represents a slice of Field carried by Field.
-type ObjectValue []Field
-
-// Encode encodes the data represented by v to an Encoder.
-func (v ObjectValue) Encode(enc Encoder) {
- enc.AppendObjectBegin()
- WriteFields(enc, v)
- enc.AppendObjectEnd()
-}
-
-// WriteFields writes a slice of Field objects to the encoder.
-func WriteFields(enc Encoder, fields []Field) {
- for _, f := range fields {
- enc.AppendKey(f.Key)
- f.Val.Encode(enc)
- }
-}
diff --git a/log/internal/caller.go b/log/internal/caller.go
deleted file mode 100644
index 897c9d6a..00000000
--- a/log/internal/caller.go
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright 2025 The Go-Spring Authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package internal
-
-import (
- "runtime"
- "sync"
-)
-
-// frameMap is used to cache call site information.
-// Benchmarking shows that using this cache improves performance by about 50%.
-var frameMap sync.Map
-
-// Caller returns the file name and line number of the calling function.
-// If 'fast' is true, it uses a cache to speed up the lookup.
-func Caller(skip int, fast bool) (file string, line int) {
-
- if !fast {
- _, file, line, _ = runtime.Caller(skip + 1)
- return
- }
-
- rpc := make([]uintptr, 1)
- n := runtime.Callers(skip+2, rpc[:])
- if n < 1 {
- return
- }
- pc := rpc[0]
- if v, ok := frameMap.Load(pc); ok {
- e := v.(*runtime.Frame)
- return e.File, e.Line
- }
- frame, _ := runtime.CallersFrames(rpc).Next()
- frameMap.Store(pc, &frame)
- return frame.File, frame.Line
-}
diff --git a/log/internal/caller_test.go b/log/internal/caller_test.go
deleted file mode 100644
index 253de7a0..00000000
--- a/log/internal/caller_test.go
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright 2025 The Go-Spring Authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package internal_test
-
-import (
- "testing"
-
- "github.com/go-spring/spring-core/log/internal"
- "github.com/lvan100/go-assert"
-)
-
-func TestCaller(t *testing.T) {
-
- t.Run("error skip", func(t *testing.T) {
- file, line := internal.Caller(100, true)
- assert.ThatString(t, file).Equal("")
- assert.That(t, line).Equal(0)
- })
-
- t.Run("fast false", func(t *testing.T) {
- file, line := internal.Caller(0, false)
- assert.ThatString(t, file).Matches(".*/caller_test.go")
- assert.That(t, line).Equal(35)
- })
-
- t.Run("fast true", func(t *testing.T) {
- for i := 0; i < 2; i++ {
- file, line := internal.Caller(0, true)
- assert.ThatString(t, file).Matches(".*/caller_test.go")
- assert.That(t, line).Equal(42)
- }
- })
-}
-
-func BenchmarkCaller(b *testing.B) {
-
- // BenchmarkCaller/fast-8 12433761 95.05 ns/op
- // BenchmarkCaller/slow-8 6314623 190.3 ns/op
-
- b.Run("fast", func(b *testing.B) {
- for i := 0; i < b.N; i++ {
- internal.Caller(0, true)
- }
- })
-
- b.Run("slow", func(b *testing.B) {
- for i := 0; i < b.N; i++ {
- internal.Caller(0, false)
- }
- })
-}
diff --git a/log/log.go b/log/log.go
deleted file mode 100644
index 6d03af9e..00000000
--- a/log/log.go
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * Copyright 2025 The Go-Spring Authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package log
-
-import (
- "context"
- "time"
-
- "github.com/go-spring/spring-core/log/internal"
-)
-
-// TimeNow is a function that can be overridden to provide custom timestamp behavior (e.g., for testing).
-var TimeNow func(ctx context.Context) time.Time
-
-// StringFromContext can be set to extract a string from the context.
-var StringFromContext func(ctx context.Context) string
-
-// FieldsFromContext can be set to extract structured fields from the context (e.g., trace IDs, user IDs).
-var FieldsFromContext func(ctx context.Context) []Field
-
-// Trace logs a message at TraceLevel using a tag and a lazy field-generating function.
-func Trace(ctx context.Context, tag *Tag, fn func() []Field) {
- if tag.GetLogger().enableLevel(TraceLevel) {
- Record(ctx, TraceLevel, tag, fn()...)
- }
-}
-
-// Debug logs a message at DebugLevel using a tag and a lazy field-generating function.
-func Debug(ctx context.Context, tag *Tag, fn func() []Field) {
- if tag.GetLogger().enableLevel(DebugLevel) {
- Record(ctx, DebugLevel, tag, fn()...)
- }
-}
-
-// Info logs a message at InfoLevel using structured fields.
-func Info(ctx context.Context, tag *Tag, fields ...Field) {
- Record(ctx, InfoLevel, tag, fields...)
-}
-
-// Warn logs a message at WarnLevel using structured fields.
-func Warn(ctx context.Context, tag *Tag, fields ...Field) {
- Record(ctx, WarnLevel, tag, fields...)
-}
-
-// Error logs a message at ErrorLevel using structured fields.
-func Error(ctx context.Context, tag *Tag, fields ...Field) {
- Record(ctx, ErrorLevel, tag, fields...)
-}
-
-// Panic logs a message at PanicLevel using structured fields.
-func Panic(ctx context.Context, tag *Tag, fields ...Field) {
- Record(ctx, PanicLevel, tag, fields...)
-}
-
-// Fatal logs a message at FatalLevel using structured fields.
-func Fatal(ctx context.Context, tag *Tag, fields ...Field) {
- Record(ctx, FatalLevel, tag, fields...)
-}
-
-// Record is the core function that handles publishing log events.
-// It checks the logger level, captures caller information, gathers context fields,
-// and sends the log event to the logger.
-func Record(ctx context.Context, level Level, tag *Tag, fields ...Field) {
- logger := tag.GetLogger()
- if !logger.enableLevel(level) {
- return // Skip if the logger doesn't allow this level
- }
-
- file, line := internal.Caller(2, true)
-
- // Determine the log timestamp
- now := time.Now()
- if TimeNow != nil {
- now = TimeNow(ctx)
- }
-
- // Extract a string from the context
- var ctxString string
- if StringFromContext != nil {
- ctxString = StringFromContext(ctx)
- }
-
- // Extract contextual fields from the context
- var ctxFields []Field
- if FieldsFromContext != nil {
- ctxFields = FieldsFromContext(ctx)
- }
-
- e := GetEvent()
- e.Level = level
- e.Time = now
- e.File = file
- e.Line = line
- e.Tag = tag.GetName()
- e.Fields = fields
- e.CtxString = ctxString
- e.CtxFields = ctxFields
-
- logger.publish(e)
-}
diff --git a/log/log_event.go b/log/log_event.go
deleted file mode 100644
index 66b440d2..00000000
--- a/log/log_event.go
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright 2025 The Go-Spring Authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package log
-
-import (
- "sync"
- "time"
-)
-
-var eventPool = sync.Pool{
- New: func() any {
- return &Event{}
- },
-}
-
-// Event provides contextual information about a log message.
-type Event struct {
- Level Level // The severity level of the log (e.g., INFO, ERROR, DEBUG)
- Time time.Time // The timestamp when the event occurred
- File string // The source file where the log was triggered
- Line int // The line number in the source file
- Tag string // A tag used to categorize the log (e.g., subsystem name)
- Fields []Field // Custom fields provided specifically for this log event
- CtxString string // The string representation of the context
- CtxFields []Field // Additional fields derived from the context (e.g., request ID, user ID)
-}
-
-func (e *Event) Reset() {
- e.Level = NoneLevel
- e.Time = time.Time{}
- e.File = ""
- e.Line = 0
- e.Tag = ""
- e.Fields = nil
- e.CtxString = ""
- e.CtxFields = nil
-}
-
-func GetEvent() *Event {
- return eventPool.Get().(*Event)
-}
-
-func PutEvent(e *Event) {
- e.Reset()
- eventPool.Put(e)
-}
diff --git a/log/log_level.go b/log/log_level.go
deleted file mode 100644
index c8b38fbd..00000000
--- a/log/log_level.go
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * Copyright 2025 The Go-Spring Authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package log
-
-import (
- "fmt"
- "strings"
-)
-
-func init() {
- RegisterConverter(ParseLevel)
-}
-
-const (
- NoneLevel Level = iota // No logging
- TraceLevel // Very detailed logging, typically for debugging at a granular level
- DebugLevel // Debugging information
- InfoLevel // General informational messages
- WarnLevel // Warnings that may indicate a potential problem
- ErrorLevel // Errors that allow the application to continue running
- PanicLevel // Severe issues that may lead to a panic
- FatalLevel // Critical issues that will cause application termination
-)
-
-// Level is an enumeration used to identify the severity of a logging event.
-type Level int32
-
-func (level Level) String() string {
- switch level {
- case NoneLevel:
- return "NONE"
- case TraceLevel:
- return "TRACE"
- case DebugLevel:
- return "DEBUG"
- case InfoLevel:
- return "INFO"
- case WarnLevel:
- return "WARN"
- case ErrorLevel:
- return "ERROR"
- case PanicLevel:
- return "PANIC"
- case FatalLevel:
- return "FATAL"
- default:
- return "INVALID"
- }
-}
-
-// ParseLevel converts a string (case-insensitive) into a corresponding Level value.
-// Returns an error if the input string does not match any valid level.
-func ParseLevel(str string) (Level, error) {
- switch strings.ToUpper(str) {
- case "NONE":
- return NoneLevel, nil
- case "TRACE":
- return TraceLevel, nil
- case "DEBUG":
- return DebugLevel, nil
- case "INFO":
- return InfoLevel, nil
- case "WARN":
- return WarnLevel, nil
- case "ERROR":
- return ErrorLevel, nil
- case "PANIC":
- return PanicLevel, nil
- case "FATAL":
- return FatalLevel, nil
- default:
- return -1, fmt.Errorf("invalid level %s", str)
- }
-}
diff --git a/log/log_reader.go b/log/log_reader.go
deleted file mode 100644
index ef6f79e8..00000000
--- a/log/log_reader.go
+++ /dev/null
@@ -1,139 +0,0 @@
-/*
- * Copyright 2025 The Go-Spring Authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package log
-
-import (
- "bytes"
- "encoding/xml"
- "errors"
- "io"
- "strings"
-
- "github.com/go-spring/spring-core/util"
-)
-
-var readers = map[string]Reader{}
-
-func init() {
- RegisterReader(new(XMLReader), ".xml")
-}
-
-// Node represents a parsed XML element with a label (tag name), child nodes,
-// and a map of attributes.
-type Node struct {
- Label string // Tag name of the XML element
- Children []*Node // Child elements (nested tags)
- Attributes map[string]string // Attributes of the XML element
- Text string // Text content of the XML element
-}
-
-// getChildren returns a slice of child nodes with a specific label.
-func (node *Node) getChildren(label string) []*Node {
- var ret []*Node
- for _, c := range node.Children {
- if c.Label == label {
- ret = append(ret, c)
- }
- }
- return ret
-}
-
-// DumpNode prints the structure of a Node to a buffer.
-func DumpNode(node *Node, indent int, buf *bytes.Buffer) {
- for i := 0; i < indent; i++ {
- buf.WriteString("\t")
- }
- buf.WriteString(node.Label)
- if len(node.Attributes) > 0 {
- buf.WriteString(" {")
- for i, k := range util.OrderedMapKeys(node.Attributes) {
- if i > 0 {
- buf.WriteString(" ")
- }
- buf.WriteString(k)
- buf.WriteString("=")
- buf.WriteString(node.Attributes[k])
- }
- buf.WriteString("}")
- }
- if node.Text != "" {
- buf.WriteString(" : ")
- buf.WriteString(node.Text)
- }
- for _, c := range node.Children {
- buf.WriteString("\n")
- DumpNode(c, indent+1, buf)
- }
-}
-
-// Reader is an interface for reading and parsing data into a Node structure.
-type Reader interface {
- Read(b []byte) (*Node, error)
-}
-
-// RegisterReader registers a Reader for one or more file extensions.
-// This allows dynamic selection of parsers based on file type.
-func RegisterReader(r Reader, ext ...string) {
- for _, s := range ext {
- readers[s] = r
- }
-}
-
-// XMLReader is an implementation of the Reader interface that parses XML data.
-type XMLReader struct{}
-
-// Read parses XML bytes into a tree of Nodes.
-// It uses a stack to track the current position in the XML hierarchy.
-func (r *XMLReader) Read(b []byte) (*Node, error) {
- stack := []*Node{{Label: "<>"}}
- d := xml.NewDecoder(bytes.NewReader(b))
- for {
- token, err := d.Token()
- if err == io.EOF {
- break
- }
- if err != nil {
- return nil, err
- }
- switch t := token.(type) {
- case xml.StartElement:
- curr := &Node{
- Label: t.Name.Local,
- Attributes: make(map[string]string),
- }
- for _, attr := range t.Attr {
- curr.Attributes[attr.Name.Local] = attr.Value
- }
- stack = append(stack, curr)
- case xml.CharData:
- if text := strings.TrimSpace(string(t)); text != "" {
- curr := stack[len(stack)-1]
- curr.Text += text
- }
- case xml.EndElement:
- curr := stack[len(stack)-1]
- parent := stack[len(stack)-2]
- parent.Children = append(parent.Children, curr)
- stack = stack[:len(stack)-1]
- default:
- }
- }
- if len(stack[0].Children) == 0 {
- return nil, errors.New("invalid XML structure: missing root element")
- }
- return stack[0].Children[0], nil
-}
diff --git a/log/log_reader_test.go b/log/log_reader_test.go
deleted file mode 100644
index 2ae1e25e..00000000
--- a/log/log_reader_test.go
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * Copyright 2025 The Go-Spring Authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package log
-
-import (
- "bytes"
- "testing"
-
- "github.com/lvan100/go-assert"
-)
-
-func TestXMLReader(t *testing.T) {
-
- t.Run("empty", func(t *testing.T) {
- reader := XMLReader{}
- _, err := reader.Read([]byte(``))
- assert.ThatError(t, err).Matches("invalid XML structure: missing root element")
- })
-
- t.Run("invalid", func(t *testing.T) {
- reader := XMLReader{}
- _, err := reader.Read([]byte(`<>`))
- assert.ThatError(t, err).Matches("XML syntax error on line 1: .*")
- })
-
- t.Run("success", func(t *testing.T) {
- reader := XMLReader{}
- node, err := reader.Read([]byte(`
-
-
-
- 1048576
- foo,bar
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- `))
- assert.Nil(t, err)
-
- buf := bytes.NewBuffer(nil)
- buf.WriteString("\n")
- DumpNode(node, 3, buf)
- assert.ThatString(t, buf.String()).Equal(`
- Configuration
- Properties
- Property {name=MaxBufferSize} : 1048576
- Property {name=Dummy} : foo,bar
- Appenders
- Console {name=Console_JSON}
- JSONLayout
- Console {name=Console_Text}
- TextLayout
- Loggers
- Root {level=trace}
- AppenderRef {ref=Console_Text}
- Logger {level=trace name=file tags=_com_request_*}
- AppenderRef {ref=Console_JSON}`)
- })
-}
diff --git a/log/log_refresh.go b/log/log_refresh.go
deleted file mode 100644
index 089e6c59..00000000
--- a/log/log_refresh.go
+++ /dev/null
@@ -1,233 +0,0 @@
-/*
- * Copyright 2025 The Go-Spring Authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package log
-
-import (
- "errors"
- "fmt"
- "io"
- "os"
- "path/filepath"
- "regexp"
- "strings"
- "sync/atomic"
-
- "github.com/go-spring/spring-core/util"
- "github.com/go-spring/spring-core/util/errutil"
-)
-
-var initOnce atomic.Bool
-
-// RefreshFile loads a logging configuration from a file by its name.
-func RefreshFile(fileName string) error {
- file, err := os.Open(fileName)
- if err != nil {
- return err
- }
- defer func() {
- _ = file.Close()
- }()
- ext := filepath.Ext(fileName)
- return RefreshReader(file, ext)
-}
-
-// RefreshReader reads the configuration from an io.Reader using the reader for the given extension.
-func RefreshReader(input io.Reader, ext string) error {
- if !initOnce.CompareAndSwap(false, true) {
- return errors.New("RefreshReader: log refresh already done")
- }
-
- var rootNode *Node
- {
- r, ok := readers[ext]
- if !ok {
- return fmt.Errorf("RefreshReader: unsupported file type %s", ext)
- }
- data, err := io.ReadAll(input)
- if err != nil {
- return err
- }
- rootNode, err = r.Read(data)
- if err != nil {
- return err
- }
- }
-
- if rootNode.Label != "Configuration" {
- return errors.New("RefreshReader: the Configuration root not found")
- }
-
- var (
- cRoot *Logger
- cLoggers = make(map[string]*Logger)
- cAppenders = make(map[string]Appender)
- cTags = make(map[string]*Logger)
- properties = make(map[string]string)
- )
-
- // Parse section
- nodes := rootNode.getChildren("Properties")
- if len(nodes) > 1 {
- return errors.New("RefreshReader: section must be unique")
- }
- for _, c := range nodes[0].Children {
- if c.Label != "Property" {
- continue
- }
- name, ok := c.Attributes["name"]
- if !ok {
- return errors.New("RefreshReader: attribute 'name' not found")
- }
- properties[name] = c.Text
- }
-
- // Parse section
- nodes = rootNode.getChildren("Appenders")
- if len(nodes) == 0 {
- return errors.New("RefreshReader: section not found")
- }
- if len(nodes) > 1 {
- return errors.New("RefreshReader: section must be unique")
- }
- for _, c := range nodes[0].Children {
- p, ok := plugins[c.Label]
- if !ok {
- return fmt.Errorf("RefreshReader: plugin %s not found", c.Label)
- }
- name, ok := c.Attributes["name"]
- if !ok {
- return errors.New("RefreshReader: attribute 'name' not found")
- }
- v, err := NewPlugin(p.Class, c, properties)
- if err != nil {
- return err
- }
- cAppenders[name] = v.Interface().(Appender)
- }
-
- // Parse section
- nodes = rootNode.getChildren("Loggers")
- if len(nodes) == 0 {
- return errors.New("RefreshReader: section not found")
- }
- if len(nodes) > 1 {
- return errors.New("RefreshReader: section must be unique")
- }
- for _, c := range nodes[0].Children {
- isRootLogger := c.Label == "Root" || c.Label == "AsyncRoot"
- if isRootLogger {
- if cRoot != nil {
- return errors.New("RefreshReader: found more than one root loggers")
- }
- c.Attributes["name"] = ""
- }
-
- p, ok := plugins[c.Label]
- if !ok || p == nil {
- return fmt.Errorf("RefreshReader: plugin %s not found", c.Label)
- }
- name, ok := c.Attributes["name"]
- if !ok {
- return errors.New("RefreshReader: attribute 'name' not found")
- }
- v, err := NewPlugin(p.Class, c, properties)
- if err != nil {
- return err
- }
-
- logger := &Logger{v.Interface().(privateConfig)}
- if isRootLogger {
- cRoot = logger
- }
- cLoggers[name] = logger
-
- var base *baseLoggerConfig
- switch config := v.Interface().(type) {
- case *LoggerConfig:
- base = &config.baseLoggerConfig
- case *AsyncLoggerConfig:
- base = &config.baseLoggerConfig
- }
-
- for _, r := range base.AppenderRefs {
- appender, ok := cAppenders[r.Ref]
- if !ok {
- return fmt.Errorf("RefreshReader: appender %s not found", r.Ref)
- }
- r.appender = appender
- }
-
- if isRootLogger {
- if base.Tags != "" {
- return fmt.Errorf("RefreshReader: root logger can not have tags attribute")
- }
- } else {
- if base.Tags == "" {
- return fmt.Errorf("RefreshReader: logger must have tags attribute except root logger")
- }
- ss := strings.Split(base.Tags, ",")
- for _, s := range ss {
- if s = strings.TrimSpace(s); s == "" {
- return fmt.Errorf("RefreshReader: logger tag can not be empty")
- }
- cTags[s] = logger
- }
- }
- }
-
- if cRoot == nil {
- return errors.New("found no root logger")
- }
-
- var (
- logArray []*Logger
- tagArray []*regexp.Regexp
- )
-
- for _, s := range util.OrderedMapKeys(cTags) {
- r, err := regexp.Compile(s)
- if err != nil {
- return errutil.WrapError(err, "RefreshReader: `%s` regexp compile error", s)
- }
- tagArray = append(tagArray, r)
- logArray = append(logArray, cTags[s])
- }
-
- for _, a := range cAppenders {
- if err := a.Start(); err != nil {
- return err
- }
- }
- for _, l := range cLoggers {
- if err := l.Start(); err != nil {
- return err
- }
- }
-
- for s, tag := range tagMap {
- logger := cRoot
- for i, r := range tagArray {
- if r.MatchString(s) {
- logger = logArray[i]
- break
- }
- }
- tag.SetLogger(logger)
- }
-
- return nil
-}
diff --git a/log/log_tag.go b/log/log_tag.go
deleted file mode 100644
index cac77019..00000000
--- a/log/log_tag.go
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Copyright 2025 The Go-Spring Authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package log
-
-import (
- "sync/atomic"
-)
-
-var tagMap = map[string]*Tag{}
-
-var initLogger = &Logger{
- privateConfig: &LoggerConfig{
- baseLoggerConfig: baseLoggerConfig{
- Level: InfoLevel,
- AppenderRefs: []*AppenderRef{
- {
- appender: &ConsoleAppender{
- BaseAppender: BaseAppender{
- Layout: &TextLayout{
- BaseLayout: BaseLayout{
- BufferSize: 500 * 1024,
- FileLineLength: 48,
- },
- },
- },
- },
- },
- },
- },
- },
-}
-
-// Tag is a struct representing a named logging tag.
-// It holds a pointer to a Logger and a string identifier.
-type Tag struct {
- v atomic.Pointer[Logger]
- s string
-}
-
-// GetName returns the name of the tag.
-func (m *Tag) GetName() string {
- return m.s
-}
-
-// GetLogger returns the Logger associated with this tag.
-// It uses atomic loading to ensure safe concurrent access.
-func (m *Tag) GetLogger() *Logger {
- return m.v.Load()
-}
-
-// SetLogger sets or replaces the Logger associated with this tag.
-// Uses atomic storing to ensure thread safety.
-func (m *Tag) SetLogger(logger *Logger) {
- m.v.Store(logger)
-}
-
-// GetTag creates or retrieves a Tag by name.
-// If the tag does not exist, it is created and added to the global registry.
-func GetTag(tag string) *Tag {
- m, ok := tagMap[tag]
- if !ok {
- m = &Tag{s: tag}
- m.v.Store(initLogger)
- tagMap[tag] = m
- }
- return m
-}
diff --git a/log/log_test.go b/log/log_test.go
deleted file mode 100644
index f7256ef0..00000000
--- a/log/log_test.go
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * Copyright 2025 The Go-Spring Authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package log_test
-
-import (
- "context"
- "strings"
- "testing"
-
- "github.com/go-spring/spring-core/log"
- "github.com/lvan100/go-assert"
-)
-
-var TagDefault = log.GetTag("_def")
-var TagRequestIn = log.GetTag("_com_request_in")
-var TagRequestOut = log.GetTag("_com_request_out")
-
-func TestLog(t *testing.T) {
- ctx := t.Context()
-
- log.StringFromContext = func(ctx context.Context) string {
- return ""
- }
-
- log.FieldsFromContext = func(ctx context.Context) []log.Field {
- traceID, _ := ctx.Value("trace_id").(string)
- spanID, _ := ctx.Value("span_id").(string)
- return []log.Field{
- log.String("trace_id", traceID),
- log.String("span_id", spanID),
- }
- }
-
- log.Debug(ctx, TagRequestOut, func() []log.Field {
- return []log.Field{
- log.Msgf("hello %s", "world"),
- }
- })
-
- log.Info(ctx, TagDefault, log.Msgf("hello %s", "world"))
- log.Info(ctx, TagRequestIn, log.Msgf("hello %s", "world"))
-
- xml := `
-
-
-
- 100KB
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- `
- err := log.RefreshReader(strings.NewReader(xml), ".xml")
- assert.Nil(t, err)
-
- ctx = context.WithValue(ctx, "trace_id", "0a882193682db71edd48044db54cae88")
- ctx = context.WithValue(ctx, "span_id", "50ef0724418c0a66")
-
- log.Debug(ctx, TagRequestOut, func() []log.Field {
- return []log.Field{
- log.Msgf("hello %s", "world"),
- }
- })
-
- log.Info(ctx, TagDefault, log.Msgf("hello %s", "world"))
- log.Info(ctx, TagRequestIn, log.Msgf("hello %s", "world"))
-}
diff --git a/log/plugin.go b/log/plugin.go
deleted file mode 100644
index 2a50b3e1..00000000
--- a/log/plugin.go
+++ /dev/null
@@ -1,281 +0,0 @@
-/*
- * Copyright 2025 The Go-Spring Authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package log
-
-import (
- "fmt"
- "reflect"
- "runtime"
- "strconv"
- "strings"
-
- "github.com/go-spring/spring-core/util/errutil"
-)
-
-var converters = map[reflect.Type]any{}
-
-// Converter function type that converts string to a specific type T.
-type Converter[T any] func(string) (T, error)
-
-// RegisterConverter Registers a converter for a specific type T.
-func RegisterConverter[T any](fn Converter[T]) {
- t := reflect.TypeFor[T]()
- converters[t] = fn
-}
-
-// Lifecycle Optional lifecycle interface for plugin instances.
-type Lifecycle interface {
- Start() error
- Stop()
-}
-
-// PluginType Defines types of plugins supported by the logging system.
-type PluginType string
-
-const (
- PluginTypeAppender PluginType = "Appender"
- PluginTypeLayout PluginType = "Layout"
- PluginTypeAppenderRef PluginType = "AppenderRef"
- PluginTypeRoot PluginType = "Root"
- PluginTypeAsyncRoot PluginType = "AsyncRoot"
- PluginTypeLogger PluginType = "Logger"
- PluginTypeAsyncLogger PluginType = "AsyncLogger"
-)
-
-var plugins = map[string]*Plugin{}
-
-// Plugin metadata structure
-type Plugin struct {
- Name string // Name of plugin
- Type PluginType // Type of plugin
- Class reflect.Type // Underlying struct type
- File string // Source file of registration
- Line int // Line number of registration
-}
-
-// RegisterPlugin Registers a plugin with a given name and type.
-func RegisterPlugin[T Lifecycle](name string, typ PluginType) {
- _, file, line, _ := runtime.Caller(1)
- if p, ok := plugins[name]; ok {
- panic(fmt.Errorf("duplicate plugin %s in %s:%d and %s:%d", typ, p.File, p.Line, file, line))
- }
- t := reflect.TypeFor[T]().Elem()
- plugins[name] = &Plugin{
- Name: name,
- Type: typ,
- Class: t,
- File: file,
- Line: line,
- }
-}
-
-// NewPlugin Creates and initializes a plugin instance.
-func NewPlugin(t reflect.Type, node *Node, properties map[string]string) (reflect.Value, error) {
- v := reflect.New(t)
- if err := inject(v.Elem(), t, node, properties); err != nil {
- return reflect.Value{}, errutil.WrapError(err, "create plugin %s error", t.String())
- }
- return v, nil
-}
-
-// inject Recursively injects values into struct fields based on tags.
-func inject(v reflect.Value, t reflect.Type, node *Node, properties map[string]string) error {
- for i := 0; i < v.NumField(); i++ {
- ft := t.Field(i)
- fv := v.Field(i)
- if tag, ok := ft.Tag.Lookup("PluginAttribute"); ok {
- if err := injectAttribute(tag, fv, ft, node, properties); err != nil {
- return err
- }
- continue
- }
- if tag, ok := ft.Tag.Lookup("PluginElement"); ok {
- if err := injectElement(tag, fv, ft, node, properties); err != nil {
- return err
- }
- continue
- }
- // Recursively process anonymous embedded structs
- if ft.Anonymous && ft.Type.Kind() == reflect.Struct {
- if err := inject(fv, fv.Type(), node, properties); err != nil {
- return err
- }
- }
- }
- return nil
-}
-
-type PluginTag string
-
-// Get Gets the value of a key or the first unnamed value.
-func (tag PluginTag) Get(key string) string {
- v, _ := tag.Lookup(key)
- return v
-}
-
-// Lookup Looks up a key-value pair in the tag.
-func (tag PluginTag) Lookup(key string) (value string, ok bool) {
- kvs := strings.Split(string(tag), ",")
- if key == "" {
- return kvs[0], true
- }
- for i := 1; i < len(kvs); i++ {
- ss := strings.Split(kvs[i], "=")
- if ss[0] == key {
- if len(ss) > 1 {
- return ss[1], true
- }
- return "", true
- }
- }
- return "", false
-}
-
-// injectAttribute Injects a value into a struct field from plugin attribute.
-func injectAttribute(tag string, fv reflect.Value, ft reflect.StructField, node *Node, properties map[string]string) error {
-
- attrTag := PluginTag(tag)
- attrName := attrTag.Get("")
- if attrName == "" {
- return fmt.Errorf("found no attribute for struct field %s", ft.Name)
- }
- val, ok := node.Attributes[attrName]
- if !ok {
- val, ok = attrTag.Lookup("default")
- if !ok {
- return fmt.Errorf("found no attribute for struct field %s", ft.Name)
- }
- }
-
- // Use a property if available
- val = strings.TrimSpace(val)
- if strings.HasPrefix(val, "${") && strings.HasSuffix(val, "}") {
- s, exist := properties[val[2:len(val)-1]]
- if !exist {
- return fmt.Errorf("property %s not found", val)
- }
- val = s
- }
-
- // Use a custom converter if available
- if fn := converters[ft.Type]; fn != nil {
- fnValue := reflect.ValueOf(fn)
- out := fnValue.Call([]reflect.Value{reflect.ValueOf(val)})
- if !out[1].IsNil() {
- err := out[1].Interface().(error)
- return errutil.WrapError(err, "inject struct field %s error", ft.Name)
- }
- fv.Set(out[0])
- return nil
- }
-
- switch fv.Kind() {
- case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
- u, err := strconv.ParseUint(val, 0, 0)
- if err == nil {
- fv.SetUint(u)
- return nil
- }
- return errutil.WrapError(err, "inject struct field %s error", ft.Name)
- case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
- i, err := strconv.ParseInt(val, 0, 0)
- if err == nil {
- fv.SetInt(i)
- return nil
- }
- return errutil.WrapError(err, "inject struct field %s error", ft.Name)
- case reflect.Float32, reflect.Float64:
- f, err := strconv.ParseFloat(val, 64)
- if err == nil {
- fv.SetFloat(f)
- return nil
- }
- return errutil.WrapError(err, "inject struct field %s error", ft.Name)
- case reflect.Bool:
- b, err := strconv.ParseBool(val)
- if err == nil {
- fv.SetBool(b)
- return nil
- }
- return errutil.WrapError(err, "inject struct field %s error", ft.Name)
- case reflect.String:
- fv.SetString(val)
- return nil
- default:
- return fmt.Errorf("unsupported inject type %s for struct field %s", ft.Type.String(), ft.Name)
- }
-}
-
-// injectElement Injects plugin elements (child nodes) into struct fields.
-func injectElement(tag string, fv reflect.Value, ft reflect.StructField, node *Node, properties map[string]string) error {
-
- elemTag := PluginTag(tag)
- elemType := elemTag.Get("")
- if elemType == "" {
- return fmt.Errorf("found no element for struct field %s", ft.Name)
- }
-
- var children []reflect.Value
- for _, c := range node.Children {
- p, ok := plugins[c.Label]
- if !ok {
- return fmt.Errorf("plugin %s not found for struct field %s", c.Label, ft.Name)
- }
- if string(p.Type) != elemType {
- continue
- }
- v, err := NewPlugin(p.Class, c, properties)
- if err != nil {
- return err
- }
- children = append(children, v)
- }
-
- if len(children) == 0 {
- elemLabel, ok := elemTag.Lookup("default")
- if !ok {
- return fmt.Errorf("found no plugin elements for struct field %s", ft.Name)
- }
- p, ok := plugins[elemLabel]
- if !ok {
- return fmt.Errorf("plugin %s not found for struct field %s", elemLabel, ft.Name)
- }
- v, err := NewPlugin(p.Class, &Node{Label: elemLabel}, properties)
- if err != nil {
- return err
- }
- children = append(children, v)
- }
-
- switch fv.Kind() {
- case reflect.Slice:
- slice := reflect.MakeSlice(ft.Type, 0, len(children))
- for j := 0; j < len(children); j++ {
- slice = reflect.Append(slice, children[j])
- }
- fv.Set(slice)
- return nil
- case reflect.Interface:
- if len(children) > 1 {
- return fmt.Errorf("found %d plugin elements for struct field %s", len(children), ft.Name)
- }
- fv.Set(children[0])
- return nil
- default:
- return fmt.Errorf("unsupported inject type %s for struct field %s", ft.Type.String(), ft.Name)
- }
-}
diff --git a/log/plugin_appender.go b/log/plugin_appender.go
deleted file mode 100644
index a7911659..00000000
--- a/log/plugin_appender.go
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * Copyright 2025 The Go-Spring Authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package log
-
-import (
- "os"
-)
-
-func init() {
- RegisterPlugin[*DiscardAppender]("Discard", PluginTypeAppender)
- RegisterPlugin[*ConsoleAppender]("Console", PluginTypeAppender)
- RegisterPlugin[*FileAppender]("File", PluginTypeAppender)
-}
-
-// Appender is an interface that defines components that handle log output.
-type Appender interface {
- Lifecycle // Appenders must be startable and stoppable
- Append(e *Event) // Handles writing a log event
-}
-
-var (
- _ Appender = (*DiscardAppender)(nil)
- _ Appender = (*ConsoleAppender)(nil)
- _ Appender = (*FileAppender)(nil)
-)
-
-// BaseAppender provides shared configuration and behavior for appenders.
-type BaseAppender struct {
- Name string `PluginAttribute:"name"` // Appender name from config
- Layout Layout `PluginElement:"Layout"` // Layout defines how logs are formatted
-}
-
-func (c *BaseAppender) Start() error { return nil }
-func (c *BaseAppender) Stop() {}
-func (c *BaseAppender) Append(e *Event) {}
-
-// DiscardAppender ignores all log events (no output).
-type DiscardAppender struct {
- BaseAppender
-}
-
-// ConsoleAppender writes formatted log events to stdout.
-type ConsoleAppender struct {
- BaseAppender
-}
-
-// Append formats the event and writes it to standard output.
-func (c *ConsoleAppender) Append(e *Event) {
- data := c.Layout.ToBytes(e)
- _, _ = os.Stdout.Write(data)
-}
-
-// FileAppender writes formatted log events to a specified file.
-type FileAppender struct {
- BaseAppender
- FileName string `PluginAttribute:"fileName"`
-
- file *os.File
-}
-
-func (c *FileAppender) Start() error {
- f, err := os.OpenFile(c.FileName, os.O_WRONLY|os.O_CREATE|os.O_APPEND, os.ModePerm)
- if err != nil {
- return err
- }
- c.file = f
- return nil
-}
-
-// Append formats the log event and writes it to the file.
-func (c *FileAppender) Append(e *Event) {
- data := c.Layout.ToBytes(e)
- _, _ = c.file.Write(data)
-}
-
-// Stop closes the file.
-func (c *FileAppender) Stop() {
- if c.file != nil {
- _ = c.file.Close()
- }
-}
diff --git a/log/plugin_appender_test.go b/log/plugin_appender_test.go
deleted file mode 100644
index 9bb53b00..00000000
--- a/log/plugin_appender_test.go
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- * Copyright 2025 The Go-Spring Authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package log
-
-import (
- "os"
- "testing"
- "time"
-
- "github.com/lvan100/go-assert"
-)
-
-func TestDiscardAppender(t *testing.T) {
- a := &DiscardAppender{}
- err := a.Start()
- assert.Nil(t, err)
- a.Append(&Event{})
- a.Stop()
-}
-
-func TestConsoleAppender(t *testing.T) {
-
- t.Run("success", func(t *testing.T) {
- file, err := os.CreateTemp(os.TempDir(), "")
- assert.Nil(t, err)
-
- oldStdout := os.Stdout
- os.Stdout = file
- defer func() {
- os.Stdout = oldStdout
- }()
-
- a := &ConsoleAppender{
- BaseAppender: BaseAppender{
- Layout: &TextLayout{
- BaseLayout{
- FileLineLength: 48,
- },
- },
- },
- }
- a.Append(&Event{
- Level: InfoLevel,
- Time: time.Time{},
- File: "file.go",
- Line: 100,
- Tag: "_def",
- Fields: []Field{Msg("hello world")},
- CtxFields: nil,
- })
-
- err = file.Close()
- assert.Nil(t, err)
-
- b, err := os.ReadFile(file.Name())
- assert.Nil(t, err)
- assert.ThatString(t, string(b)).Equal("[INFO][0001-01-01T00:00:00.000][file.go:100] _def||msg=hello world\n")
- })
-}
-
-func TestFileAppender(t *testing.T) {
-
- t.Run("Start error", func(t *testing.T) {
- a := &FileAppender{
- BaseAppender: BaseAppender{
- Layout: &TextLayout{
- BaseLayout{
- FileLineLength: 48,
- },
- },
- },
- FileName: "/not-exist-dir/file.log",
- }
- err := a.Start()
- assert.ThatError(t, err).Matches("open /not-exist-dir/file.log: no such file or directory")
- })
-
- t.Run("success", func(t *testing.T) {
- file, err := os.CreateTemp(os.TempDir(), "")
- assert.Nil(t, err)
- err = file.Close()
- assert.Nil(t, err)
-
- a := &FileAppender{
- BaseAppender: BaseAppender{
- Layout: &TextLayout{
- BaseLayout{
- FileLineLength: 48,
- },
- },
- },
- FileName: file.Name(),
- }
- err = a.Start()
- assert.Nil(t, err)
-
- a.Append(&Event{
- Level: InfoLevel,
- Time: time.Time{},
- File: "file.go",
- Line: 100,
- Tag: "_def",
- Fields: []Field{Msg("hello world")},
- CtxFields: nil,
- })
-
- a.Stop()
-
- b, err := os.ReadFile(a.file.Name())
- assert.Nil(t, err)
- assert.ThatString(t, string(b)).Equal("[INFO][0001-01-01T00:00:00.000][file.go:100] _def||msg=hello world\n")
- })
-}
diff --git a/log/plugin_layout.go b/log/plugin_layout.go
deleted file mode 100644
index 0e8fd3aa..00000000
--- a/log/plugin_layout.go
+++ /dev/null
@@ -1,174 +0,0 @@
-/*
- * Copyright 2025 The Go-Spring Authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package log
-
-import (
- "bytes"
- "fmt"
- "strconv"
- "strings"
- "unicode"
-)
-
-var bytesSizeTable = map[string]int64{
- "B": 1,
- "KB": 1024,
- "MB": 1024 * 1024,
-}
-
-func init() {
- RegisterConverter[HumanizeBytes](ParseHumanizeBytes)
- RegisterPlugin[*TextLayout]("TextLayout", PluginTypeLayout)
- RegisterPlugin[*JSONLayout]("JSONLayout", PluginTypeLayout)
-}
-
-type HumanizeBytes int
-
-// ParseHumanizeBytes converts a human-readable byte string to an integer.
-func ParseHumanizeBytes(s string) (HumanizeBytes, error) {
- lastDigit := 0
- for _, r := range s {
- if !unicode.IsDigit(r) {
- break
- }
- lastDigit++
- }
- num := s[:lastDigit]
- f, err := strconv.ParseInt(num, 10, 64)
- if err != nil {
- return 0, err
- }
- extra := strings.ToUpper(strings.TrimSpace(s[lastDigit:]))
- if m, ok := bytesSizeTable[extra]; ok {
- f *= m
- return HumanizeBytes(f), nil
- }
- return 0, fmt.Errorf("unhandled size name: %q", extra)
-}
-
-// Layout is the interface that defines how a log event is converted to bytes.
-type Layout interface {
- Lifecycle
- ToBytes(e *Event) []byte
-}
-
-// BaseLayout is the base class for Layout.
-type BaseLayout struct {
- BufferSize HumanizeBytes `PluginAttribute:"bufferSize,default=1MB"`
- FileLineLength int `PluginAttribute:"fileLineLength,default=48"`
-
- buffer *bytes.Buffer
-}
-
-func (c *BaseLayout) Start() error { return nil }
-func (c *BaseLayout) Stop() {}
-
-// GetBuffer returns a buffer that can be used to format the log event.
-func (c *BaseLayout) GetBuffer() *bytes.Buffer {
- if c.buffer == nil {
- c.buffer = &bytes.Buffer{}
- c.buffer.Grow(int(c.BufferSize))
- }
- return c.buffer
-}
-
-// PutBuffer puts a buffer back into the pool.
-func (c *BaseLayout) PutBuffer(buf *bytes.Buffer) {
- if buf.Cap() > int(c.BufferSize) {
- c.buffer = nil
- return
- }
- c.buffer = buf
- c.buffer.Reset()
-}
-
-// GetFileLine returns the file name and line number of the log event.
-func (c *BaseLayout) GetFileLine(e *Event) string {
- fileLine := e.File + ":" + strconv.Itoa(e.Line)
- if n := len(fileLine); n > c.FileLineLength-3 {
- fileLine = "..." + fileLine[n-c.FileLineLength:]
- }
- return fileLine
-}
-
-// TextLayout formats the log event as a human-readable text string.
-type TextLayout struct {
- BaseLayout
-}
-
-// ToBytes converts a log event to a formatted plain-text line.
-func (c *TextLayout) ToBytes(e *Event) []byte {
- const separator = "||"
-
- buf := c.GetBuffer()
- defer c.PutBuffer(buf)
-
- buf.WriteString("[")
- buf.WriteString(strings.ToUpper(e.Level.String()))
- buf.WriteString("][")
- buf.WriteString(e.Time.Format("2006-01-02T15:04:05.000"))
- buf.WriteString("][")
- buf.WriteString(c.GetFileLine(e))
- buf.WriteString("] ")
- buf.WriteString(e.Tag)
- buf.WriteString(separator)
-
- if e.CtxString != "" {
- buf.WriteString(e.CtxString)
- buf.WriteString(separator)
- }
-
- enc := NewTextEncoder(buf, separator)
- enc.AppendEncoderBegin()
- WriteFields(enc, e.CtxFields)
- WriteFields(enc, e.Fields)
- enc.AppendEncoderEnd()
-
- buf.WriteByte('\n')
- return buf.Bytes()
-}
-
-// JSONLayout formats the log event as a structured JSON object.
-type JSONLayout struct {
- BaseLayout
-}
-
-// ToBytes converts a log event to a JSON-formatted byte slice.
-func (c *JSONLayout) ToBytes(e *Event) []byte {
- buf := c.GetBuffer()
- defer c.PutBuffer(buf)
-
- fields := make([]Field, 0, 5)
- fields = append(fields, String("level", strings.ToLower(e.Level.String())))
- fields = append(fields, String("time", e.Time.Format("2006-01-02T15:04:05.000")))
- fields = append(fields, String("fileLine", c.GetFileLine(e)))
- fields = append(fields, String("tag", e.Tag))
-
- if e.CtxString != "" {
- fields = append(fields, String("ctxString", e.CtxString))
- }
-
- enc := NewJSONEncoder(buf)
- enc.AppendEncoderBegin()
- WriteFields(enc, fields)
- WriteFields(enc, e.CtxFields)
- WriteFields(enc, e.Fields)
- enc.AppendEncoderEnd()
-
- buf.WriteByte('\n')
- return buf.Bytes()
-}
diff --git a/log/plugin_layout_test.go b/log/plugin_layout_test.go
deleted file mode 100644
index bb783fe1..00000000
--- a/log/plugin_layout_test.go
+++ /dev/null
@@ -1,149 +0,0 @@
-/*
- * Copyright 2025 The Go-Spring Authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package log
-
-import (
- "errors"
- "testing"
- "time"
-
- "github.com/lvan100/go-assert"
-)
-
-func TestParseHumanizeBytes(t *testing.T) {
- tests := []struct {
- name string
- input string
- want HumanizeBytes
- wantErr error
- }{
- {
- name: "basic bytes",
- input: "1024B",
- want: 1024,
- },
- {
- name: "kilobytes",
- input: "1KB",
- want: 1024,
- },
- {
- name: "megabytes",
- input: "2MB",
- want: 2 * 1024 * 1024,
- },
- {
- name: "case insensitive",
- input: "1kb",
- want: 1024,
- },
- {
- name: "space before unit",
- input: "1 KB",
- want: 1024,
- },
- {
- name: "space after unit",
- input: "1KB ",
- want: 1024,
- },
- {
- name: "invalid number",
- input: "abcKB",
- wantErr: errors.New(`strconv.ParseInt: parsing "": invalid syntax`),
- },
- {
- name: "missing unit",
- input: "1024",
- wantErr: errors.New(`unhandled size name: ""`),
- },
- {
- name: "unknown unit",
- input: "1GB",
- wantErr: errors.New(`unhandled size name: "GB"`),
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- got, err := ParseHumanizeBytes(tt.input)
- if err != nil && err.Error() != tt.wantErr.Error() {
- t.Errorf("ParseHumanizeBytes() error = %v, wantErr %v", err, tt.wantErr)
- return
- }
- if got != tt.want {
- t.Errorf("ParseHumanizeBytes() = %v, want %v", got, tt.want)
- }
- })
- }
-}
-
-func TestTextLayout(t *testing.T) {
-
- t.Run("success", func(t *testing.T) {
- layout := &TextLayout{
- BaseLayout{
- FileLineLength: 48,
- },
- }
-
- err := layout.Start()
- assert.Nil(t, err)
-
- b := layout.ToBytes(&Event{
- Level: InfoLevel,
- Time: time.Time{},
- File: "gs/examples/bookman/src/biz/service/book_service/book_service_test.go",
- Line: 100,
- Tag: "_def",
- Fields: []Field{Msg("hello world")},
- CtxString: "trace_id=0a882193682db71edd48044db54cae88||span_id=50ef0724418c0a66",
- CtxFields: nil,
- })
- assert.ThatString(t, string(b)).Equal("[INFO][0001-01-01T00:00:00.000][...iz/service/book_service/book_service_test.go:100] _def||trace_id=0a882193682db71edd48044db54cae88||span_id=50ef0724418c0a66||msg=hello world\n")
-
- layout.Stop()
- })
-}
-
-func TestJSONLayout(t *testing.T) {
-
- t.Run("success", func(t *testing.T) {
- layout := &JSONLayout{
- BaseLayout{
- FileLineLength: 48,
- },
- }
-
- err := layout.Start()
- assert.Nil(t, err)
-
- b := layout.ToBytes(&Event{
- Level: InfoLevel,
- Time: time.Time{},
- File: "gs/examples/bookman/src/biz/service/book_service/book_service_test.go",
- Line: 100,
- Tag: "_def",
- Fields: []Field{Msg("hello world")},
- CtxString: "trace_id=0a882193682db71edd48044db54cae88||span_id=50ef0724418c0a66",
- CtxFields: nil,
- })
- assert.ThatString(t, string(b)).Equal(`{"level":"info","time":"0001-01-01T00:00:00.000","fileLine":"...iz/service/book_service/book_service_test.go:100","tag":"_def","ctxString":"trace_id=0a882193682db71edd48044db54cae88||span_id=50ef0724418c0a66","msg":"hello world"}` + "\n")
-
- layout.Stop()
- })
-}
diff --git a/log/plugin_logger.go b/log/plugin_logger.go
deleted file mode 100644
index 609c4295..00000000
--- a/log/plugin_logger.go
+++ /dev/null
@@ -1,136 +0,0 @@
-/*
- * Copyright 2025 The Go-Spring Authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package log
-
-import (
- "errors"
-)
-
-// OnDropEvent is a callback function that is called when an event is dropped.
-var OnDropEvent func(*Event)
-
-func init() {
- RegisterPlugin[*AppenderRef]("AppenderRef", PluginTypeAppenderRef)
- RegisterPlugin[*LoggerConfig]("Root", PluginTypeRoot)
- RegisterPlugin[*AsyncLoggerConfig]("AsyncRoot", PluginTypeAsyncRoot)
- RegisterPlugin[*LoggerConfig]("Logger", PluginTypeLogger)
- RegisterPlugin[*AsyncLoggerConfig]("AsyncLogger", PluginTypeAsyncLogger)
-}
-
-// Logger is the primary logging structure used to emit log events.
-type Logger struct {
- privateConfig
-}
-
-// privateConfig is the interface implemented by all logger configs.
-type privateConfig interface {
- Lifecycle // Start/Stop methods
- publish(e *Event) // Logic for sending events to appenders
- enableLevel(level Level) bool // Whether a log level is enabled
-}
-
-// AppenderRef represents a reference to an appender by name,
-// which will be resolved and bound later.
-type AppenderRef struct {
- Ref string `PluginAttribute:"ref"`
- appender Appender
-}
-
-func (a *AppenderRef) Start() error { return nil }
-func (a *AppenderRef) Stop() {}
-
-// baseLoggerConfig contains shared fields for all logger configurations.
-type baseLoggerConfig struct {
- Name string `PluginAttribute:"name"`
- Level Level `PluginAttribute:"level"`
- Tags string `PluginAttribute:"tags,default="`
- AppenderRefs []*AppenderRef `PluginElement:"AppenderRef"`
-}
-
-func (c *baseLoggerConfig) Start() error { return nil }
-func (c *baseLoggerConfig) Stop() {}
-
-// callAppenders sends the event to all configured appenders.
-func (c *baseLoggerConfig) callAppenders(e *Event) {
- for _, r := range c.AppenderRefs {
- r.appender.Append(e)
- }
-}
-
-// enableLevel returns true if the specified log level is enabled.
-func (c *baseLoggerConfig) enableLevel(level Level) bool {
- return level >= c.Level
-}
-
-// LoggerConfig is a synchronous logger configuration.
-type LoggerConfig struct {
- baseLoggerConfig
-}
-
-// publish sends the event directly to the appenders.
-func (c *LoggerConfig) publish(e *Event) {
- c.callAppenders(e)
- PutEvent(e)
-}
-
-// AsyncLoggerConfig is an asynchronous logger configuration.
-// It buffers log events and processes them in a separate goroutine.
-type AsyncLoggerConfig struct {
- baseLoggerConfig
- BufferSize int `PluginAttribute:"bufferSize,default=10000"`
-
- buf chan *Event // Channel buffer for log events
- wait chan struct{}
-}
-
-// Start initializes the asynchronous logger and starts its worker goroutine.
-func (c *AsyncLoggerConfig) Start() error {
- if c.BufferSize < 100 {
- return errors.New("bufferSize is too small")
- }
- c.buf = make(chan *Event, c.BufferSize)
- c.wait = make(chan struct{})
-
- // Launch a background goroutine to process events
- go func() {
- for e := range c.buf {
- c.callAppenders(e)
- PutEvent(e)
- }
- close(c.wait)
- }()
- return nil
-}
-
-// publish places the event in the buffer if there's space; drops it otherwise.
-func (c *AsyncLoggerConfig) publish(e *Event) {
- select {
- case c.buf <- e:
- default:
- // Drop the event if the buffer is full
- PutEvent(e)
- if OnDropEvent != nil {
- OnDropEvent(e)
- }
- }
-}
-
-// Stop shuts down the asynchronous logger and waits for the worker goroutine to finish.
-func (c *AsyncLoggerConfig) Stop() {
- close(c.buf)
- <-c.wait
-}
diff --git a/log/plugin_logger_test.go b/log/plugin_logger_test.go
deleted file mode 100644
index c2d4dd02..00000000
--- a/log/plugin_logger_test.go
+++ /dev/null
@@ -1,180 +0,0 @@
-/*
- * Copyright 2025 The Go-Spring Authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package log
-
-import (
- "testing"
- "time"
-
- "github.com/lvan100/go-assert"
-)
-
-type CountAppender struct {
- Appender
- count int
-}
-
-func (c *CountAppender) Append(e *Event) {
- c.count++
- c.Appender.Append(e)
-}
-
-func TestLoggerConfig(t *testing.T) {
-
- t.Run("success", func(t *testing.T) {
- a := &CountAppender{
- Appender: &DiscardAppender{},
- }
-
- err := a.Start()
- assert.Nil(t, err)
-
- l := &LoggerConfig{baseLoggerConfig{
- Level: InfoLevel,
- Tags: "_com_*",
- AppenderRefs: []*AppenderRef{
- {appender: a},
- },
- }}
-
- err = l.Start()
- assert.Nil(t, err)
-
- assert.False(t, l.enableLevel(TraceLevel))
- assert.False(t, l.enableLevel(DebugLevel))
- assert.True(t, l.enableLevel(InfoLevel))
- assert.True(t, l.enableLevel(WarnLevel))
- assert.True(t, l.enableLevel(ErrorLevel))
- assert.True(t, l.enableLevel(PanicLevel))
- assert.True(t, l.enableLevel(FatalLevel))
-
- for i := 0; i < 5; i++ {
- l.publish(&Event{})
- }
-
- assert.That(t, a.count).Equal(5)
-
- l.Stop()
- a.Stop()
- })
-}
-
-func TestAsyncLoggerConfig(t *testing.T) {
-
- t.Run("enable level", func(t *testing.T) {
- l := &AsyncLoggerConfig{
- baseLoggerConfig: baseLoggerConfig{
- Level: InfoLevel,
- },
- }
-
- assert.False(t, l.enableLevel(TraceLevel))
- assert.False(t, l.enableLevel(DebugLevel))
- assert.True(t, l.enableLevel(InfoLevel))
- assert.True(t, l.enableLevel(WarnLevel))
- assert.True(t, l.enableLevel(ErrorLevel))
- assert.True(t, l.enableLevel(PanicLevel))
- assert.True(t, l.enableLevel(FatalLevel))
- })
-
- t.Run("error BufferSize", func(t *testing.T) {
- l := &AsyncLoggerConfig{
- baseLoggerConfig: baseLoggerConfig{
- Name: "file",
- },
- BufferSize: 10,
- }
-
- err := l.Start()
- assert.ThatError(t, err).Matches("bufferSize is too small")
- })
-
- t.Run("drop events", func(t *testing.T) {
- a := &CountAppender{
- Appender: &DiscardAppender{},
- }
-
- err := a.Start()
- assert.Nil(t, err)
-
- dropCount := 0
- OnDropEvent = func(*Event) {
- dropCount++
- }
- defer func() {
- OnDropEvent = nil
- }()
-
- l := &AsyncLoggerConfig{
- baseLoggerConfig: baseLoggerConfig{
- Level: InfoLevel,
- Tags: "_com_*",
- AppenderRefs: []*AppenderRef{
- {appender: a},
- },
- },
- BufferSize: 100,
- }
-
- err = l.Start()
- assert.Nil(t, err)
-
- for i := 0; i < 5000; i++ {
- l.publish(GetEvent())
- }
-
- time.Sleep(200 * time.Millisecond)
-
- l.Stop()
- a.Stop()
-
- assert.True(t, dropCount > 0)
- })
-
- t.Run("success", func(t *testing.T) {
- a := &CountAppender{
- Appender: &DiscardAppender{},
- }
-
- err := a.Start()
- assert.Nil(t, err)
-
- l := &AsyncLoggerConfig{
- baseLoggerConfig: baseLoggerConfig{
- Level: InfoLevel,
- Tags: "_com_*",
- AppenderRefs: []*AppenderRef{
- {appender: a},
- },
- },
- BufferSize: 100,
- }
-
- err = l.Start()
- assert.Nil(t, err)
-
- for i := 0; i < 5; i++ {
- l.publish(GetEvent())
- }
-
- time.Sleep(100 * time.Millisecond)
- assert.That(t, a.count).Equal(5)
-
- l.Stop()
- a.Stop()
- })
-}
diff --git a/log/plugin_test.go b/log/plugin_test.go
deleted file mode 100644
index da9f923f..00000000
--- a/log/plugin_test.go
+++ /dev/null
@@ -1,175 +0,0 @@
-/*
- * Copyright 2025 The Go-Spring Authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package log
-
-import (
- "reflect"
- "testing"
-
- "github.com/lvan100/go-assert"
-)
-
-func TestRegisterPlugin(t *testing.T) {
- assert.Panic(t, func() {
- RegisterPlugin[*FileAppender]("File", PluginTypeAppender)
- }, "duplicate plugin Appender in .*/plugin_appender.go:26 and .*/plugin_test.go:28")
-}
-
-func TestInjectAttribute(t *testing.T) {
-
- t.Run("no attribute - 1", func(t *testing.T) {
- type ErrorPlugin struct {
- Name string `PluginAttribute:""`
- }
- typ := reflect.TypeFor[ErrorPlugin]()
- _, err := NewPlugin(typ, nil, nil)
- assert.ThatError(t, err).Matches("create plugin log.ErrorPlugin error << found no attribute for struct field Name")
- })
-
- t.Run("no attribute - 2", func(t *testing.T) {
- type ErrorPlugin struct {
- Name string `PluginAttribute:"name"`
- }
- typ := reflect.TypeFor[ErrorPlugin]()
- _, err := NewPlugin(typ, &Node{}, nil)
- assert.ThatError(t, err).Matches("create plugin log.ErrorPlugin error << found no attribute for struct field Name")
- })
-
- t.Run("property not found", func(t *testing.T) {
- type ErrorPlugin struct {
- Name string `PluginAttribute:"name,default=${not-exist-prop}"`
- }
- typ := reflect.TypeFor[ErrorPlugin]()
- _, err := NewPlugin(typ, &Node{}, nil)
- assert.ThatError(t, err).Matches("create plugin log.ErrorPlugin error << property \\${not-exist-prop} not found")
- })
-
- t.Run("converter error", func(t *testing.T) {
- type ErrorPlugin struct {
- Level Level `PluginAttribute:"level,default=NOT-EXIST-LEVEL"`
- }
- typ := reflect.TypeFor[ErrorPlugin]()
- _, err := NewPlugin(typ, &Node{}, nil)
- assert.ThatError(t, err).Matches("create plugin log.ErrorPlugin error << inject struct field Level error << invalid level NOT-EXIST-LEVEL")
- })
-
- t.Run("uint64 error", func(t *testing.T) {
- type ErrorPlugin struct {
- M uint64 `PluginAttribute:"m,default=111"`
- N uint64 `PluginAttribute:"n,default=abc"`
- }
- typ := reflect.TypeFor[ErrorPlugin]()
- _, err := NewPlugin(typ, &Node{}, nil)
- assert.ThatError(t, err).Matches(`create plugin log.ErrorPlugin error << inject struct field N error << strconv.ParseUint: parsing \"abc\": invalid syntax`)
- })
-
- t.Run("int64 error", func(t *testing.T) {
- type ErrorPlugin struct {
- M int64 `PluginAttribute:"m,default=111"`
- N int64 `PluginAttribute:"n,default=abc"`
- }
- typ := reflect.TypeFor[ErrorPlugin]()
- _, err := NewPlugin(typ, &Node{}, nil)
- assert.ThatError(t, err).Matches(`create plugin log.ErrorPlugin error << inject struct field N error << strconv.ParseInt: parsing \"abc\": invalid syntax`)
- })
-
- t.Run("float64 error", func(t *testing.T) {
- type ErrorPlugin struct {
- M float64 `PluginAttribute:"m,default=111"`
- N float64 `PluginAttribute:"n,default=abc"`
- }
- typ := reflect.TypeFor[ErrorPlugin]()
- _, err := NewPlugin(typ, &Node{}, nil)
- assert.ThatError(t, err).Matches(`create plugin log.ErrorPlugin error << inject struct field N error << strconv.ParseFloat: parsing \"abc\": invalid syntax`)
- })
-
- t.Run("boolean error", func(t *testing.T) {
- type ErrorPlugin struct {
- M bool `PluginAttribute:"m,default=true"`
- N bool `PluginAttribute:"n,default=abc"`
- }
- typ := reflect.TypeFor[ErrorPlugin]()
- _, err := NewPlugin(typ, &Node{}, nil)
- assert.ThatError(t, err).Matches(`create plugin log.ErrorPlugin error << inject struct field N error << strconv.ParseBool: parsing \"abc\": invalid syntax`)
- })
-
- t.Run("type error", func(t *testing.T) {
- type ErrorPlugin struct {
- M chan error `PluginAttribute:"m,default=true"`
- }
- typ := reflect.TypeFor[ErrorPlugin]()
- _, err := NewPlugin(typ, &Node{}, nil)
- assert.ThatError(t, err).Matches(`create plugin log.ErrorPlugin error << unsupported inject type chan error for struct field M`)
- })
-}
-
-func TestInjectElement(t *testing.T) {
-
- t.Run("no element - 1", func(t *testing.T) {
- type ErrorPlugin struct {
- Layout Layout `PluginElement:""`
- }
- typ := reflect.TypeFor[ErrorPlugin]()
- _, err := NewPlugin(typ, nil, nil)
- assert.ThatError(t, err).Matches("create plugin log.ErrorPlugin error << found no element for struct field Layout")
- })
-
- t.Run("plugin not found", func(t *testing.T) {
- type ErrorPlugin struct {
- Layout Layout `PluginElement:"Layout"`
- }
- typ := reflect.TypeFor[ErrorPlugin]()
- _, err := NewPlugin(typ, &Node{
- Children: []*Node{
- {Label: "NotExistElement"},
- },
- }, nil)
- assert.ThatError(t, err).Matches("create plugin log.ErrorPlugin error << plugin NotExistElement not found for struct field Layout")
- })
-
- t.Run("plugin type mismatch", func(t *testing.T) {
- type ErrorPlugin struct {
- Layout Layout `PluginElement:"Layout"`
- }
- typ := reflect.TypeFor[ErrorPlugin]()
- _, err := NewPlugin(typ, &Node{
- Children: []*Node{
- {Label: "File"},
- },
- }, nil)
- assert.ThatError(t, err).Matches("create plugin log.ErrorPlugin error << found no plugin elements for struct field Layout")
- })
-
- t.Run("NewPlugin error", func(t *testing.T) {
- type ErrorPlugin struct {
- Layout Layout `PluginElement:"Layout"`
- }
- typ := reflect.TypeFor[ErrorPlugin]()
- _, err := NewPlugin(typ, &Node{
- Children: []*Node{
- {
- Label: "TextLayout",
- Attributes: map[string]string{
- "bufferSize": "1GB",
- },
- },
- },
- }, nil)
- assert.ThatError(t, err).Matches(`create plugin log.ErrorPlugin error << create plugin log.TextLayout error ` +
- `<< inject struct field BufferSize error << unhandled size name: \"GB\"`)
- })
-}
diff --git a/util/color/color.go b/util/color/color.go
index 78c049d2..50f75b79 100644
--- a/util/color/color.go
+++ b/util/color/color.go
@@ -55,12 +55,12 @@ const (
type Attribute string
// Sprint returns a string formatted according to console properties.
-func (attr Attribute) Sprint(a ...interface{}) string {
+func (attr Attribute) Sprint(a ...any) string {
return wrap([]Attribute{attr}, fmt.Sprint(a...))
}
// Sprintf returns a string formatted according to console properties.
-func (attr Attribute) Sprintf(format string, a ...interface{}) string {
+func (attr Attribute) Sprintf(format string, a ...any) string {
return wrap([]Attribute{attr}, fmt.Sprintf(format, a...))
}
@@ -74,12 +74,12 @@ func NewText(attributes ...Attribute) *Text {
}
// Sprint returns a string formatted according to console properties.
-func (c *Text) Sprint(a ...interface{}) string {
+func (c *Text) Sprint(a ...any) string {
return wrap(c.attributes, fmt.Sprint(a...))
}
// Sprintf returns a string formatted according to console properties.
-func (c *Text) Sprintf(format string, a ...interface{}) string {
+func (c *Text) Sprintf(format string, a ...any) string {
return wrap(c.attributes, fmt.Sprintf(format, a...))
}
@@ -89,7 +89,7 @@ func wrap(attributes []Attribute, str string) string {
}
var buf bytes.Buffer
buf.WriteString("\x1b[")
- for i := 0; i < len(attributes); i++ {
+ for i := range len(attributes) {
buf.WriteString(string(attributes[i]))
if i < len(attributes)-1 {
buf.WriteByte(';')
diff --git a/util/goutil/goutil.go b/util/goutil/goutil.go
index 44613573..388b8db6 100644
--- a/util/goutil/goutil.go
+++ b/util/goutil/goutil.go
@@ -29,15 +29,14 @@ package goutil
import (
"context"
"errors"
+ "fmt"
"runtime/debug"
"sync"
-
- "github.com/go-spring/spring-core/util/syslog"
)
// OnPanic is a global callback function triggered when a panic occurs.
var OnPanic = func(ctx context.Context, r any) {
- syslog.Errorf("panic: %v\n%s", r, debug.Stack())
+ fmt.Printf("panic: %v\n%s\n", r, debug.Stack())
}
/********************************** go ***************************************/
diff --git a/util/sysconf/sysconf.go b/util/sysconf/sysconf.go
deleted file mode 100644
index 1512897e..00000000
--- a/util/sysconf/sysconf.go
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright 2024 The Go-Spring Authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-/*
-Package sysconf provides a unified configuration container for the Go programming language.
-
-In the Go programming language, unlike many other languages,
-the standard library lacks a unified and general-purpose configuration container.
-To address this gap, go-spring introduces a powerful configuration system that supports
-layered configuration management and flexible injection.
-
-So sysconf serves as the fallback configuration container within an application,
-acting as the lowest-level foundation of the configuration system.
-It can be used independently or as a lightweight alternative or supplement to other
-configuration sources such as environment variables, command-line arguments, or configuration files.
-*/
-package sysconf
-
-import (
- "sync"
-
- "github.com/go-spring/spring-core/conf"
- "github.com/go-spring/spring-core/util/syslog"
-)
-
-var (
- prop = conf.New()
- lock sync.Mutex
-)
-
-// Has returns whether the key exists.
-func Has(key string) bool {
- lock.Lock()
- defer lock.Unlock()
- return prop.Has(key)
-}
-
-// Get returns the property of the key.
-func Get(key string) string {
- lock.Lock()
- defer lock.Unlock()
- return prop.Get(key)
-}
-
-// Set sets the property of the key.
-func Set(key string, val string) {
- lock.Lock()
- defer lock.Unlock()
- if err := prop.Set(key, val); err != nil {
- syslog.Errorf("failed to set property key=%s, err=%v", key, err)
- }
-}
-
-// Clear clears all properties.
-func Clear() {
- lock.Lock()
- defer lock.Unlock()
- prop = conf.New()
-}
-
-// Clone copies all properties into another properties.
-func Clone() *conf.MutableProperties {
- lock.Lock()
- defer lock.Unlock()
- p := conf.New()
- err := prop.CopyTo(p)
- _ = err // should no error
- return p
-}
diff --git a/util/sysconf/sysconf_test.go b/util/sysconf/sysconf_test.go
deleted file mode 100644
index 8f7c2acc..00000000
--- a/util/sysconf/sysconf_test.go
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright 2025 The Go-Spring Authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package sysconf_test
-
-import (
- "testing"
-
- "github.com/go-spring/spring-core/util/sysconf"
- "github.com/lvan100/go-assert"
-)
-
-func TestSysConf(t *testing.T) {
- assert.False(t, sysconf.Has("name"))
-
- sysconf.Set("name", "Alice")
- assert.True(t, sysconf.Has("name"))
- assert.That(t, "Alice").Equal(sysconf.Get("name"))
-
- sysconf.Clear()
- assert.False(t, sysconf.Has("name"))
-
- sysconf.Set("name", "Alice")
- sysconf.Set("name.first", "Alice")
-
- p := sysconf.Clone()
- assert.That(t, p.Data()).Equal(map[string]string{"name": "Alice"})
-}
diff --git a/util/syslog/syslog.go b/util/syslog/syslog.go
deleted file mode 100644
index a3cfdc31..00000000
--- a/util/syslog/syslog.go
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright 2024 The Go-Spring Authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-/*
-Package syslog provides simplified logging utilities for tracking the execution flow
-of the go-spring framework. It is designed to offer a more convenient interface than
-the standard library's slog package, whose Info, Warn, and related methods can be
-cumbersome to use. Logs produced by this package are typically output to the console.
-*/
-package syslog
-
-import (
- "context"
- "fmt"
- "log"
- "log/slog"
- "os"
- "runtime"
- "time"
-)
-
-func init() {
- log.SetOutput(os.Stdout)
- log.SetFlags(log.Flags() | log.Lshortfile)
-}
-
-// Debugf logs a debug-level message using slog.
-func Debugf(format string, a ...any) {
- logMsg(slog.LevelDebug, format, a...)
-}
-
-// Infof logs an info-level message using slog.
-func Infof(format string, a ...any) {
- logMsg(slog.LevelInfo, format, a...)
-}
-
-// Warnf logs a warning-level message using slog.
-func Warnf(format string, a ...any) {
- logMsg(slog.LevelWarn, format, a...)
-}
-
-// Errorf logs an error-level message using slog.
-func Errorf(format string, a ...any) {
- logMsg(slog.LevelError, format, a...)
-}
-
-// logMsg constructs and logs a message at the specified log level.
-func logMsg(level slog.Level, format string, a ...any) {
- ctx := context.Background()
- if !slog.Default().Enabled(ctx, level) {
- return
- }
-
- // skip [runtime.Callers, syslog.logMsg, syslog.*f]
- var pcs [1]uintptr
- runtime.Callers(3, pcs[:])
-
- msg := fmt.Sprintf(format, a...)
- r := slog.NewRecord(time.Now(), level, msg, pcs[0])
- err := slog.Default().Handler().Handle(ctx, r)
- _ = err // ignore error
-}
diff --git a/util/syslog/syslog_test.go b/util/syslog/syslog_test.go
deleted file mode 100644
index ba66eb7e..00000000
--- a/util/syslog/syslog_test.go
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright 2024 The Go-Spring Authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package syslog_test
-
-import (
- "testing"
-
- "github.com/go-spring/spring-core/util/syslog"
-)
-
-func TestLog(t *testing.T) {
- syslog.Debugf("hello %s", "world")
- syslog.Infof("hello %s", "world")
- syslog.Warnf("hello %s", "world")
- syslog.Errorf("hello %s", "world")
-}
diff --git a/util/type.go b/util/type.go
index edee1e57..5154a3e6 100644
--- a/util/type.go
+++ b/util/type.go
@@ -23,6 +23,21 @@ import (
// errorType is the [reflect.Type] of the error interface.
var errorType = reflect.TypeFor[error]()
+// IntType is the type of int, int8, int16, int32, int64.
+type IntType interface {
+ ~int | ~int8 | ~int16 | ~int32 | ~int64
+}
+
+// UintType is the type of uint, uint8, uint16, uint32, uint64.
+type UintType interface {
+ ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
+}
+
+// FloatType is the type of float32, float64.
+type FloatType interface {
+ ~float32 | ~float64
+}
+
// IsFuncType returns true if the provided type t is a function type.
func IsFuncType(t reflect.Type) bool {
return t.Kind() == reflect.Func
diff --git a/util/value.go b/util/value.go
index b4fccd03..9d7e8c68 100644
--- a/util/value.go
+++ b/util/value.go
@@ -23,6 +23,11 @@ import (
"unsafe"
)
+// Ptr returns a pointer to the given value.
+func Ptr[T any](i T) *T {
+ return &i
+}
+
const (
flagStickyRO = 1 << 5
flagEmbedRO = 1 << 6