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 @@ license go-version release - test-coverage + + test-coverage + + Ask DeepWiki -[中文](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 @@ license go-version release - test-coverage + + test-coverage + + Ask DeepWiki -[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